diff --git a/README.md b/README.md index 4e28774f..7bb2b6e3 100644 --- a/README.md +++ b/README.md @@ -802,6 +802,7 @@ Get user aliases. Input: - `Authentication` header that contains the api key - `page_id` used for the pagination. The endpoint returns maximum 20 aliases for each page. `page_id` starts at 0. +- (Optional) query: included in request body. Some frameworks might prevent GET request having a non-empty body, in this case this endpoint also supports POST. Output: If success, 200 with the list of aliases, for example: @@ -929,6 +930,7 @@ If success, 200 with the list of contacts, for example: { "contacts": [ { + "id": 1, "contact": "marketing@example.com", "creation_date": "2020-02-21 11:35:00+00:00", "creation_timestamp": 1582284900, @@ -937,6 +939,7 @@ If success, 200 with the list of contacts, for example: "reverse_alias": "marketing at example.com " }, { + "id": 2, "contact": "newsletter@example.com", "creation_date": "2020-02-21 11:35:00+00:00", "creation_timestamp": 1582284900, @@ -966,6 +969,7 @@ Return 409 if contact is already added. ``` { + "id": 1, "contact": "First Last ", "creation_date": "2020-03-14 11:52:41+00:00", "creation_timestamp": 1584186761, @@ -975,6 +979,24 @@ Return 409 if contact is already added. } ``` +#### DELETE /api/contacts/:contact_id + +Delete a contact + +Input: +- `Authentication` header that contains the api key +- `contact_id` in url. + +Output: +If success, 200. + + +```json +{ + "deleted": true +} +``` + ### Database migration The database migration is handled by `alembic` diff --git a/app/api/views/alias.py b/app/api/views/alias.py index 25ac419f..46ae5e04 100644 --- a/app/api/views/alias.py +++ b/app/api/views/alias.py @@ -9,15 +9,18 @@ from app.api.base import api_bp, verify_api_key from app.config import EMAIL_DOMAIN from app.config import PAGE_LIMIT from app.dashboard.views.alias_log import get_alias_log -from app.dashboard.views.index import get_alias_info, AliasInfo +from app.dashboard.views.index import ( + AliasInfo, + get_alias_infos_with_pagination, +) from app.extensions import db from app.log import LOG -from app.models import EmailLog from app.models import Alias, Contact +from app.models import EmailLog from app.utils import random_string -@api_bp.route("/aliases") +@api_bp.route("/aliases", methods=["GET", "POST"]) @cross_origin() @verify_api_key def get_aliases(): @@ -43,7 +46,14 @@ def get_aliases(): except (ValueError, TypeError): return jsonify(error="page_id must be provided in request query"), 400 - alias_infos: [AliasInfo] = get_alias_info(user, page_id=page_id) + query = None + data = request.get_json(silent=True) + if data: + query = data.get("query") + + alias_infos: [AliasInfo] = get_alias_infos_with_pagination( + user, page_id=page_id, query=query + ) return ( jsonify( @@ -202,6 +212,7 @@ def update_alias(alias_id): def serialize_contact(fe: Contact) -> dict: res = { + "id": fe.id, "creation_date": fe.created_at.format(), "creation_timestamp": fe.created_at.timestamp, "last_email_sent_date": None, @@ -319,3 +330,28 @@ def create_contact_route(alias_id): db.session.commit() return jsonify(**serialize_contact(contact)), 201 + + +@api_bp.route("/contacts/", methods=["DELETE"]) +@cross_origin() +@verify_api_key +def delete_contact(contact_id): + """ + Delete contact + Input: + contact_id: in url + Output: + 200 + + + """ + user = g.user + contact = Contact.get(contact_id) + + if not contact or contact.alias.user_id != user.id: + return jsonify(error="Forbidden"), 403 + + Contact.delete(contact_id) + db.session.commit() + + return jsonify(deleted=True), 200 diff --git a/app/dashboard/views/index.py b/app/dashboard/views/index.py index 46db83a3..a7c46c44 100644 --- a/app/dashboard/views/index.py +++ b/app/dashboard/views/index.py @@ -176,7 +176,7 @@ def index(): return render_template( "dashboard/index.html", client_users=client_users, - aliases=get_alias_info(current_user, query, highlight_alias_id), + aliases=get_alias_infos(current_user, query, highlight_alias_id), highlight_alias_id=highlight_alias_id, query=query, AliasGeneratorEnum=AliasGeneratorEnum, @@ -184,9 +184,56 @@ def index(): ) -def get_alias_info( - user, query=None, highlight_alias_id=None, page_id=None -) -> [AliasInfo]: +def get_alias_info(alias: Alias) -> AliasInfo: + q = ( + db.session.query(Contact, EmailLog) + .filter(Contact.alias_id == alias.id) + .filter(EmailLog.contact_id == Contact.id) + ) + + alias_info = AliasInfo( + id=alias.id, + alias=alias, + mailbox=alias.mailbox, + note=alias.note, + nb_blocked=0, + nb_forward=0, + nb_reply=0, + ) + + for _, el in q: + if el.is_reply: + alias_info.nb_reply += 1 + elif el.blocked: + alias_info.nb_blocked += 1 + else: + alias_info.nb_forward += 1 + + return alias_info + + +def get_alias_infos_with_pagination(user, page_id=0, query=None) -> [AliasInfo]: + ret = [] + q = ( + db.session.query(Alias) + .options(joinedload(Alias.mailbox)) + .filter(Alias.user_id == user.id) + ) + + if query: + q = q.filter( + or_(Alias.email.ilike(f"%{query}%"), Alias.note.ilike(f"%{query}%")) + ) + + q = q.limit(PAGE_LIMIT).offset(page_id * PAGE_LIMIT) + + for alias in q: + ret.append(get_alias_info(alias)) + + return ret + + +def get_alias_infos(user, query=None, highlight_alias_id=None) -> [AliasInfo]: if query: query = query.strip().lower() @@ -206,10 +253,6 @@ def get_alias_info( or_(Alias.email.ilike(f"%{query}%"), Alias.note.ilike(f"%{query}%")) ) - # pagination activated - if page_id is not None: - q = q.limit(PAGE_LIMIT).offset(page_id * PAGE_LIMIT) - for ge, fe, fel, mb in q: if ge.email not in aliases: aliases[ge.email] = AliasInfo( diff --git a/tests/api/test_alias.py b/tests/api/test_alias.py index aa145b89..7b067eac 100644 --- a/tests/api/test_alias.py +++ b/tests/api/test_alias.py @@ -8,7 +8,7 @@ from app.models import User, ApiKey, Alias, Contact, EmailLog from app.utils import random_word -def test_error_without_pagination(flask_client): +def test_get_aliases_error_without_pagination(flask_client): user = User.create( email="a@b.c", password="password", name="Test User", activated=True ) @@ -26,7 +26,7 @@ def test_error_without_pagination(flask_client): assert r.json["error"] -def test_success_with_pagination(flask_client): +def test_get_aliases_with_pagination(flask_client): user = User.create( email="a@b.c", password="password", name="Test User", activated=True ) @@ -70,6 +70,38 @@ def test_success_with_pagination(flask_client): assert len(r.json["aliases"]) == 2 +def test_get_aliases_with_pagination(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 more aliases than PAGE_LIMIT + Alias.create_new(user, "prefix1") + Alias.create_new(user, "prefix2") + db.session.commit() + + # get aliases without query, should return 3 aliases as one alias is created when user is created + r = flask_client.get( + url_for("api.get_aliases", page_id=0), headers={"Authentication": api_key.code} + ) + assert r.status_code == 200 + assert len(r.json["aliases"]) == 3 + + # get aliases with "prefix1" query, should return 1 alias + r = flask_client.get( + url_for("api.get_aliases", page_id=0), + headers={"Authentication": api_key.code}, + json={"query": "prefix1"}, + ) + assert r.status_code == 200 + assert len(r.json["aliases"]) == 1 + + def test_delete_alias(flask_client): user = User.create( email="a@b.c", password="password", name="Test User", activated=True @@ -267,3 +299,32 @@ def test_create_contact_route(flask_client): json={"contact": "First2 Last2 "}, ) assert r.status_code == 409 + + +def test_delete_contact(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() + + alias = Alias.create_new_random(user) + db.session.commit() + + contact = Contact.create( + alias_id=alias.id, + website_email="contact@example.com", + reply_email="reply+random@sl.io", + ) + db.session.commit() + + r = flask_client.delete( + url_for("api.delete_contact", contact_id=contact.id), + headers={"Authentication": api_key.code}, + ) + + assert r.status_code == 200 + assert r.json == {"deleted": True}