From 5a25f0073d14c53ba8ff9c18087080374c09f9d5 Mon Sep 17 00:00:00 2001 From: the catalyst <adityabhatnagar14@gmail.com> Date: Thu, 16 Jan 2020 02:38:41 +0530 Subject: [PATCH 01/11] add a command to create admin users and removed admin.json file also authenticated the stories/all route using jwt --- README.md | 5 ++ fact-bounty-flask/api/app.py | 34 ++++----- fact-bounty-flask/api/commands.py | 56 ++++++++++++++- fact-bounty-flask/api/data-dev.sqlite | Bin 0 -> 32768 bytes fact-bounty-flask/api/helpers.py | 7 +- fact-bounty-flask/api/stories/controller.py | 52 ++++++++------ fact-bounty-flask/api/user/model.py | 15 +++- .../migrations/versions/02db7a3aaa95_.py | 66 ++++++++++++++++++ .../migrations/versions/78d18dc2789d_.py | 58 +++++++++++++++ fact-bounty-flask/requirements.txt | 2 +- 10 files changed, 248 insertions(+), 47 deletions(-) create mode 100644 fact-bounty-flask/api/data-dev.sqlite mode change 100644 => 100755 fact-bounty-flask/api/user/model.py create mode 100644 fact-bounty-flask/migrations/versions/02db7a3aaa95_.py create mode 100644 fact-bounty-flask/migrations/versions/78d18dc2789d_.py diff --git a/README.md b/README.md index 3a265574..63596016 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,11 @@ Run npm install in fact-bounty-client folder. export ELASTIC_SEARCH_USERNAME="" export ELASTIC_SEARCH_PASSWORD="" + export ADMIN_USERNAME = "admin" + export ADMIN_PASSWORD = "password" + export ADMIN_EMAIL = "email@email.com" + + export TZ=“Asia/Colombo” ``` diff --git a/fact-bounty-flask/api/app.py b/fact-bounty-flask/api/app.py index f8c360d2..4bfc9022 100644 --- a/fact-bounty-flask/api/app.py +++ b/fact-bounty-flask/api/app.py @@ -15,28 +15,23 @@ def create_app(config_name): - try: - # create and configure the app - app = Flask( - __name__, - static_folder="../build/static", - template_folder="../build", - ) - app.config.from_object(config[config_name]) - config[config_name].init_app(app) + # create and configure the app + app = Flask( + __name__, static_folder="../build/static", template_folder="../build" + ) + app.config.from_object(config[config_name]) + config[config_name].init_app(app) - register_extensions(app) - register_blueprint(app) - register_shellcontext(app) - register_commands(app) + register_extensions(app) + register_blueprint(app) + register_shellcontext(app) + register_commands(app) - @app.before_first_request - def create_tables(): - db.create_all() + @app.before_first_request + def create_tables(): + db.create_all() - return app - except Exception as err: - print("Error occured:", err) + return app def register_extensions(app): @@ -82,6 +77,7 @@ def register_commands(app): app.cli.add_command(commands.clean) app.cli.add_command(commands.urls) app.cli.add_command(commands.deploy) + app.cli.add_command(commands.create_admin) def register_shellcontext(app): diff --git a/fact-bounty-flask/api/commands.py b/fact-bounty-flask/api/commands.py index e99267ad..53dcd1c8 100644 --- a/fact-bounty-flask/api/commands.py +++ b/fact-bounty-flask/api/commands.py @@ -6,16 +6,39 @@ import coverage import sys import click -from flask import current_app +from flask import current_app, make_response, jsonify from flask.cli import with_appcontext from flask_migrate import upgrade from werkzeug.exceptions import MethodNotAllowed, NotFound +from .user import model +import getpass +import re COV = None if os.environ.get("FLASK_COVERAGE"): COV = coverage.coverage(branch=True, include="./*") COV.start() +""" +regex to check valid email is entered +""" +regex = r"^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$" + +""" +function for +for validating an Email +""" + + +def check(email): + """ + pass the regualar expression + and the string in search() method + """ + if re.search(regex, email): + return True + else: + return False @click.command() @@ -168,3 +191,34 @@ def deploy(): migrate database to latest revision """ upgrade() + + +@click.command(name="create_admin") +@with_appcontext +def create_admin(): + """ + create an admin user + """ + admin_username = input("Enter the admin username: ") + admin_password = getpass.getpass(prompt="Enter the admin password: ") + admin_role = "admin" + valid = False + while not valid: + admin_email = input("Enter the admin email: ") + valid = check(admin_email) + if valid: + break + else: + pass + try: + user = model.User( + name=admin_username, + email=admin_email, + password=admin_password, + role=admin_role, + ) + user.save() + except Exception as e: + print(str(e)) + response = {"message": "Something went wrong!!"} + return make_response(jsonify(response)), 500 diff --git a/fact-bounty-flask/api/data-dev.sqlite b/fact-bounty-flask/api/data-dev.sqlite new file mode 100644 index 0000000000000000000000000000000000000000..08de9227599eb6c8295e880198c571ad3a758069 GIT binary patch literal 32768 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 literal 0 HcmV?d00001 diff --git a/fact-bounty-flask/api/helpers.py b/fact-bounty-flask/api/helpers.py index 96c0d0d3..9778d726 100644 --- a/fact-bounty-flask/api/helpers.py +++ b/fact-bounty-flask/api/helpers.py @@ -11,8 +11,11 @@ def send_async_email(app, msg): def send_email(to, subject, body): app = current_app._get_current_object() - msg = Message(app.config['FACTBOUNTY_MAIL_SUBJECT_PREFIX'] + subject, - sender=app.config['FACTBOUNTY_MAIL_SENDER'], recipients=[to]) + msg = Message( + app.config["FACTBOUNTY_MAIL_SUBJECT_PREFIX"] + subject, + sender=app.config["FACTBOUNTY_MAIL_SENDER"], + recipients=[to], + ) msg.body = body thr = Thread(target=send_async_email, args=[app, msg]) thr.start() diff --git a/fact-bounty-flask/api/stories/controller.py b/fact-bounty-flask/api/stories/controller.py index 5855eb05..05bbd45f 100644 --- a/fact-bounty-flask/api/stories/controller.py +++ b/fact-bounty-flask/api/stories/controller.py @@ -4,41 +4,49 @@ from elasticsearch.helpers import scan from .model import Vote, Comment from flasgger import swag_from +from ..user import model class AllStories(MethodView): """ - Retrieve stories + Retrieve stories only for admin :return: JSON object with all stories and HTTP status code 200. """ + @jwt_required @swag_from("../../docs/stories/get_all.yml") def get(self): + user_id = get_jwt_identity() + user = model.User.find_by_user_id(user_id) - es_index = current_app.config["ES_INDEX"] - es = current_app.elasticsearch + if user.role == "admin": + es_index = current_app.config["ES_INDEX"] + es = current_app.elasticsearch - doc = { - "sort": [{"date": {"order": "desc"}}], - "query": {"match_all": {}}, - } - stories = {} - try: - for story in scan(es, doc, index=es_index, doc_type="story"): - PID = story["_id"] - source = story["_source"] - stories[PID] = source - except Exception as e: - # An error occured, so return a string message containing error - response = {"message": str(e)} - return make_response(jsonify(response)), 500 + doc = { + "sort": [{"date": {"order": "desc"}}], + "query": {"match_all": {}}, + } + stories = {} + try: + for story in scan(es, doc, index=es_index, doc_type="story"): + PID = story["_id"] + source = story["_source"] + stories[PID] = source + except Exception as e: + # An error occured, so return a string message containing error + response = {"message": str(e)} + return make_response(jsonify(response)), 500 - response = { - "message": "Stories successfully fetched", - "stories": stories, - } - return make_response(jsonify(response)), 200 + response = { + "message": "Stories successfully fetched", + "stories": stories, + } + return make_response(jsonify(response)), 200 + else: + response = {"message": "only for admins"} + return make_response(jsonify(response)), 400 class GetRange(MethodView): diff --git a/fact-bounty-flask/api/user/model.py b/fact-bounty-flask/api/user/model.py old mode 100644 new mode 100755 index 8fb28dc7..623a9315 --- a/fact-bounty-flask/api/user/model.py +++ b/fact-bounty-flask/api/user/model.py @@ -22,8 +22,9 @@ class User(Model): date = Column(db.DateTime, default=datetime.now()) votes = db.relationship("Vote", backref=db.backref("user")) type = Column(db.String(50), default="remote") + role = Column(db.String(10), default="user") - def __init__(self, name, email, password, _type="remote"): + def __init__(self, id, name, email, password, role="user", _type="remote"): """ Initializes the user instance """ @@ -32,6 +33,7 @@ def __init__(self, name, email, password, _type="remote"): if password: self.password = Bcrypt().generate_password_hash(password).decode() self.type = _type + self.role = role def __repr__(self): """ @@ -45,13 +47,22 @@ def to_json(self): :return: user JSON object """ - user_json = {"name": self.name, "email": self.email, "date": self.date} + user_json = { + "name": self.name, + "email": self.email, + "date": self.date, + "role": self.role, + } return user_json @classmethod def find_by_email(cls, email): return cls.query.filter_by(email=email).first() + @classmethod + def find_by_user_id(cls, id): + return cls.query.filter_by(id=id).first() + def verify_password(self, password): """ Verify the password diff --git a/fact-bounty-flask/migrations/versions/02db7a3aaa95_.py b/fact-bounty-flask/migrations/versions/02db7a3aaa95_.py new file mode 100644 index 00000000..a10bf7ff --- /dev/null +++ b/fact-bounty-flask/migrations/versions/02db7a3aaa95_.py @@ -0,0 +1,66 @@ +"""empty message + +Revision ID: 02db7a3aaa95 +Revises: 78d18dc2789d +Create Date: 2020-01-25 11:13:21.447023 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "02db7a3aaa95" +down_revision = "78d18dc2789d" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "revoked_tokens", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("jti", sa.String(length=120), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "user", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("name", sa.String(length=80), nullable=False), + sa.Column("password", sa.String(length=128), nullable=True), + sa.Column("email", sa.String(length=100), nullable=False), + sa.Column("date", sa.DateTime(), nullable=True), + sa.Column("type", sa.String(length=50), nullable=True), + sa.Column("role", sa.String(length=60), nullable=True), + sa.PrimaryKeyConstraint("id"), + sa.UniqueConstraint("email"), + ) + op.create_table( + "comment", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("story_id", sa.String(length=128), nullable=True), + sa.Column("user_id", sa.Integer(), nullable=True), + sa.Column("content", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(["user_id"], ["user.id"],), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "vote", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("story_id", sa.String(length=128), nullable=True), + sa.Column("user_id", sa.Integer(), nullable=True), + sa.Column("value", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(["user_id"], ["user.id"],), + sa.PrimaryKeyConstraint("id"), + ) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_table("vote") + op.drop_table("comment") + op.drop_table("user") + op.drop_table("revoked_tokens") + # ### end Alembic commands ### diff --git a/fact-bounty-flask/migrations/versions/78d18dc2789d_.py b/fact-bounty-flask/migrations/versions/78d18dc2789d_.py new file mode 100644 index 00000000..1d7f5203 --- /dev/null +++ b/fact-bounty-flask/migrations/versions/78d18dc2789d_.py @@ -0,0 +1,58 @@ +"""empty message + +Revision ID: 78d18dc2789d +Revises: d2fc5d7ff47e +Create Date: 2020-01-25 10:50:19.720464 + +""" +from alembic import op +import sqlalchemy as sa + + +# revision identifiers, used by Alembic. +revision = "78d18dc2789d" +down_revision = "d2fc5d7ff47e" +branch_labels = None +depends_on = None + + +def upgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.create_table( + "revoked_tokens", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("jti", sa.String(length=120), nullable=True), + sa.PrimaryKeyConstraint("id"), + ) + op.create_table( + "comment", + sa.Column("id", sa.Integer(), nullable=False), + sa.Column("story_id", sa.String(length=128), nullable=True), + sa.Column("user_id", sa.Integer(), nullable=True), + sa.Column("content", sa.Integer(), nullable=True), + sa.ForeignKeyConstraint(["user_id"], ["user.id"],), + sa.PrimaryKeyConstraint("id"), + ) + op.add_column( + "user", sa.Column("role", sa.String(length=60), nullable=True) + ) + op.alter_column( + "user", "password", existing_type=sa.VARCHAR(length=128), nullable=True + ) + op.create_unique_constraint(None, "user", ["email"]) + # ### end Alembic commands ### + + +def downgrade(): + # ### commands auto generated by Alembic - please adjust! ### + op.drop_constraint(None, "user", type_="unique") + op.alter_column( + "user", + "password", + existing_type=sa.VARCHAR(length=128), + nullable=False, + ) + op.drop_column("user", "role") + op.drop_table("comment") + op.drop_table("revoked_tokens") + # ### end Alembic commands ### diff --git a/fact-bounty-flask/requirements.txt b/fact-bounty-flask/requirements.txt index 08ad1896..734b3fff 100644 --- a/fact-bounty-flask/requirements.txt +++ b/fact-bounty-flask/requirements.txt @@ -43,7 +43,7 @@ SQLAlchemy==1.2.18 tornado==6.0 tweepy==3.8.0 typed-ast==1.2.0 -urllib3==1.24.1 +urllib3==1.23.0 Werkzeug==0.14.1 wrapt==1.11.1 WTForms==2.2.1 From f1e4d5436b43fdb482383c79bbc4853af17eb245 Mon Sep 17 00:00:00 2001 From: hv7214 <hverma1@cs.iitr.ac.in> Date: Fri, 24 Jan 2020 11:41:26 +0530 Subject: [PATCH 02/11] feat(server/user): added forgot password functionality --- README.md | 10 ++ fact-bounty-flask/api/user/controller.py | 139 ++++++++++++++++++++++- fact-bounty-flask/api/user/model.py | 27 ++++- fact-bounty-flask/api/user/views.py | 17 +++ 4 files changed, 188 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 63596016..d92bfd24 100644 --- a/README.md +++ b/README.md @@ -77,6 +77,15 @@ Run npm install in fact-bounty-client folder. export ELASTIC_SEARCH_URL="" export ELASTIC_SEARCH_USERNAME="" export ELASTIC_SEARCH_PASSWORD="" + + export TZ="Asia/Colombo" + + export MAIL_USERNAME="" + export MAIL_PASSWORD="" + export FACTBOUNTY_ADMIN="" + export MAIL_PORT="587" + export MAIL_USE_TLS="true" + export MAIL_SERVER="smtp.gmail.com" export ADMIN_USERNAME = "admin" export ADMIN_PASSWORD = "password" @@ -216,3 +225,4 @@ And use [localhost:3000](https://) to browse. ## License [](https://app.fossa.io/projects/git%2Bgithub.com%2Fscorelab%2Ffact-Bounty?ref=badge_large) + diff --git a/fact-bounty-flask/api/user/controller.py b/fact-bounty-flask/api/user/controller.py index 0f1485e6..421e2185 100644 --- a/fact-bounty-flask/api/user/controller.py +++ b/fact-bounty-flask/api/user/controller.py @@ -1,5 +1,5 @@ from flask.views import MethodView -from flask import make_response, request, jsonify +from flask import make_response, request, jsonify, current_app from flask_jwt_extended import ( create_access_token, create_refresh_token, @@ -10,6 +10,9 @@ ) from .model import User, RevokedToken from flasgger import swag_from +import smtplib +from email.mime.multipart import MIMEMultipart +from email.mime.text import MIMEText class Register(MethodView): @@ -50,7 +53,8 @@ def post(self): try: user = User(email=email, password=password, name=name) user.save() - except Exception: + except Exception as err: + print("Error occured: ", err) response = {"message": "Something went wrong!!"} return make_response(jsonify(response)), 500 @@ -72,7 +76,8 @@ def post(self): try: email = data["email"] password = data["password"] - except Exception: + except Exception as err: + print("Error occured: ", err) response = {"message": "Please provide all the required fields."} return make_response(jsonify(response)), 404 @@ -109,6 +114,129 @@ def post(self): return make_response(jsonify(response)), 200 +class ForgotPassword(MethodView): + """This class sends a reset password mail.""" + + def post(self): + """Handle POST request for this view. Url --> /api/users/forgot_password""" + # getting JSON data from request + post_data = request.get_json(silent=True) + + try: + email = post_data["email"] + except Exception: + response = {"message": "Please provide email."} + return make_response(jsonify(response)), 404 + + # Querying the database with requested email + user = User.find_by_email(email) + if user: + verification_token = user.verification_token + # Body of the email + mail_content = ( + """Hi, + + You are receiving this because you have requested to reset password for your account. + + Here is you verification token: """ + + verification_token + ) + # The email addresses and password + sender_address = current_app.config["MAIL_USERNAME"] + sender_pass = current_app.config["MAIL_PASSWORD"] + receiver_address = user.email + # Setup the MIME + message = MIMEMultipart() + message["From"] = sender_address + message["To"] = receiver_address + message["Subject"] = "Password reset" + # The body and the attachments for the mail + message.attach(MIMEText(mail_content, "plain")) + # Create SMTP session for sending the mail + session = smtplib.SMTP( + current_app.config["MAIL_SERVER"], + current_app.config["MAIL_PORT"], + ) # use gmail with port + session.starttls() # enable security + session.login( + sender_address, sender_pass + ) # login with mail_id and password + text = message.as_string() + session.sendmail(sender_address, receiver_address, text) + session.quit() + print("Password reset email sent successfully") + + response = { + "message": "Verification token has been sent to you on your registered email" + } + + # return a response notifying the user that a password reset mail has + # been sent to registered email + return make_response(jsonify(response)), 200 + else: + response = {"message": "Email not registered"} + return make_response(jsonify(response)), 202 + + +class AuthVerificationToken(MethodView): + """This class verifies token which is being used to reset the password""" + + def post(self): + data = request.get_json(silent=True) + try: + verification_token = data["verification_token"] + except Exception: + response = {"message": "Please provide verification token."} + return make_response(jsonify(response)), 404 + + # since token is unique, find user by its verification token + # if it exist it means input token is correct + user = User.find_by_verification_token(verification_token) + + if user: + response = {"message": "Auth success"} + return make_response(jsonify(response)), 200 + else: + response = {"message": "Incorrect, please check again"} + return make_response(jsonify(response)), 202 + + +class ResetPassword(MethodView): + """This class is used to change the password""" + + def post(self): + + data = request.get_json(silent=True) + try: + verification_token = data["verificationToken"] + new_password = data["password"] + new_password_confirm = data["password2"] + except Exception: + response = {"message": "Please provide all required fields."} + return make_response(jsonify(response)), 202 + + # find user by token + user = User.find_by_verification_token(verification_token) + + # if token is correct + if user: + # new password and confirm has to be equal + if new_password != new_password_confirm: + response = { + "message": "New password and confirm password does not match" + } + return make_response(jsonify(response)), 202 + # overwrite old password with new password + else: + user.password = user.generate_password_hash(new_password) + user.save() + response = {"message": "Password successfully changed"} + return make_response(jsonify(response)), 200 + else: + response = {"message": "Unauthorized"} + return make_response(jsonify(response)), 401 + + class Auth(MethodView): """This class-based view handles user register and access token generation \ via 3rd sources like facebook, google""" @@ -218,6 +346,11 @@ def post(self): userController = { "register": Register.as_view("register"), "login": Login.as_view("login"), + "forgot_password": ForgotPassword.as_view("forgot_password"), + "auth_verification_token": AuthVerificationToken.as_view( + "auth_verification_token" + ), + "reset_password": ResetPassword.as_view("reset_password"), "auth": Auth.as_view("auth"), "logout_access": LogoutAccess.as_view("logout_access"), "logout_refresh": LogoutRefresh.as_view("logout_refresh"), diff --git a/fact-bounty-flask/api/user/model.py b/fact-bounty-flask/api/user/model.py index 623a9315..76c0a19c 100755 --- a/fact-bounty-flask/api/user/model.py +++ b/fact-bounty-flask/api/user/model.py @@ -1,6 +1,7 @@ from datetime import datetime from flask import current_app from flask_bcrypt import Bcrypt +from uuid import uuid4 # from itsdangerous import ( # TimedJSONWebSignatureSerializer as Serializer, @@ -18,6 +19,7 @@ class User(Model): id = Column(db.Integer, primary_key=True) name = Column(db.String(80), nullable=False) password = Column(db.String(128)) + verification_token = Column(db.String(128), nullable=False, unique=True) email = Column(db.String(100), nullable=False, unique=True) date = Column(db.DateTime, default=datetime.now()) votes = db.relationship("Vote", backref=db.backref("user")) @@ -30,8 +32,9 @@ def __init__(self, id, name, email, password, role="user", _type="remote"): """ self.name = name self.email = email + self.verification_token = User.generate_token() if password: - self.password = Bcrypt().generate_password_hash(password).decode() + self.password = User.generate_password_hash(password) self.type = _type self.role = role @@ -63,6 +66,26 @@ def find_by_email(cls, email): def find_by_user_id(cls, id): return cls.query.filter_by(id=id).first() + @classmethod + def find_by_verification_token(cls, verification_token): + return cls.query.filter_by( + verification_token=verification_token + ).first() + + @staticmethod + def generate_token(): + """ + Returns a random token + """ + return uuid4().hex + + @staticmethod + def generate_password_hash(password): + """ + Returns hash of password + """ + return Bcrypt().generate_password_hash(password).decode() + def verify_password(self, password): """ Verify the password @@ -82,7 +105,7 @@ def save(self): class RevokedToken(Model): """ - This model holds information about revoked tokens, users who have looged out + This model holds information about revoked tokens, users who have logged out """ __tablename__ = "revoked_tokens" diff --git a/fact-bounty-flask/api/user/views.py b/fact-bounty-flask/api/user/views.py index ea9213da..82a45048 100644 --- a/fact-bounty-flask/api/user/views.py +++ b/fact-bounty-flask/api/user/views.py @@ -10,6 +10,23 @@ userprint.add_url_rule( "/register", view_func=userController["register"], methods=["POST"] ) +userprint.add_url_rule( + "/forgot_password", + view_func=userController["forgot_password"], + methods=["POST"], +) + +userprint.add_url_rule( + "/auth_verification_token", + view_func=userController["auth_verification_token"], + methods=["POST"], +) + +userprint.add_url_rule( + "/reset_password", + view_func=userController["reset_password"], + methods=["POST"], +) userprint.add_url_rule( "/oauth", view_func=userController["auth"], methods=["POST"] From 1329797b9949802d79f8fffa206fe506621f8cd1 Mon Sep 17 00:00:00 2001 From: lukasz-zbrzeski <lukasz.zbrzeski1@gmail.com> Date: Sat, 18 Jan 2020 15:54:25 +0100 Subject: [PATCH 03/11] Update README.md --- README.md | 60 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) diff --git a/README.md b/README.md index d92bfd24..479cab1e 100644 --- a/README.md +++ b/README.md @@ -216,6 +216,66 @@ And use [localhost:3000](https://) to browse. * #### Once build completes, run `docker-compose up` +## Setting up an OAuth Daemon + +## Step 1: Pre-requisite + +### Technologies required + +* **[Node.js](http://nodejs.org/)** +* **[Redis](http://redis.io/download)** + +You should also install the `grunt-cli` npm package with the following command: +```sh +$ (sudo) npm install -g grunt-cli +``` + +## Step 2: Install OAuthD +You should install OAuthD from npm, by executing the following command: +```sh +$ (sudo) npm install -g oauthd +``` + +Then check the correctness of the OAuthD installation with the following command: +```sh +$ oauthd -v +``` + +## Step 3: Create an instance + +Go into a folder where you want to create your oauthd instance and run the following command: +```sh +$ oauthd init +``` + +Follow the prompt instructions. It will guide you in the instance creation. You need to answer 'Y' or just press enter when the prompt ask you: +```sh +oauthd> Do you want to install default plugins? (Y|n)> (Y) +``` + +## Step 4: Run instance +To run the instance, just run the following command: +```sh +$ cd myinstance && oauthd start +``` + +You should see something like this in your shell: +```sh +Initializing plugins engine +Loading auth +Loading request +Loading slashme +Loading statistics +Loading front +oauthd start server +oauthd listening at http://0.0.0.0:6284 for http://localhost:6284 +Server is ready (load time: 0.9s) +``` + +Then go to browser and type http://localhost:6284/ + +Learn more about the OAuthD [configuration](https://github.com/oauth-io/oauthd/wiki/Configuration) and the [command line features](https://github.com/oauth-io/oauthd/wiki/Command-Line-Interface). + # How to Contribute - First fork the repository and clone it. From fb8713285b273ea77677311be824b142c55a6460 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Zbrzeski?= <lukasz.zbrzeski1@gmail.com> Date: Thu, 23 Jan 2020 17:24:01 +0100 Subject: [PATCH 04/11] Update README.md --- README.md | 58 +------------------------------------------------------ 1 file changed, 1 insertion(+), 57 deletions(-) diff --git a/README.md b/README.md index 479cab1e..d69415bb 100644 --- a/README.md +++ b/README.md @@ -218,63 +218,7 @@ And use [localhost:3000](https://) to browse. ## Setting up an OAuth Daemon -## Step 1: Pre-requisite - -### Technologies required - -* **[Node.js](http://nodejs.org/)** -* **[Redis](http://redis.io/download)** - -You should also install the `grunt-cli` npm package with the following command: -```sh -$ (sudo) npm install -g grunt-cli -``` - -## Step 2: Install OAuthD -You should install OAuthD from npm, by executing the following command: -```sh -$ (sudo) npm install -g oauthd -``` - -Then check the correctness of the OAuthD installation with the following command: -```sh -$ oauthd -v -``` - -## Step 3: Create an instance - -Go into a folder where you want to create your oauthd instance and run the following command: -```sh -$ oauthd init -``` - -Follow the prompt instructions. It will guide you in the instance creation. You need to answer 'Y' or just press enter when the prompt ask you: -```sh -oauthd> Do you want to install default plugins? (Y|n)> (Y) -``` - -## Step 4: Run instance -To run the instance, just run the following command: -```sh -$ cd myinstance && oauthd start -``` - -You should see something like this in your shell: -```sh -Initializing plugins engine -Loading auth -Loading request -Loading slashme -Loading statistics -Loading front -oauthd start server -oauthd listening at http://0.0.0.0:6284 for http://localhost:6284 -Server is ready (load time: 0.9s) -``` - -Then go to browser and type http://localhost:6284/ - -Learn more about the OAuthD [configuration](https://github.com/oauth-io/oauthd/wiki/Configuration) and the [command line features](https://github.com/oauth-io/oauthd/wiki/Command-Line-Interface). +[How to setup OAuth Daemon](OAuthdSetup.md) # How to Contribute From 8fe02647231cb6ddb67c34d9c015dcb67d641212 Mon Sep 17 00:00:00 2001 From: RAJPRAKASH <42652941+rajprakash00@users.noreply.github.com> Date: Wed, 12 Feb 2020 22:34:51 +0530 Subject: [PATCH 05/11] Update README.md --- README.md | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index d69415bb..a8f62c54 100644 --- a/README.md +++ b/README.md @@ -121,12 +121,12 @@ Run npm install in fact-bounty-client folder. ### How to install Elasticsearch and start elasticsearch server -* #### Elasticsearch v6.7.0 can be installed as follows: +* #### Elasticsearch v7.6.0 can be installed as follows: ``` - (venv)$ wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.7.0.deb - (venv)$ wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-6.7.0.deb.sha512 - (venv)$ shasum -a 512 -c elasticsearch-6.7.0.deb.sha512 - (venv)$ sudo dpkg -i elasticsearch-6.7.0.deb + (venv)$ wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.6.0.deb + (venv)$ wget https://artifacts.elastic.co/downloads/elasticsearch/elasticsearch-7.6.0.deb.sha512 + (venv)$ shasum -a 512 -c elasticsearch-7.6.0.deb.sha512 + (venv)$ sudo dpkg -i elasticsearch-7.6.0.deb ``` From d7045d1b15f4d6b9f03e0576b4ad14773742fdea Mon Sep 17 00:00:00 2001 From: hv7214 <hverma1@cs.iitr.ac.in> Date: Mon, 20 Jan 2020 15:13:00 +0530 Subject: [PATCH 06/11] feat(client/ForgotPassword): Made different components and routes --- fact-bounty-client/src/AppRouter.js | 9 +- .../pages/ForgotPassword/ForgotPassword.jsx | 255 ++++++++++++++++ .../ForgotPassword/ForgotPassword.style.js | 32 ++ .../src/pages/ForgotPassword/index.js | 2 + fact-bounty-client/src/pages/Login/Login.jsx | 8 +- .../src/pages/ResetPassword/ResetPassword.jsx | 279 ++++++++++++++++++ .../ResetPassword/ResetPassword.style.js | 32 ++ .../src/pages/ResetPassword/index.js | 2 + .../src/redux/actions/actionTypes.js | 2 + .../src/redux/actions/authActions.js | 95 +++++- .../src/redux/actions/successActions.js | 4 + .../src/redux/reducers/index.js | 2 + .../src/redux/reducers/successReducers.js | 16 + .../src/services/AuthService.js | 28 +- 14 files changed, 762 insertions(+), 4 deletions(-) create mode 100644 fact-bounty-client/src/pages/ForgotPassword/ForgotPassword.jsx create mode 100644 fact-bounty-client/src/pages/ForgotPassword/ForgotPassword.style.js create mode 100644 fact-bounty-client/src/pages/ForgotPassword/index.js create mode 100644 fact-bounty-client/src/pages/ResetPassword/ResetPassword.jsx create mode 100644 fact-bounty-client/src/pages/ResetPassword/ResetPassword.style.js create mode 100644 fact-bounty-client/src/pages/ResetPassword/index.js create mode 100644 fact-bounty-client/src/redux/actions/successActions.js create mode 100644 fact-bounty-client/src/redux/reducers/successReducers.js diff --git a/fact-bounty-client/src/AppRouter.js b/fact-bounty-client/src/AppRouter.js index dbbff8c5..f2fc5fab 100755 --- a/fact-bounty-client/src/AppRouter.js +++ b/fact-bounty-client/src/AppRouter.js @@ -13,7 +13,8 @@ import Dashboard from './pages/Dashboard' import Posts from './pages/Posts' import PostDetailView from './pages/PostDetailView' import Tweets from './pages/Tweets' - +import ForgotPassword from './pages/ForgotPassword' +import ResetPassword from './pages/ResetPassword' class AppRouter extends Component { render() { return ( @@ -23,6 +24,12 @@ class AppRouter extends Component { <Route exact path="/" component={Landing} /> <Route exact path="/register" component={Register} /> <Route exact path="/login" component={Login} /> + <Route exact path="/forgotpassword" component={ForgotPassword} /> + <Route + exact + path="/resetpassword/:verificationToken" + component={ResetPassword} + /> <Route path="/dashboard" component={Dashboard} /> <Route exact path="/posts" component={Posts} /> <Route exact path="/tweets" component={Tweets} /> diff --git a/fact-bounty-client/src/pages/ForgotPassword/ForgotPassword.jsx b/fact-bounty-client/src/pages/ForgotPassword/ForgotPassword.jsx new file mode 100644 index 00000000..52c89977 --- /dev/null +++ b/fact-bounty-client/src/pages/ForgotPassword/ForgotPassword.jsx @@ -0,0 +1,255 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import { connect } from 'react-redux' +import classnames from 'classnames' +import compose from 'recompose/compose' +import Avatar from '@material-ui/core/Avatar' +import Button from '@material-ui/core/Button' +import CssBaseline from '@material-ui/core/CssBaseline' +import FormControl from '@material-ui/core/FormControl' +import Input from '@material-ui/core/Input' +import InputLabel from '@material-ui/core/InputLabel' +import LockOutlinedIcon from '@material-ui/icons/LockOutlined' +import Paper from '@material-ui/core/Paper' +import Typography from '@material-ui/core/Typography' +import withStyles from '@material-ui/core/styles/withStyles' +import Toast from '../../components/Toast' +import { updateError } from '../../redux/actions/errorActions' +import { updateSuccess } from '../../redux/actions/successActions' +import { + forgotPassword, + authVerificationToken +} from '../../redux/actions/authActions' +import styles from './ForgotPassword.style' + +class ForgotPassword extends Component { + constructor() { + super() + this.state = { + email: '', + verificationToken: '', + errors: {}, + success: {}, + emailValid: false, + verificationTokenValid: false, + formValid: false, + openToast: false + } + } + + componentDidMount() { + // If logged in and user navigates to Register page, should redirect them to dashboard + this.props.updateError({}) + this.props.updateSuccess({}) + if (this.props.auth.isAuthenticated) { + this.props.history.push('/dashboard') + } + } + + static getDerivedStateFromProps(props, state) { + if (props.errors) { + const errors = props.errors + let openToast = false + if (errors.fetch) { + openToast = true + } + return { errors, openToast } + } + if (props.success) { + const success = props.success + let openToast = false + if (success.fetch) { + openToast = true + } + return { success, openToast } + } + return null + } + + onChange = e => { + let { id, value } = e.target + this.setState({ [id]: value }, () => { + this.validateField(id, value) + }) + } + + validateField = (fieldname, value) => { + let { emailValid, verificationTokenValid, errors } = this.state + + emailValid = value.match(/^([\w.%+-]+)@([\w-]+\.)+([\w]{2,})$/i) + verificationTokenValid = !!this.state.verificationToken + switch (fieldname) { + case 'email': + errors.email = emailValid ? '' : 'Invalid E-mail' + break + default: + break + } + this.setState( + { + errors, + emailValid, + verificationTokenValid + }, + this.validateForm + ) + } + + validateForm = () => { + this.setState({ + formValid: this.state.emailValid || this.state.verificationToken !== '' + }) + } + closeToast = () => { + // Remove error from store + this.props.updateError({}) + this.props.updateSuccess({}) + this.setState({ openToast: false }) + } + + onSubmit = e => { + e.preventDefault() + // Remove error from store + this.props.updateSuccess({}) + this.props.updateError({}) + + const { email } = this.state + if (!this.props.success.message) { + this.props.forgotPassword({ email: email }) + } else { + this.props.authVerificationToken( + { + verification_token: this.state.verificationToken + }, + this.props.history + ) + } + } + + render() { + const { errors, openToast } = this.state + var formInput, buttonName + if (!this.props.success.message) { + formInput = ( + <FormControl margin="normal" required fullWidth> + <InputLabel htmlFor="email">Email Address</InputLabel> + <Input + autoComplete="on" + onChange={this.onChange} + validate + value={this.state.email} + error={!!errors.email} + id="email" + type="email" + className={classnames('', { + invalid: errors.email + })} + /> + <Typography component="span" variant="caption" color="error"> + {errors.email} + </Typography> + </FormControl> + ) + buttonName = 'Send verification code' + } else { + formInput = ( + <FormControl margin="normal" required fullWidth> + <InputLabel htmlFor="verificationToken"> + Verification token + </InputLabel> + <Input + autoComplete="on" + onChange={this.onChange} + value={this.state.verificationToken} + id="verificationToken" + /> + </FormControl> + ) + buttonName = 'Verify token' + } + return ( + <main className={this.props.classes.main}> + <CssBaseline /> + <Paper className={this.props.classes.paper}> + {errors.fetch ? ( + <Toast + open={openToast} + onClose={this.closeToast} + message="Something went wrong, Try again later" + variant="error" + /> + ) : null} + <Avatar className={this.props.classes.avatar}> + <LockOutlinedIcon /> + </Avatar> + <Typography component="h1" variant="h5"> + Forgot Password + </Typography> + + <form + noValidate + onSubmit={this.onSubmit} + className={this.props.classes.form} + > + <Typography component="span" variant="caption" color="error"> + {typeof this.props.errors === 'object' + ? this.props.errors.message + : null} + </Typography> + <Typography + component="span" + variant="caption" + color="primary" + align="center" + > + {typeof this.props.success === 'object' + ? this.props.success.message + : null} + </Typography> + {formInput} + <Button + type="submit" + fullWidth + variant="contained" + color="primary" + className={this.props.classes.submit} + disabled={!this.state.formValid} + > + {buttonName} + </Button> + </form> + </Paper> + </main> + ) + } +} + +ForgotPassword.propTypes = { + forgotPassword: PropTypes.func.isRequired, + authVerificationToken: PropTypes.func.isRequired, + auth: PropTypes.object.isRequired, + errors: PropTypes.object.isRequired, + success: PropTypes.object.isRequired, + history: PropTypes.object, + classes: PropTypes.object, + updateError: PropTypes.func.isRequired, + updateSuccess: PropTypes.func.isRequired +} + +const mapStateToProps = state => ({ + auth: state.auth, + errors: state.errors, + success: state.success +}) + +export default compose( + withStyles(styles, { + name: 'ForgotPassword' + }), + connect(mapStateToProps, { + forgotPassword, + authVerificationToken, + updateError, + updateSuccess + }) +)(ForgotPassword) diff --git a/fact-bounty-client/src/pages/ForgotPassword/ForgotPassword.style.js b/fact-bounty-client/src/pages/ForgotPassword/ForgotPassword.style.js new file mode 100644 index 00000000..4af949a6 --- /dev/null +++ b/fact-bounty-client/src/pages/ForgotPassword/ForgotPassword.style.js @@ -0,0 +1,32 @@ +export default theme => ({ + main: { + width: 'auto', + display: 'block', // Fix IE 11 issue. + marginLeft: theme.spacing.unit * 3, + marginRight: theme.spacing.unit * 3, + [theme.breakpoints.up(400 + theme.spacing.unit * 3 * 2)]: { + width: 400, + marginLeft: 'auto', + marginRight: 'auto' + } + }, + paper: { + marginTop: theme.spacing.unit * 12, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: `${theme.spacing.unit * 2}px ${theme.spacing.unit * 3}px ${theme + .spacing.unit * 3}px` + }, + avatar: { + margin: theme.spacing.unit, + backgroundColor: theme.palette.secondary.main + }, + form: { + width: '100%', // Fix IE 11 issue. + marginTop: theme.spacing.unit + }, + submit: { + marginTop: theme.spacing.unit * 3 + } +}) diff --git a/fact-bounty-client/src/pages/ForgotPassword/index.js b/fact-bounty-client/src/pages/ForgotPassword/index.js new file mode 100644 index 00000000..7a92c0ce --- /dev/null +++ b/fact-bounty-client/src/pages/ForgotPassword/index.js @@ -0,0 +1,2 @@ +import ForgotPassword from './ForgotPassword' +export default ForgotPassword diff --git a/fact-bounty-client/src/pages/Login/Login.jsx b/fact-bounty-client/src/pages/Login/Login.jsx index b067d9e6..1efed1b4 100644 --- a/fact-bounty-client/src/pages/Login/Login.jsx +++ b/fact-bounty-client/src/pages/Login/Login.jsx @@ -228,7 +228,13 @@ class Login extends Component { > Login </Button> - + <p style={{ textAlign: 'center' }}> + {' '} + <br /> + <Link component={RouterLink} to="/forgotpassword"> + Forgot password ? + </Link> + </p> <div style={{ width: '100%', diff --git a/fact-bounty-client/src/pages/ResetPassword/ResetPassword.jsx b/fact-bounty-client/src/pages/ResetPassword/ResetPassword.jsx new file mode 100644 index 00000000..8c25b4f9 --- /dev/null +++ b/fact-bounty-client/src/pages/ResetPassword/ResetPassword.jsx @@ -0,0 +1,279 @@ +import React, { Component } from 'react' +import PropTypes from 'prop-types' +import { connect } from 'react-redux' +import classnames from 'classnames' +import IconButton from '@material-ui/core/IconButton' +import InputAdornment from '@material-ui/core/InputAdornment' +import Visibility from '@material-ui/icons/Visibility' +import VisibilityOff from '@material-ui/icons/VisibilityOff' +import compose from 'recompose/compose' +import Avatar from '@material-ui/core/Avatar' +import Button from '@material-ui/core/Button' +import CssBaseline from '@material-ui/core/CssBaseline' +import FormControl from '@material-ui/core/FormControl' +import Input from '@material-ui/core/Input' +import InputLabel from '@material-ui/core/InputLabel' +import LockOutlinedIcon from '@material-ui/icons/LockOutlined' +import Paper from '@material-ui/core/Paper' +import Typography from '@material-ui/core/Typography' +import withStyles from '@material-ui/core/styles/withStyles' +import Toast from '../../components/Toast' +import { updateError } from '../../redux/actions/errorActions' +import { resetPassword } from '../../redux/actions/authActions' +import styles from './ResetPassword.style' + +class Register extends Component { + constructor() { + super() + this.state = { + verificationToken: '', + password: '', + password2: '', + errors: {}, + showPassword: false, + showPassword2: false, + passwordValid: false, + password2Valid: false, + formValid: false, + openToast: false + } + } + + componentDidMount() { + this.props.updateError({}) + // set verification token + const { + match: { params } + } = this.props + this.setState({ + verificationToken: params.verificationToken + }) + + // If logged in and user navigates to Register page, should redirect them to dashboard + if (this.props.auth.isAuthenticated) { + this.props.history.push('/dashboard') + } + } + + static getDerivedStateFromProps(props) { + if (props.errors) { + const errors = props.errors + let openToast = false + if (errors.fetch) { + openToast = true + } + return { errors, openToast } + } + return null + } + + onChange = e => { + let { id, value } = e.target + this.setState({ [id]: value }, () => { + this.validateField(id, value) + }) + } + + handleClickShowPassword = () => { + this.setState(state => ({ showPassword: !state.showPassword })) + } + + handleClickShowPassword2 = () => { + this.setState(state => ({ showPassword2: !state.showPassword2 })) + } + + validateField = (fieldName, value) => { + let { passwordValid, password2Valid, errors } = this.state + + switch (fieldName) { + case 'password': + passwordValid = value.length >= 8 + errors.password = passwordValid ? '' : 'Too short!' + password2Valid = value === this.state.password2 + if (password2Valid && passwordValid) { + errors.password2 = null + } + break + case 'password2': + password2Valid = value === this.state.password + errors.password2 = password2Valid ? '' : "Password don't match" + break + default: + break + } + this.setState( + { + errors, + passwordValid, + password2Valid + }, + this.validateForm + ) + } + + validateForm = () => { + this.setState({ + formValid: this.state.passwordValid && this.state.password2Valid + }) + } + closeToast = () => { + // Remove error from store + this.props.updateError({}) + this.setState({ openToast: false }) + } + + onSubmit = e => { + e.preventDefault() + // Remove error from store + const { password, password2, verificationToken } = this.state + + if (password === password2) { + const data = { + password, + password2, + verificationToken + } + this.props.resetPassword(data, this.props.history) + } else { + const passwordError = 'Password dont match' + const errors = { + password2: password !== password2 ? passwordError : '' + } + this.props.updateError(errors) + } + } + + render() { + const { errors, openToast } = this.state + return ( + <main className={this.props.classes.main}> + <CssBaseline /> + <Paper className={this.props.classes.paper}> + {errors.fetch ? ( + <Toast + open={openToast} + onClose={this.closeToast} + message="Something went wrong, Try again later" + variant="error" + /> + ) : null} + <Avatar className={this.props.classes.avatar}> + <LockOutlinedIcon /> + </Avatar> + <Typography component="h1" variant="h5"> + Reset Password + </Typography> + + <form + noValidate + onSubmit={this.onSubmit} + className={this.props.classes.form} + > + <Typography component="span" variant="caption" color="error"> + {typeof this.props.errors === 'object' + ? this.props.errors.message + : null} + </Typography> + + <FormControl margin="normal" required fullWidth> + <InputLabel htmlFor="password">New Password</InputLabel> + <Input + autoComplete="on" + onChange={this.onChange} + value={this.state.password} + error={!!errors.password} + id="password" + type={this.state.showPassword ? 'text' : 'password'} + className={classnames('', { + invalid: errors.password + })} + endAdornment={ + <InputAdornment position="end"> + <IconButton + aria-label="Toggle password visibility" + onClick={this.handleClickShowPassword} + > + {this.state.showPassword ? ( + <Visibility /> + ) : ( + <VisibilityOff /> + )} + </IconButton> + </InputAdornment> + } + /> + <Typography component="span" variant="caption" color="error"> + {errors.password} + </Typography> + </FormControl> + + <FormControl margin="normal" required fullWidth> + <InputLabel htmlFor="password2">Confirm New Password</InputLabel> + <Input + autoComplete="on" + onChange={this.onChange} + value={this.state.password2} + error={!!errors.password2} + id="password2" + type={this.state.showPassword2 ? 'text' : 'password'} + className={classnames('', { + invalid: errors.password2 + })} + endAdornment={ + <InputAdornment position="end"> + <IconButton + aria-label="Toggle password visibility" + onClick={this.handleClickShowPassword2} + > + {this.state.showPassword2 ? ( + <Visibility /> + ) : ( + <VisibilityOff /> + )} + </IconButton> + </InputAdornment> + } + /> + <Typography component="span" variant="caption" color="error"> + {errors.password2} + </Typography> + </FormControl> + + <Button + type="submit" + fullWidth + variant="contained" + color="primary" + className={this.props.classes.submit} + disabled={!this.state.formValid} + > + Change password + </Button> + </form> + </Paper> + </main> + ) + } +} + +Register.propTypes = { + resetPassword: PropTypes.func.isRequired, + auth: PropTypes.object.isRequired, + errors: PropTypes.object.isRequired, + history: PropTypes.object, + classes: PropTypes.object, + updateError: PropTypes.func.isRequired, + match: PropTypes.object.isRequired +} + +const mapStateToProps = state => ({ + auth: state.auth, + errors: state.errors +}) + +export default compose( + withStyles(styles, { + name: 'Register' + }), + connect(mapStateToProps, { resetPassword, updateError }) +)(Register) diff --git a/fact-bounty-client/src/pages/ResetPassword/ResetPassword.style.js b/fact-bounty-client/src/pages/ResetPassword/ResetPassword.style.js new file mode 100644 index 00000000..4af949a6 --- /dev/null +++ b/fact-bounty-client/src/pages/ResetPassword/ResetPassword.style.js @@ -0,0 +1,32 @@ +export default theme => ({ + main: { + width: 'auto', + display: 'block', // Fix IE 11 issue. + marginLeft: theme.spacing.unit * 3, + marginRight: theme.spacing.unit * 3, + [theme.breakpoints.up(400 + theme.spacing.unit * 3 * 2)]: { + width: 400, + marginLeft: 'auto', + marginRight: 'auto' + } + }, + paper: { + marginTop: theme.spacing.unit * 12, + display: 'flex', + flexDirection: 'column', + alignItems: 'center', + padding: `${theme.spacing.unit * 2}px ${theme.spacing.unit * 3}px ${theme + .spacing.unit * 3}px` + }, + avatar: { + margin: theme.spacing.unit, + backgroundColor: theme.palette.secondary.main + }, + form: { + width: '100%', // Fix IE 11 issue. + marginTop: theme.spacing.unit + }, + submit: { + marginTop: theme.spacing.unit * 3 + } +}) diff --git a/fact-bounty-client/src/pages/ResetPassword/index.js b/fact-bounty-client/src/pages/ResetPassword/index.js new file mode 100644 index 00000000..6b2df239 --- /dev/null +++ b/fact-bounty-client/src/pages/ResetPassword/index.js @@ -0,0 +1,2 @@ +import ResetPassword from './ResetPassword' +export default ResetPassword diff --git a/fact-bounty-client/src/redux/actions/actionTypes.js b/fact-bounty-client/src/redux/actions/actionTypes.js index 5364673b..ea705304 100644 --- a/fact-bounty-client/src/redux/actions/actionTypes.js +++ b/fact-bounty-client/src/redux/actions/actionTypes.js @@ -2,6 +2,8 @@ export const GET_ERRORS = 'GET_ERRORS' export const UPDATE_ERRORS = 'UPDATE_ERRORS' export const USER_LOADING = 'USER_LOADING' export const SET_CURRENT_USER = 'SET_CURRENT_USER' +export const GET_SUCCESS = 'GET_SUCCESS' +export const UPDATE_SUCCESS = 'UPDATE_SUCCESS' export const LOADING_POSTS = 'LOADING_POSTS' export const FETCH_POSTS = 'FETCH_POSTS' diff --git a/fact-bounty-client/src/redux/actions/authActions.js b/fact-bounty-client/src/redux/actions/authActions.js index f6adc61c..462b61cb 100644 --- a/fact-bounty-client/src/redux/actions/authActions.js +++ b/fact-bounty-client/src/redux/actions/authActions.js @@ -1,6 +1,11 @@ import jwt_decode from 'jwt-decode' import { setAuthToken, saveAllTokens } from '../../helpers/AuthTokenHelper' -import { SET_CURRENT_USER, USER_LOADING, GET_ERRORS } from './actionTypes' +import { + SET_CURRENT_USER, + USER_LOADING, + GET_ERRORS, + GET_SUCCESS +} from './actionTypes' import AuthService from '../../services/AuthService' // Set logged in user @@ -134,3 +139,91 @@ export const logoutUser = () => dispatch => { console.error(err) }) } + +export const forgotPassword = userData => dispatch => { + AuthService.forgotPassword(userData) + .then(res => { + if (res.status === 202) { + dispatch({ + type: GET_ERRORS, + payload: { message: res.data.message } + }) + } + if (res.status === 200) { + dispatch({ + type: GET_SUCCESS, + payload: { message: res.data.message } + }) + } + }) + .catch(err => { + if (err && err.response) { + let payload = err.response.data + if (typeof payload === 'string') { + payload = { fetch: err.response.data } + } + dispatch({ + type: GET_ERRORS, + payload + }) + } + }) +} + +export const authVerificationToken = (userData, history) => dispatch => { + AuthService.authVerificationToken(userData) + .then(res => { + if (res.status === 202) { + dispatch({ + type: GET_ERRORS, + payload: { message: res.data.message } + }) + } + if (res.status === 200) { + history.push('/resetpassword/' + userData.verification_token) + } + }) + .catch(err => { + if (err && err.response) { + let payload = err.response.data + if (typeof payload === 'string') { + payload = { fetch: err.response.data } + } + dispatch({ + type: GET_ERRORS, + payload + }) + } + }) +} + +export const resetPassword = (userData, history) => dispatch => { + AuthService.resetPassword(userData) + .then(res => { + if (res.status === 202) { + dispatch({ + type: GET_ERRORS, + payload: { message: res.data.message } + }) + } + if (res.status === 200) { + dispatch({ + type: GET_SUCCESS, + payload: { message: res.data.message } + }) + history.push('/login') + } + }) + .catch(err => { + if (err && err.response) { + let payload = err.response.data + if (typeof payload === 'string') { + payload = { fetch: err.response.data } + } + dispatch({ + type: GET_ERRORS, + payload + }) + } + }) +} diff --git a/fact-bounty-client/src/redux/actions/successActions.js b/fact-bounty-client/src/redux/actions/successActions.js new file mode 100644 index 00000000..8f40ed74 --- /dev/null +++ b/fact-bounty-client/src/redux/actions/successActions.js @@ -0,0 +1,4 @@ +import { UPDATE_SUCCESS } from './actionTypes' + +export const updateSuccess = payload => dispatch => + dispatch({ type: UPDATE_SUCCESS, payload }) diff --git a/fact-bounty-client/src/redux/reducers/index.js b/fact-bounty-client/src/redux/reducers/index.js index ef358a13..ea3e30d4 100644 --- a/fact-bounty-client/src/redux/reducers/index.js +++ b/fact-bounty-client/src/redux/reducers/index.js @@ -4,10 +4,12 @@ import errorReducer from './errorReducers' import postReducer from './postReducers' import contactUsReducer from './contactUsReducers' import twitterReducer from './twitterReducers' +import successReducers from './successReducers' export default combineReducers({ auth: authReducer, errors: errorReducer, + success: successReducers, posts: postReducer, contactUs: contactUsReducer, tweets: twitterReducer diff --git a/fact-bounty-client/src/redux/reducers/successReducers.js b/fact-bounty-client/src/redux/reducers/successReducers.js new file mode 100644 index 00000000..b3fda27e --- /dev/null +++ b/fact-bounty-client/src/redux/reducers/successReducers.js @@ -0,0 +1,16 @@ +import { GET_SUCCESS, UPDATE_SUCCESS } from '../actions/actionTypes' + +const initialState = {} + +export default function(state = initialState, action) { + switch (action.type) { + case GET_SUCCESS: + return action.payload + + case UPDATE_SUCCESS: + return action.payload + + default: + return state + } +} diff --git a/fact-bounty-client/src/services/AuthService.js b/fact-bounty-client/src/services/AuthService.js index 460b2912..3ecd4f4d 100644 --- a/fact-bounty-client/src/services/AuthService.js +++ b/fact-bounty-client/src/services/AuthService.js @@ -17,7 +17,30 @@ const loginUser = userData => { const registerUser = userData => { return ApiBuilder.API.post(`/api/users/register`, userData) } - +/** + * + * POST : forgotPassword + * + */ +const forgotPassword = userData => { + return ApiBuilder.API.post(`/api/users/forgot_password`, userData) +} +/** + * + * POST : authVerificationToken + * + */ +const authVerificationToken = userData => { + return ApiBuilder.API.post(`/api/users/auth_verification_token`, userData) +} +/** + * + * POST : resetPassword + * + */ +const resetPassword = userData => { + return ApiBuilder.API.post(`/api/users/reset_password`, userData) +} /** * * POST : OauthUser @@ -56,6 +79,9 @@ const revokeRefreshToken = () => { export default { loginUser, registerUser, + forgotPassword, + authVerificationToken, + resetPassword, OauthUser, tokenRefresh, revokeAccessToken, From b9f3b7fd82a207cc55532e81ed09b6551d479337 Mon Sep 17 00:00:00 2001 From: hv7214 <hverma1@cs.iitr.ac.in> Date: Wed, 12 Feb 2020 15:07:22 +0530 Subject: [PATCH 07/11] feat(server/docs/users): swagger user documentation --- fact-bounty-flask/api/user/controller.py | 5 ++ fact-bounty-flask/docs/users/login.yml | 2 +- .../docs/users/logout_access.yml | 17 +++++ .../docs/users/logout_refresh.yml | 17 +++++ fact-bounty-flask/docs/users/oauth.yml | 69 +++++++++++++++++++ fact-bounty-flask/docs/users/register.yml | 67 ++++++++++++++++++ .../docs/users/token_refresh.yml | 15 ++++ 7 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 fact-bounty-flask/docs/users/logout_access.yml create mode 100644 fact-bounty-flask/docs/users/logout_refresh.yml create mode 100644 fact-bounty-flask/docs/users/oauth.yml create mode 100644 fact-bounty-flask/docs/users/register.yml create mode 100644 fact-bounty-flask/docs/users/token_refresh.yml diff --git a/fact-bounty-flask/api/user/controller.py b/fact-bounty-flask/api/user/controller.py index 421e2185..667ab8af 100644 --- a/fact-bounty-flask/api/user/controller.py +++ b/fact-bounty-flask/api/user/controller.py @@ -18,6 +18,7 @@ class Register(MethodView): """This class registers a new user.""" + @swag_from("../../docs/users/register.yml") def post(self): """Handle POST request for this view. Url --> /api/users/register""" # getting JSON data from request @@ -241,6 +242,7 @@ class Auth(MethodView): """This class-based view handles user register and access token generation \ via 3rd sources like facebook, google""" + @swag_from("../../docs/users/oauth.yml") def post(self): # Querying the database with requested email data = request.get_json(silent=True) @@ -300,6 +302,7 @@ def post(self): class LogoutAccess(MethodView): @jwt_required + @swag_from("../../docs/users/logout_access.yml") def post(self): jti = get_raw_jwt()["jti"] try: @@ -316,6 +319,7 @@ def post(self): class LogoutRefresh(MethodView): @jwt_refresh_token_required + @swag_from("../../docs/users/logout_refresh.yml") def post(self): jti = get_raw_jwt()["jti"] try: @@ -332,6 +336,7 @@ def post(self): class TokenRefresh(MethodView): @jwt_refresh_token_required + @swag_from("../../docs/users/token_refresh.yml") def post(self): current_user = get_jwt_identity() access_token = create_access_token(identity=current_user, fresh=False) diff --git a/fact-bounty-flask/docs/users/login.yml b/fact-bounty-flask/docs/users/login.yml index ba76f27b..f3faca2b 100644 --- a/fact-bounty-flask/docs/users/login.yml +++ b/fact-bounty-flask/docs/users/login.yml @@ -1,7 +1,7 @@ User login --- tags: -- Auth +- User consumes: - "application/json" parameters: diff --git a/fact-bounty-flask/docs/users/logout_access.yml b/fact-bounty-flask/docs/users/logout_access.yml new file mode 100644 index 00000000..47d991c3 --- /dev/null +++ b/fact-bounty-flask/docs/users/logout_access.yml @@ -0,0 +1,17 @@ +User logout +--- +tags: +- User +produces: + - application/json +parameters: +- in: header + name: Authorization + description: an authorization header + required: true + type: string +responses: + 200: + description: Access token has been revoked. + 500: + description: Something went wrong! diff --git a/fact-bounty-flask/docs/users/logout_refresh.yml b/fact-bounty-flask/docs/users/logout_refresh.yml new file mode 100644 index 00000000..e5c4e3b5 --- /dev/null +++ b/fact-bounty-flask/docs/users/logout_refresh.yml @@ -0,0 +1,17 @@ +User logout +--- +tags: +- User +produces: + - application/json +parameters: +- in: header + name: Authorization + description: an authorization header + required: true + type: string +responses: + 200: + description: Refresh token has been revoked. + 500: + description: Something went wrong! diff --git a/fact-bounty-flask/docs/users/oauth.yml b/fact-bounty-flask/docs/users/oauth.yml new file mode 100644 index 00000000..b88f2bee --- /dev/null +++ b/fact-bounty-flask/docs/users/oauth.yml @@ -0,0 +1,69 @@ +User registration via OAuth like facebook, google, etc. +--- +tags: + - User +consumes: + - "application/json" +produces: + - "application/json" +parameters: +- in: body + name: body + schema: + type: object + properties: + name: + type: string + example: xyz + email: + type: string + example: xyz@gmail.com + type: + type: string + example: admin +responses: + 201: + description: (for new user) JSON object containing success message, access token and refresh token. + schema: + type: object + properties: + message: + type: string + example: You logged in successfully. + access_token: + type: string + example: hash-generated-by-jwt + refresh_token: + type: string + example: hash-generated-by-jwt + 202: + description: (for existing user) JSON object containing success message, access token and refresh token. + schema: + type: object + properties: + message: + type: string + example: You logged in successfully. + access_token: + type: string + example: hash-generated-by-jwt + refresh_token: + type: string + example: hash-generated-by-jwt + + 404: + description: Fields are missing + schema: + type: object + properties: + message: + type: string + example: Please provide all the required fields. + 500: + description: Something went wrong + schema: + type: object + properties: + message: + type: string + example: Something went wrong diff --git a/fact-bounty-flask/docs/users/register.yml b/fact-bounty-flask/docs/users/register.yml new file mode 100644 index 00000000..6c042f36 --- /dev/null +++ b/fact-bounty-flask/docs/users/register.yml @@ -0,0 +1,67 @@ +User registration via form +--- +tags: + - User +consumes: + - "application/json" +produces: + - "application/json" +parameters: +- in: body + name: body + schema: + type: object + properties: + name: + type: string + example: xyz + email: + type: string + example: xyz@gmail.com + password: + type: string + example: password123 + password2: + type: string + example: password123 +responses: + 201: + description: JSON object containing success message + schema: + type: object + properties: + message: + type: string + example: You registered successfully. Please log in. + 202: + description: JSON object containing success message + schema: + type: object + properties: + message: + type: string + example: User already exists. Please login. + 401: + description: Passwords do not match + schema: + type: object + properties: + message: + type: string + example: Both passwords does not match + 404: + description: Fields are missing + schema: + type: object + properties: + message: + type: string + example: Please provide all the required fields. + 500: + description: Something went wrong + schema: + type: object + properties: + message: + type: string + example: Something went wrong diff --git a/fact-bounty-flask/docs/users/token_refresh.yml b/fact-bounty-flask/docs/users/token_refresh.yml new file mode 100644 index 00000000..1176c67b --- /dev/null +++ b/fact-bounty-flask/docs/users/token_refresh.yml @@ -0,0 +1,15 @@ +Token refresh +--- +tags: +- User +produces: + - application/json +parameters: +- in: header + name: Authorization + description: an authorization header + required: true + type: string +responses: + 200: + description: Token refreshed successfully. From e29c3cd06d5690278483891fd40f2d91761a5c4b Mon Sep 17 00:00:00 2001 From: Joseph Semrai <josephsemrai@gmail.com> Date: Sat, 28 Dec 2019 13:43:14 -0500 Subject: [PATCH 08/11] Added profile controllers and routes Decorator fix Codacy fix --- fact-bounty-flask/api/user/controller.py | 37 ++++++++++++++++++++++++ fact-bounty-flask/api/user/views.py | 6 ++++ 2 files changed, 43 insertions(+) diff --git a/fact-bounty-flask/api/user/controller.py b/fact-bounty-flask/api/user/controller.py index 667ab8af..2bd75087 100644 --- a/fact-bounty-flask/api/user/controller.py +++ b/fact-bounty-flask/api/user/controller.py @@ -348,6 +348,42 @@ def post(self): return make_response(jsonify(response)), 200 +class Profile(MethodView): + """This class-based view handles retrieving and updating the current \ + user's information""" + + @staticmethod + @jwt_required + def get(self): + current_user = get_jwt_identity() + + response = jsonify(current_user) + return make_response(response), 200 + + @jwt_required + def post(self): + try: + post_data = request.get_json(silent=True) + name = post_data["name"] + email = post_data["email"] + except Exception: + response = {"message": "Some user details are missing."} + return make_response(jsonify(response)), 404 + + # Querying the database to get the user to update + user = User.find_by_email(email) + + if user: + user = user.update(email=email, name=name) + user.save() + response = {"message": "User has been updated."} + return make_response(jsonify(response)), 204 + + else: + response = {"message": "User not found. Please check your request."} + return make_response(jsonify(response)), 404 + + userController = { "register": Register.as_view("register"), "login": Login.as_view("login"), @@ -360,4 +396,5 @@ def post(self): "logout_access": LogoutAccess.as_view("logout_access"), "logout_refresh": LogoutRefresh.as_view("logout_refresh"), "token_refresh": TokenRefresh.as_view("token_refresh"), + "profile": Profile.as_view("profile") } diff --git a/fact-bounty-flask/api/user/views.py b/fact-bounty-flask/api/user/views.py index 82a45048..20ddefc3 100644 --- a/fact-bounty-flask/api/user/views.py +++ b/fact-bounty-flask/api/user/views.py @@ -49,3 +49,9 @@ view_func=userController["token_refresh"], methods=["POST"], ) + +userprint.add_url_rule( + "/profile", + view_func=userController["profile"], + methods=["GET", "POST"] +) From 9c365694038796dc63aef3a59e799a181248f1f5 Mon Sep 17 00:00:00 2001 From: hv7214 <hverma1@cs.iitr.ac.in> Date: Fri, 24 Jan 2020 11:41:26 +0530 Subject: [PATCH 09/11] feat(server/user): added forgot password functionality --- fact-bounty-flask/api/user/model.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/fact-bounty-flask/api/user/model.py b/fact-bounty-flask/api/user/model.py index 76c0a19c..4d1023dc 100755 --- a/fact-bounty-flask/api/user/model.py +++ b/fact-bounty-flask/api/user/model.py @@ -66,7 +66,7 @@ def find_by_email(cls, email): def find_by_user_id(cls, id): return cls.query.filter_by(id=id).first() - @classmethod + @classmethod def find_by_verification_token(cls, verification_token): return cls.query.filter_by( verification_token=verification_token From ec9e54dae8df7860fee5749ebcbfec982a83d502 Mon Sep 17 00:00:00 2001 From: Aditya Bhatnagar <adityabhatnagar14@gmail.com> Date: Sat, 28 Dec 2019 13:43:14 -0500 Subject: [PATCH 10/11] Add admin controller --- fact-bounty-flask/api/app.py | 4 +++- fact-bounty-flask/api/commands.py | 4 +--- fact-bounty-flask/api/user/model.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/fact-bounty-flask/api/app.py b/fact-bounty-flask/api/app.py index 4bfc9022..bc0660cf 100644 --- a/fact-bounty-flask/api/app.py +++ b/fact-bounty-flask/api/app.py @@ -30,8 +30,10 @@ def create_app(config_name): @app.before_first_request def create_tables(): db.create_all() - return app + + except Exception as err: + print("Error occured:", err) def register_extensions(app): diff --git a/fact-bounty-flask/api/commands.py b/fact-bounty-flask/api/commands.py index 53dcd1c8..1f9d12e5 100644 --- a/fact-bounty-flask/api/commands.py +++ b/fact-bounty-flask/api/commands.py @@ -219,6 +219,4 @@ def create_admin(): ) user.save() except Exception as e: - print(str(e)) - response = {"message": "Something went wrong!!"} - return make_response(jsonify(response)), 500 + return {"Error occured": str(e)} diff --git a/fact-bounty-flask/api/user/model.py b/fact-bounty-flask/api/user/model.py index 4d1023dc..8361befb 100755 --- a/fact-bounty-flask/api/user/model.py +++ b/fact-bounty-flask/api/user/model.py @@ -26,7 +26,7 @@ class User(Model): type = Column(db.String(50), default="remote") role = Column(db.String(10), default="user") - def __init__(self, id, name, email, password, role="user", _type="remote"): + def __init__(self, name, email, password, role="user", _type="remote"): """ Initializes the user instance """ From 7d9d4c8be90d49617b26da1f0288c0b4b8f5bbe8 Mon Sep 17 00:00:00 2001 From: the catalyst <adityabhatnagar14@gmail.com> Date: Mon, 9 Mar 2020 17:33:20 +0530 Subject: [PATCH 11/11] Fix indentation errors --- fact-bounty-flask/api/app.py | 35 +++++++++++++++++++---------------- 1 file changed, 19 insertions(+), 16 deletions(-) diff --git a/fact-bounty-flask/api/app.py b/fact-bounty-flask/api/app.py index bc0660cf..ffd4f2d6 100644 --- a/fact-bounty-flask/api/app.py +++ b/fact-bounty-flask/api/app.py @@ -16,22 +16,25 @@ def create_app(config_name): # create and configure the app - app = Flask( - __name__, static_folder="../build/static", template_folder="../build" - ) - app.config.from_object(config[config_name]) - config[config_name].init_app(app) - - register_extensions(app) - register_blueprint(app) - register_shellcontext(app) - register_commands(app) - - @app.before_first_request - def create_tables(): - db.create_all() - return app - + try: + app = Flask( + __name__, + static_folder="../build/static", + template_folder="../build", + ) + app.config.from_object(config[config_name]) + config[config_name].init_app(app) + + register_extensions(app) + register_blueprint(app) + register_shellcontext(app) + register_commands(app) + + @app.before_first_request + def create_tables(): + db.create_all() + + return app except Exception as err: print("Error occured:", err)