diff --git a/README.md b/README.md index 4d51f2e8..b3bdbae0 100644 --- a/README.md +++ b/README.md @@ -755,6 +755,35 @@ Input: Output: Same output as for `/api/auth/login` endpoint + +#### POST /api/auth/register + +Input: +- email +- password + +Output: 200 means user is going to receive an email that contains an *activation code*. User needs to enter this code to confirm their account -> next endpoint. + + +#### POST /api/auth/activate + +Input: +- email +- code: the activation code + +Output: +- 200: account is activated. User can login now +- 400: wrong email, code +- 410: wrong code too many times. User needs to ask for an reactivation -> next endpoint + +#### POST /api/auth/reactivate + +Input: +- email + +Output: +- 200: user is going to receive an email that contains the activation code. + #### GET /api/aliases Get user aliases. diff --git a/app/api/views/auth_login.py b/app/api/views/auth_login.py index cd29e918..7bf7dbb7 100644 --- a/app/api/views/auth_login.py +++ b/app/api/views/auth_login.py @@ -1,4 +1,5 @@ -from flask import jsonify, request +import random + import facebook import google.oauth2.credentials import googleapiclient.discovery @@ -12,10 +13,15 @@ from app.config import ( FLASK_SECRET, DISABLE_REGISTRATION, ) -from app.email_utils import can_be_used_as_personal_email, email_already_used +from app.email_utils import ( + can_be_used_as_personal_email, + email_already_used, + send_email, + render, +) from app.extensions import db from app.log import LOG -from app.models import User, ApiKey, SocialAuth +from app.models import User, ApiKey, SocialAuth, AccountActivation @api_bp.route("/auth/login", methods=["POST"]) @@ -55,6 +61,145 @@ def auth_login(): return jsonify(**auth_payload(user, device)), 200 +@api_bp.route("/auth/register", methods=["POST"]) +@cross_origin() +def auth_register(): + """ + User signs up - will need to activate their account with an activation code. + Input: + email + password + Output: + 200: user needs to confirm their account + + """ + data = request.get_json() + if not data: + return jsonify(error="request body cannot be empty"), 400 + + email = data.get("email") + password = data.get("password") + + if DISABLE_REGISTRATION: + return jsonify(error="registration is closed"), 400 + if not can_be_used_as_personal_email(email) or email_already_used(email): + return jsonify(error=f"cannot use {email} as personal inbox"), 400 + + if not password or len(password) < 8: + return jsonify(error="password too short"), 400 + + LOG.debug("create user %s", email) + user = User.create(email=email, name="", password=password) + db.session.flush() + + # create activation code + code = "".join([str(random.randint(0, 9)) for _ in range(6)]) + AccountActivation.create(user_id=user.id, code=code) + db.session.commit() + + send_email( + email, + f"Just one more step to join SimpleLogin", + render("transactional/code-activation.txt", code=code), + render("transactional/code-activation.html", code=code), + ) + + return jsonify(msg="User needs to confirm their account"), 200 + + +@api_bp.route("/auth/activate", methods=["POST"]) +@cross_origin() +def auth_activate(): + """ + User enters the activation code to confirm their account. + Input: + email + code + Output: + 200: user account is now activated, user can login now + 400: wrong email, code + 410: wrong code too many times + + """ + data = request.get_json() + if not data: + return jsonify(error="request body cannot be empty"), 400 + + email = data.get("email") + code = data.get("code") + + user = User.get_by(email=email) + + # do not use a different message to avoid exposing existing email + if not user or user.activated: + return jsonify(error="Wrong email or code"), 400 + + account_activation = AccountActivation.get_by(user_id=user.id) + if not account_activation: + return jsonify(error="Wrong email or code"), 400 + + if account_activation.code != code: + # decrement nb tries + account_activation.tries -= 1 + db.session.commit() + + if account_activation.tries == 0: + AccountActivation.delete(account_activation.id) + db.session.commit() + return jsonify(error="Too many wrong tries"), 410 + + return jsonify(error="Wrong email or code"), 400 + + LOG.debug("activate user %s", user) + user.activated = True + AccountActivation.delete(account_activation.id) + db.session.commit() + + return jsonify(msg="Account is activated, user can login now"), 200 + + +@api_bp.route("/auth/reactivate", methods=["POST"]) +@cross_origin() +def auth_reactivate(): + """ + User asks for another activation code + Input: + email + Output: + 200: user is going to receive an email for activate their account + + """ + data = request.get_json() + if not data: + return jsonify(error="request body cannot be empty"), 400 + + email = data.get("email") + user = User.get_by(email=email) + + # do not use a different message to avoid exposing existing email + if not user or user.activated: + return jsonify(error="Something went wrong"), 400 + + account_activation = AccountActivation.get_by(user_id=user.id) + if account_activation: + AccountActivation.delete(account_activation.id) + db.session.commit() + + # create activation code + code = "".join([str(random.randint(0, 9)) for _ in range(6)]) + AccountActivation.create(user_id=user.id, code=code) + db.session.commit() + + send_email( + email, + f"Just one more step to join SimpleLogin", + render("transactional/code-activation.txt", code=code), + render("transactional/code-activation.html", code=code), + ) + + return jsonify(msg="User needs to confirm their account"), 200 + + @api_bp.route("/auth/facebook", methods=["POST"]) @cross_origin() def auth_facebook(): diff --git a/templates/emails/transactional/code-activation.html b/templates/emails/transactional/code-activation.html new file mode 100644 index 00000000..3184fcba --- /dev/null +++ b/templates/emails/transactional/code-activation.html @@ -0,0 +1,10 @@ +{% extends "base.html" %} + +{% block content %} + {{ render_text("Hi") }} + {{ render_text("Thank you for choosing SimpleLogin.") }} + {{ render_text("To get started, please activate your account by entering the following code into the application:") }} + {{ render_text("