diff --git a/README.md b/README.md index 4646cecb..5cf73ebd 100644 --- a/README.md +++ b/README.md @@ -691,7 +691,7 @@ For ex: } ``` -#### POST /api/alias/custom/new +#### POST /api/v2/alias/custom/new Create a new custom alias. @@ -700,7 +700,7 @@ Input: - (Optional but recommended) `hostname` passed in query string - Request Message Body in json (`Content-Type` is `application/json`) - alias_prefix: string. The first part of the alias that user can choose. - - alias_suffix: should be one of the suffixes returned in the `GET /api/v2/alias/options` endpoint. + - signed_suffix: should be one of the suffixes returned in the `GET /api/v4/alias/options` endpoint. - (Optional) note: alias note Output: diff --git a/app/api/views/new_custom_alias.py b/app/api/views/new_custom_alias.py index d04dd2b4..81522ebe 100644 --- a/app/api/views/new_custom_alias.py +++ b/app/api/views/new_custom_alias.py @@ -1,11 +1,12 @@ from flask import g from flask import jsonify, request from flask_cors import cross_origin +from itsdangerous import SignatureExpired from app.api.base import api_bp, require_api_auth from app.api.serializer import serialize_alias_info, get_alias_info from app.config import MAX_NB_EMAIL_FREE_PLAN, ALIAS_DOMAINS -from app.dashboard.views.custom_alias import verify_prefix_suffix +from app.dashboard.views.custom_alias import verify_prefix_suffix, signer from app.extensions import db from app.log import LOG from app.models import Alias, AliasUsedOn, User, CustomDomain @@ -39,7 +40,6 @@ def new_custom_alias(): 400, ) - user_custom_domains = [cd.domain for cd in user.verified_custom_domains()] hostname = request.args.get("hostname") data = request.get_json() @@ -77,3 +77,81 @@ def new_custom_alias(): db.session.commit() return jsonify(alias=full_alias, **serialize_alias_info(get_alias_info(alias))), 201 + + +@api_bp.route("/v2/alias/custom/new", methods=["POST"]) +@cross_origin() +@require_api_auth +def new_custom_alias_v2(): + """ + Create a new custom alias + Same as v1 but signed_suffix is actually the suffix with signature, e.g. + .random_word@SL.co.Xq19rQ.s99uWQ7jD1s5JZDZqczYI5TbNNU + Input: + alias_prefix, for ex "www_groupon_com" + signed_suffix, either .random_letters@simplelogin.co or @my-domain.com + optional "hostname" in args + optional "note" + Output: + 201 if success + 409 if the alias already exists + + """ + user: User = g.user + if not user.can_create_new_alias(): + LOG.d("user %s cannot create any custom alias", user) + return ( + jsonify( + error="You have reached the limitation of a free account with the maximum of " + f"{MAX_NB_EMAIL_FREE_PLAN} aliases, please upgrade your plan to create more aliases" + ), + 400, + ) + + hostname = request.args.get("hostname") + + data = request.get_json() + if not data: + return jsonify(error="request body cannot be empty"), 400 + + alias_prefix = data.get("alias_prefix", "").strip() + signed_suffix = data.get("signed_suffix", "").strip() + note = data.get("note") + alias_prefix = convert_to_id(alias_prefix) + + # hypothesis: user will click on the button in the 300 secs + try: + alias_suffix = signer.unsign(signed_suffix, max_age=300).decode() + except SignatureExpired: + LOG.error("Alias creation time expired") + return jsonify(error="alias creation is expired, please try again"), 400 + except Exception: + LOG.error("Alias suffix is tampered, user %s", user) + return jsonify(error="Tampered suffix"), 400 + + if not verify_prefix_suffix(user, alias_prefix, alias_suffix): + return jsonify(error="wrong alias prefix or suffix"), 400 + + full_alias = alias_prefix + alias_suffix + if Alias.get_by(email=full_alias): + LOG.d("full alias already used %s", full_alias) + return jsonify(error=f"alias {full_alias} already exists"), 409 + + alias = Alias.create( + user_id=user.id, email=full_alias, mailbox_id=user.default_mailbox_id, note=note + ) + + if alias_suffix.startswith("@"): + alias_domain = alias_suffix[1:] + if alias_domain not in ALIAS_DOMAINS: + domain = CustomDomain.get_by(domain=alias_domain) + LOG.d("set alias %s to domain %s", full_alias, domain) + alias.custom_domain_id = domain.id + + db.session.commit() + + if hostname: + AliasUsedOn.create(alias_id=alias.id, hostname=hostname, user_id=alias.user_id) + db.session.commit() + + return jsonify(alias=full_alias, **serialize_alias_info(get_alias_info(alias))), 201 diff --git a/tests/api/test_new_custom_alias.py b/tests/api/test_new_custom_alias.py index bf3432a6..091f837c 100644 --- a/tests/api/test_new_custom_alias.py +++ b/tests/api/test_new_custom_alias.py @@ -1,6 +1,7 @@ from flask import url_for from app.config import EMAIL_DOMAIN, MAX_NB_EMAIL_FREE_PLAN +from app.dashboard.views.custom_alias import signer from app.extensions import db from app.models import User, ApiKey, Alias from app.utils import random_word @@ -98,3 +99,43 @@ def test_out_of_quota(flask_client): assert r.json == { "error": "You have reached the limitation of a free account with the maximum of 3 aliases, please upgrade your plan to create more aliases" } + + +def test_success_v2(flask_client): + user = User.create( + email="a@b.c", password="password", name="Test User", activated=True + ) + db.session.commit() + + # create api_key + api_key = ApiKey.create(user.id, "for test") + db.session.commit() + + # create new alias with note + word = random_word() + suffix = f".{word}@{EMAIL_DOMAIN}" + suffix = signer.sign(suffix).decode() + + r = flask_client.post( + url_for("api.new_custom_alias_v2", hostname="www.test.com"), + headers={"Authentication": api_key.code}, + json={"alias_prefix": "prefix", "signed_suffix": suffix, "note": "test note",}, + ) + + assert r.status_code == 201 + assert r.json["alias"] == f"prefix.{word}@{EMAIL_DOMAIN}" + + # assert returned field + res = r.json + assert "id" in res + assert "email" in res + assert "creation_date" in res + assert "creation_timestamp" in res + assert "nb_forward" in res + assert "nb_block" in res + assert "nb_reply" in res + assert "enabled" in res + assert "note" in res + + new_ge = Alias.get_by(email=r.json["alias"]) + assert new_ge.note == "test note"