diff --git a/max/db/aliases.py b/max/db/aliases.py index 6378997..81333a3 100644 --- a/max/db/aliases.py +++ b/max/db/aliases.py @@ -39,7 +39,8 @@ def get_alias_with_user_by_id(cur, alias_id): cur.execute( """\ SELECT a.id, a.source_email_id, ea.email, a.destination_user_id, a.enabled, a.note, - u.id, u.email_id, eu.email, u.passwordhash, u.enabled, u.is_admin, u.note + u.id, u.email_id, eu.email, u.passwordhash, u.enabled, u.is_admin, + u.can_use_different_alias_domain, u.note FROM aliases a INNER JOIN emails ea ON a.source_email_id=ea.id INNER JOIN users u ON a.destination_user_id=u.id diff --git a/max/db/schema.sql b/max/db/schema.sql index ef1d0e3..f868233 100644 --- a/max/db/schema.sql +++ b/max/db/schema.sql @@ -16,6 +16,7 @@ CREATE TABLE IF NOT EXISTS users ( passwordhash varchar(255) NOT NULL, enabled boolean NOT NULL DEFAULT TRUE, is_admin boolean NOT NULL DEFAULT FALSE, + can_use_different_alias_domain boolean NOT NULL DEFAULT FALSE, note text NOT NULL DEFAULT '' ); @@ -28,10 +29,3 @@ CREATE TABLE IF NOT EXISTS aliases ( note text, UNIQUE(source_email_id, destination_user_id) ); - -DO $$ -BEGIN - IF NOT EXISTS (SELECT 1 FROM domains) THEN - INSERT INTO domains (name) VALUES ('finn.st'); - END IF; -END$$; diff --git a/max/db/users.py b/max/db/users.py index 7fbb577..9620345 100644 --- a/max/db/users.py +++ b/max/db/users.py @@ -12,6 +12,7 @@ class User(NoteLinesMixin): passwordhash: str enabled: bool is_admin: bool + can_use_different_alias_domain: bool note: str @@ -52,7 +53,7 @@ def get_users(cur): @with_cursor def update_user(cur, id, **fields): - """only pwhash, enabled, is_admin, note""" + """only pwhash, enabled, is_admin, can_use_different_alias_domain, note""" cur.execute( "UPDATE users SET " + ", ".join(f"{field}=%s" for field in fields.keys()) @@ -62,13 +63,23 @@ def update_user(cur, id, **fields): @with_cursor -def create_user(cur, email, passwordhash, enabled, is_admin, note): +def create_user( + cur, email, passwordhash, enabled, is_admin, can_use_different_alias_domain, note +): email_id = create_email(cur, email) cur.execute( """\ - INSERT INTO users (email_id, passwordhash, enabled, is_admin, note) - VALUES (%s, %s, %s, %s, %s)""", - [email_id, passwordhash, enabled, is_admin, note], + INSERT INTO users + (email_id, passwordhash, enabled, is_admin, can_use_different_alias_domain, note) + VALUES (%s, %s, %s, %s, %s, %s)""", + [ + email_id, + passwordhash, + enabled, + is_admin, + can_use_different_alias_domain, + note, + ], ) @@ -105,8 +116,8 @@ def reset_or_create_user(cur, email, hash, is_admin): cur.execute("INSERT INTO emails (email) VALUES (%s) RETURNING id", [email]) email_id = cur.fetchone()[0] cur.execute( - "INSERT INTO users (email_id, passwordhash, enabled, is_admin, note) VALUES (%s, %s, %s, %s, %s)", # noqa - [email_id, hash, True, is_admin, "Created by commandline"], + "INSERT INTO users (email_id, passwordhash, enabled, is_admin, can_use_different_alias_domain, note) VALUES (%s, %s, %s, %s, %s, %s)", # noqa + [email_id, hash, True, is_admin, is_admin, "Created by commandline"], ) user_type = "admin" if is_admin else "user" print(f"Created {user_type} {email}") diff --git a/max/private_static/i18n/de.json b/max/private_static/i18n/de.json index f52d94b..57a8e43 100644 --- a/max/private_static/i18n/de.json +++ b/max/private_static/i18n/de.json @@ -1,5 +1,4 @@ { - "test.test": "LOL", "Hi": "Moin", "You are not logged in": "Du bist nicht angemeldet", "Not found": "Nicht gefunden", @@ -44,7 +43,6 @@ "Login was sucessfull!": "Erfolgreich angemeldet!", "Logout was sucessfull!": "Erfolgreich abgemeldet!", "The email is not valid": "Die Email ist nicht valide", - "The domain is invalid. Must be one of: {domains}": "Die Domain ist nacht valide. Dies sind die erlaubten Domains: {domains}", "Email must be given": "Die Email muss gegeben sein", "Enabled is not set": "Aktiviert ist nicht gesetzt", "Note must be a string": "Die Notiz muss eine Zeichenkette sein", @@ -64,5 +62,8 @@ "Email already exists.": "Email existiert bereits.", "Email already exists: {msg}": "Email existiert bereits: {msg}", "Used as an user.": "Wird für einen Nutzer verwendet", - "Used as an alias by {alias}.": "Word für den Alias {alias} verwendet" + "Used as an alias by {alias}.": "Word für den Alias {alias} verwendet", + "Invalid domain": "Invalide Domain", + "Can use different alias domains": "Kann unterschiedliche Aliasdomains benutzen", + "Can use different alias domains is not set": "Kann unterschiedliche Aliasdomains benutzen ist nicht gesetzt" } \ No newline at end of file diff --git a/max/routes/__init__.py b/max/routes/__init__.py index bd76a20..f18309d 100644 --- a/max/routes/__init__.py +++ b/max/routes/__init__.py @@ -15,12 +15,16 @@ from .user.detail import UserDetail from .user.edit_note import UserEditNote from .user.list import UserList from .user.toggle_admin import UserToggleAdmin +from .user.toggle_can_use_different_alias_domain import ( + UserToggleCanUseDifferentAliasDomain, +) from .user.toggle_enabled import UserToggleEnabled def init_routes(app): app.add_url_rule("/", "own-user-detail", UserDetail.as_view()) app.add_url_rule("/list", "user-list", UserList.as_view()) + app.add_url_rule("/user/", "user-detail", UserDetail.as_view()) app.add_url_rule("/user/create", "user-create", UserCreate.as_view()) app.add_url_rule( @@ -41,10 +45,16 @@ def init_routes(app): "user-toggle-admin", UserToggleAdmin.as_view(), ) + app.add_url_rule( + "/user//ser-toggle-can-use-different-alias-domain", + "user-toggle-can-use-different-alias-domain", + UserToggleCanUseDifferentAliasDomain.as_view(), + ) app.add_url_rule("/user//delete", "user-delete", UserDelete.as_view()) app.add_url_rule( "/user//aliases/create", "alias-create", AliasCreate.as_view() ) + app.add_url_rule("/alias/", "alias-detail", AliasDetail.as_view()) app.add_url_rule( "/alias//delete", "alias-delete", AliasDelete.as_view() diff --git a/max/routes/alias/create.py b/max/routes/alias/create.py index c2bd87b..a395d88 100644 --- a/max/routes/alias/create.py +++ b/max/routes/alias/create.py @@ -1,6 +1,6 @@ from flask import flash -from max.db import EmailAlreadyExists, create_alias +from max.db import EmailAlreadyExists, create_alias, get_domains from max.permissions import AllowAdminOrSelf from max.translations import t @@ -26,7 +26,21 @@ class AliasCreate( else: self.email = email - if not self.is_email_valid(email): + if self.user.can_use_different_alias_domain: + domain = self.request.form.get("domain") + if not isinstance(domain, str) or not domain: + flash(t("Domain must be given"), category="error") + error = True + elif domain not in get_domains(): + flash(t("Invalid domain"), category="error") + error = True + else: + self.domain = domain + else: + self.domain = self.get_user_domain() + + full_email = f"{self.email}@{self.domain}" + if not self.is_email_valid(full_email): error = True enabled = self.request.form.get("enabled") @@ -49,7 +63,7 @@ class AliasCreate( return # force re-rendering try: - create_alias(self.email, self.user, self.enabled, self.note) + create_alias(full_email, self.user, self.enabled, self.note) except EmailAlreadyExists as e: if self.auth_user.is_admin: flash( @@ -61,14 +75,24 @@ class AliasCreate( return flash( - t("Creation of alias {email} was successful!", email=email), + t("Creation of alias {email} was successful!", email=full_email), category="success", ) return self.redirect() def get_context(self): context = super().get_context() - for key in ("email", "enabled", "note"): + for key in ("email", "domain", "enabled", "note"): if hasattr(self, key): context[key] = getattr(self, key) + context[ + "can_use_different_alias_domain" + ] = self.user.can_use_different_alias_domain + if self.user.can_use_different_alias_domain: + context["domains"] = get_domains() + else: + context["domain"] = self.get_user_domain() return context + + def get_user_domain(self): + return self.user.email.split("@")[1] diff --git a/max/routes/check_email_mixin.py b/max/routes/check_email_mixin.py index abe4850..206acb1 100644 --- a/max/routes/check_email_mixin.py +++ b/max/routes/check_email_mixin.py @@ -6,26 +6,14 @@ from max.db import get_domains from max.translations import t -EMAIL_REGEX = re.compile(r"^[a-zA-Z0-9+_-]+@(?P[a-zA-Z0-9-]+\.[a-z]{2,61})$") - - class CheckEmailMixin: def is_email_valid(self, email: str): """Checks for the email being valid and that it has a valid domain.""" - match = EMAIL_REGEX.match(email) + regex = re.compile(f"^[a-zA-Z0-9+_-]+@({ '|'.join(get_domains()) })$") + print(regex, email) + match = regex.match(email) if not match: flash(t("The email is not valid"), category="error") return False - domains = get_domains() - if match.group("domain") not in domains: - flash( - t( - "The domain is invalid. Must be one of: {domains}", - domains=",".join(domains), - ), - category="error", - ) - return False - return True diff --git a/max/routes/user/create.py b/max/routes/user/create.py index ac2cce3..dc56134 100644 --- a/max/routes/user/create.py +++ b/max/routes/user/create.py @@ -1,7 +1,7 @@ from flask import flash from max.auth import hash_password -from max.db import EmailAlreadyExists, create_user +from max.db import EmailAlreadyExists, create_user, get_domains from max.permissions import AllowAdmin from max.translations import t from max.views import BaseTemplateGetView @@ -22,7 +22,18 @@ class UserCreate(AllowAdmin, CheckEmailMixin, BackToUsersMixin, BaseTemplateGetV else: self.email = email - if not self.is_email_valid(email): + domain = self.request.form.get("domain") + if not isinstance(domain, str) or not domain: + flash(t("Domain must be given"), category="error") + error = True + elif domain not in get_domains(): + flash(t("Invalid domain"), category="error") + error = True + else: + self.domain = domain + + full_email = f"{self.email}@{self.domain}" + if not self.is_email_valid(full_email): error = True pw1 = self.request.form.get("password1") @@ -57,6 +68,17 @@ class UserCreate(AllowAdmin, CheckEmailMixin, BackToUsersMixin, BaseTemplateGetV flash(t("Admin is not set"), category="error") error = True + can_use_different_alias_domain = self.request.form.get( + "can_use_different_alias_domain" + ) + if can_use_different_alias_domain == "on": + self.can_use_different_alias_domain = True + elif can_use_different_alias_domain is None: + self.can_use_different_alias_domain = False + else: + flash(t("Can use different alias domains is not set"), category="error") + error = True + note = self.request.form.get("note", "") if not isinstance(note, str): flash(t("Note must be a string"), category="error") @@ -70,7 +92,12 @@ class UserCreate(AllowAdmin, CheckEmailMixin, BackToUsersMixin, BaseTemplateGetV passwordhash = hash_password(self.password) try: create_user( - self.email, passwordhash, self.enabled, self.is_admin, self.note + full_email, + passwordhash, + self.enabled, + self.is_admin, + self.can_use_different_alias_domain, + self.note, ) except EmailAlreadyExists as e: if self.auth_user.is_admin: @@ -82,14 +109,26 @@ class UserCreate(AllowAdmin, CheckEmailMixin, BackToUsersMixin, BaseTemplateGetV flash(t("Email already exists.")) return - flash(t("Creation of {email} was successful!", email=email), category="success") + flash( + t("Creation of {email} was successful!", email=full_email), + category="success", + ) return self.redirect() def get_context(self): context = super().get_context() - for key in ("email", "password", "enabled", "is_admin", "note"): + for key in ( + "email", + "domain", + "password", + "enabled", + "is_admin", + "can_use_different_alias_domain", + "note", + ): if hasattr(self, key): context[key] = getattr(self, key) + context["domains"] = get_domains() return context def get_webpagetitle(self): diff --git a/max/routes/user/toggle_can_use_different_alias_domain.py b/max/routes/user/toggle_can_use_different_alias_domain.py new file mode 100644 index 0000000..a88100f --- /dev/null +++ b/max/routes/user/toggle_can_use_different_alias_domain.py @@ -0,0 +1,19 @@ +from max.db import update_user +from max.permissions import AllowAdmin +from max.views import BaseView + +from ..base_user_views import BackToUsersMixin, FetchUserMixin + + +class UserToggleCanUseDifferentAliasDomain( + AllowAdmin, FetchUserMixin, BackToUsersMixin, BaseView +): + def post(self): + update_user( + self.user.id, + can_use_different_alias_domain=( + not self.user.can_use_different_alias_domain + ), + ) + _, back_url = self.get_back_text_and_url() + return self.redirect(back_url) diff --git a/max/static/base.css b/max/static/base.css index 704ee09..9b6c0a5 100644 --- a/max/static/base.css +++ b/max/static/base.css @@ -193,6 +193,10 @@ form { line-height: 1em; } +.domain-selector { + display: inline-block; +} + @media (max-width: 1200px) { body { font-size: 13px; diff --git a/max/templates/alias/create.html b/max/templates/alias/create.html index 5945607..f0b1b36 100644 --- a/max/templates/alias/create.html +++ b/max/templates/alias/create.html @@ -7,7 +7,18 @@
- + + +
+ {% if can_use_different_alias_domain %} + @ + + {% else %} + @{{ domain }} + {% endif %} +
diff --git a/max/templates/user/create.html b/max/templates/user/create.html index bf3262d..461d7ab 100644 --- a/max/templates/user/create.html +++ b/max/templates/user/create.html @@ -7,9 +7,16 @@
- + + +
+ @ + +
- +
@@ -34,6 +41,13 @@
+
+ +
+
diff --git a/max/templates/user/detail.html b/max/templates/user/detail.html index 885205d..2593298 100644 --- a/max/templates/user/detail.html +++ b/max/templates/user/detail.html @@ -24,6 +24,10 @@ {{ t("Admin") }}: {{ macros.checkbox(user, "is_admin", url_for('user-toggle-admin', user_id=user.id, return='detail')) }}
+
+ {{ t("Can use different alias domains") }}: + {{ macros.checkbox(user, "can_use_different_alias_domain", url_for('user-toggle-can-use-different-alias-domain', user_id=user.id, return='detail')) }} +