diff --git a/app/dashboard/views/subdomain.py b/app/dashboard/views/subdomain.py index 11ec9018..54e97a06 100644 --- a/app/dashboard/views/subdomain.py +++ b/app/dashboard/views/subdomain.py @@ -3,6 +3,7 @@ from flask_login import login_required, current_user from app.config import MAX_NB_SUBDOMAIN from app.dashboard.base import dashboard_bp +from app.errors import SubdomainInTrashError from app.log import LOG from app.models import CustomDomain, Mailbox, SLDomain @@ -51,30 +52,36 @@ def subdomain_route(): ).first(): flash(f"{full_domain} already used in a SimpleLogin mailbox", "error") else: - new_custom_domain = CustomDomain.create( - is_sl_subdomain=True, - catch_all=True, # by default catch-all is enabled - domain=full_domain, - user_id=current_user.id, - verified=True, - dkim_verified=False, # wildcard DNS does not work for DKIM - spf_verified=True, - dmarc_verified=False, # wildcard DNS does not work for DMARC - ownership_verified=True, - commit=True, - ) - - flash( - f"New subdomain {new_custom_domain.domain} is created", - "success", - ) - - return redirect( - url_for( - "dashboard.domain_detail", - custom_domain_id=new_custom_domain.id, + try: + new_custom_domain = CustomDomain.create( + is_sl_subdomain=True, + catch_all=True, # by default catch-all is enabled + domain=full_domain, + user_id=current_user.id, + verified=True, + dkim_verified=False, # wildcard DNS does not work for DKIM + spf_verified=True, + dmarc_verified=False, # wildcard DNS does not work for DMARC + ownership_verified=True, + commit=True, + ) + except SubdomainInTrashError: + flash( + f"{full_domain} has been used before and cannot be reused", + "error", + ) + else: + flash( + f"New subdomain {new_custom_domain.domain} is created", + "success", + ) + + return redirect( + url_for( + "dashboard.domain_detail", + custom_domain_id=new_custom_domain.id, + ) ) - ) return render_template( "dashboard/subdomain.html", diff --git a/app/errors.py b/app/errors.py index de22c865..3fa14abd 100644 --- a/app/errors.py +++ b/app/errors.py @@ -8,3 +8,9 @@ class DirectoryInTrashError(Exception): """raised when a directory is deleted before """ pass + + +class SubdomainInTrashError(Exception): + """raised when a subdomain is deleted before """ + + pass diff --git a/app/models.py b/app/models.py index 6ed4ee0d..78c5817a 100644 --- a/app/models.py +++ b/app/models.py @@ -32,7 +32,7 @@ from app.config import ( ALIAS_RANDOM_SUFFIX_LENGTH, ) from app.db import Session -from app.errors import AliasInTrashError, DirectoryInTrashError +from app.errors import AliasInTrashError, DirectoryInTrashError, SubdomainInTrashError from app.log import LOG from app.oauth_models import Scope from app.pw_models import PasswordOracle @@ -1979,8 +1979,12 @@ class CustomDomain(Base, ModelMixin): return f"sl-verification={self.ownership_txt_token}" @classmethod - def create(cls, **kw): - domain: CustomDomain = super(CustomDomain, cls).create(**kw) + def create(cls, **kwargs): + domain = kwargs.get("domain") + if DeletedSubdomain.get_by(domain=domain): + raise SubdomainInTrashError + + domain: CustomDomain = super(CustomDomain, cls).create(**kwargs) # generate a domain ownership txt token if not domain.ownership_txt_token: @@ -1989,6 +1993,14 @@ class CustomDomain(Base, ModelMixin): return domain + @classmethod + def delete(cls, obj_id): + obj: CustomDomain = cls.get(obj_id) + if obj.is_sl_subdomain: + DeletedSubdomain.create(domain=obj.domain) + + return super(CustomDomain, cls).delete(obj_id) + @property def auto_create_rules(self): return sorted(self._auto_create_rules, key=lambda rule: rule.order) diff --git a/tests/dashboard/test_subdomain.py b/tests/dashboard/test_subdomain.py new file mode 100644 index 00000000..4ed253fe --- /dev/null +++ b/tests/dashboard/test_subdomain.py @@ -0,0 +1,83 @@ +from flask import url_for + +from app.db import Session +from app.models import SLDomain, CustomDomain, Job +from tests.utils import login + + +def setup_sl_domain() -> SLDomain: + """Take the first SLDomain and set its can_use_subdomain=True""" + sl_domain: SLDomain = SLDomain.first() + sl_domain.can_use_subdomain = True + Session.commit() + + return sl_domain + + +def test_create_subdomain(flask_client): + login(flask_client) + sl_domain = setup_sl_domain() + + r = flask_client.post( + url_for("dashboard.subdomain_route"), + data={"form-name": "create", "subdomain": "test", "domain": sl_domain.domain}, + follow_redirects=True, + ) + + assert r.status_code == 200 + assert f"New subdomain test.{sl_domain.domain} is created" in r.data.decode() + assert CustomDomain.get_by(domain=f"test.{sl_domain.domain}") is not None + + +def test_delete_subdomain(flask_client): + user = login(flask_client) + sl_domain = setup_sl_domain() + + subdomain = CustomDomain.create( + domain=f"test.{sl_domain.domain}", + user_id=user.id, + is_sl_subdomain=True, + commit=True, + ) + + nb_job = Job.count() + + r = flask_client.post( + url_for("dashboard.domain_detail", custom_domain_id=subdomain.id), + data={"form-name": "delete"}, + follow_redirects=True, + ) + + assert r.status_code == 200 + assert f"test.{sl_domain.domain} scheduled for deletion." in r.data.decode() + + # a domain deletion job is scheduled + assert Job.count() == nb_job + 1 + + +def test_create_subdomain_in_trash(flask_client): + user = login(flask_client) + sl_domain = setup_sl_domain() + + subdomain = CustomDomain.create( + domain=f"test.{sl_domain.domain}", + user_id=user.id, + is_sl_subdomain=True, + commit=True, + ) + + # delete the subdomain + CustomDomain.delete(subdomain.id) + assert CustomDomain.get_by(domain=f"test.{sl_domain.domain}") is None + + r = flask_client.post( + url_for("dashboard.subdomain_route"), + data={"form-name": "create", "subdomain": "test", "domain": sl_domain.domain}, + follow_redirects=True, + ) + + assert r.status_code == 200 + assert ( + f"test.{sl_domain.domain} has been used before and cannot be reused" + in r.data.decode() + )