Compare commits

..

9 Commits

Author SHA1 Message Date
Finn Stutzenstein
a1c6b656d7 multi-domain support 2022-03-15 18:36:41 +01:00
Finn Stutzenstein
58f46d0bbe Fix typo 2022-03-12 08:38:42 +01:00
Finn Stutzenstein
7124133f88 Fix SQL queries and order tables 2022-03-06 11:22:45 +01:00
Finn Stutzenstein
818a43202a remove can_receive 2022-03-06 11:14:19 +01:00
Finn Stutzenstein
8f623a264d Fix user creation 2022-03-06 10:01:17 +01:00
Finn Stutzenstein
d7641c2da4 Fix SQL queries 2022-03-06 09:53:34 +01:00
Finn Stutzenstein
b31f83e214 Fix prod docker file 2022-03-06 09:39:06 +01:00
Finn Stutzenstein
aff7bf3b48 Unique emails and added can_receive 2022-03-06 09:34:23 +01:00
Finn Stutzenstein
ba580483b4 Fix real ip header 2022-02-01 18:35:41 +01:00
23 changed files with 235 additions and 69 deletions

View File

@ -1,7 +1,12 @@
FROM python:3.8.1 FROM python:3.8.12-slim-buster
RUN apt-get -y update && apt-get -y upgrade && \ RUN apt-get -y update && apt-get -y upgrade && \
apt-get install --no-install-recommends -y wait-for-it postgresql-client apt-get install --no-install-recommends -y wait-for-it lsb-release wget gnupg2 gcc libpq-dev libc-dev
# install psql 13 (11 is the default with debian)
RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ `lsb_release -cs`-pgdg main" | tee /etc/apt/sources.list.d/pgdg.list
RUN apt-get update && apt-get --no-install-recommends -y install postgresql-client-13 && apt-get clean all
WORKDIR /app WORKDIR /app

View File

@ -1,7 +1,12 @@
FROM python:3.8.1 FROM python:3.8.12-slim-buster
RUN apt-get -y update && apt-get -y upgrade && \ RUN apt-get -y update && apt-get -y upgrade && \
apt-get install --no-install-recommends -y wait-for-it postgresql-client vim apt-get install --no-install-recommends -y wait-for-it lsb-release wget gnupg2 gcc libpq-dev libc-dev vim
# install psql 13 (11 is the default with debian)
RUN wget --quiet -O - https://www.postgresql.org/media/keys/ACCC4CF8.asc | apt-key add -
RUN echo "deb http://apt.postgresql.org/pub/repos/apt/ `lsb_release -cs`-pgdg main" | tee /etc/apt/sources.list.d/pgdg.list
RUN apt-get update && apt-get --no-install-recommends -y install postgresql-client-13 && apt-get clean all
WORKDIR /app WORKDIR /app

View File

@ -14,11 +14,17 @@ run-bash: | build-dev
docker-compose -f dc.dev.yml exec -u $$(id -u $${USER}):$$(id -g $${USER}) max bash || true docker-compose -f dc.dev.yml exec -u $$(id -u $${USER}):$$(id -g $${USER}) max bash || true
docker-compose -f dc.dev.yml down docker-compose -f dc.dev.yml down
run-bash-root: | build-dev
MAX_COMMAND="sleep infinity" docker-compose -f dc.dev.yml up -d
docker-compose -f dc.dev.yml exec max "./entrypoint-dev.sh"
docker-compose -f dc.dev.yml exec max bash || true
docker-compose -f dc.dev.yml down
stop-dev: stop-dev:
docker-compose -f dc.dev.yml down docker-compose -f dc.dev.yml down
run-cleanup: | build-dev run-cleanup: | build-dev
docker run -ti -v `pwd`/max:/app/max max-dev ./cleanup.sh docker run -ti -v `pwd`/max:/app/max --entrypoint="" max-dev ./cleanup.sh
# PROD # PROD

View File

@ -2,4 +2,4 @@
A small little program to manage emails. A small little program to manage emails.
The proxy should add X-RealIp to get a better logging. The proxy should add X-Real-IP to get a better logging.

View File

@ -27,7 +27,10 @@ SELECT {select_fields} FROM aliases a INNER JOIN emails e ON a.source_email_id=e
@with_cursor @with_cursor
def get_aliases(cur, user_id): def get_aliases(cur, user_id):
cur.execute(f"{base_alias_query} WHERE a.destination_user_id=%s", [user_id]) cur.execute(
f"{base_alias_query} WHERE a.destination_user_id=%s ORDER BY e.email ASC",
[user_id],
)
return [Alias(*row) for row in cur.fetchall()] return [Alias(*row) for row in cur.fetchall()]
@ -36,7 +39,8 @@ def get_alias_with_user_by_id(cur, alias_id):
cur.execute( cur.execute(
"""\ """\
SELECT a.id, a.source_email_id, ea.email, a.destination_user_id, a.enabled, a.note, 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 FROM aliases a
INNER JOIN emails ea ON a.source_email_id=ea.id INNER JOIN emails ea ON a.source_email_id=ea.id
INNER JOIN users u ON a.destination_user_id=u.id INNER JOIN users u ON a.destination_user_id=u.id

View File

@ -6,15 +6,17 @@ class EmailAlreadyExists(Exception):
class NoteLinesMixin: class NoteLinesMixin:
def note_lines(self): def note_lines(self):
return [line.strip() for line in self.note.split("\n")] if not self.note.strip():
return []
return [line.strip() for line in self.note.strip().split("\n")]
def create_email(cur, email): def create_email(cur, email):
cur.execute("SELECT EXISTS (SELECT 1 FROM emails WHERE email=%s)", [email]) cur.execute("SELECT EXISTS (SELECT 1 FROM emails WHERE email=lower(%s))", [email])
email_exists = cur.fetchone()[0] email_exists = cur.fetchone()[0]
if email_exists: if email_exists:
cur.execute("SELECT id FROM emails WHERE email=%s", [email]) cur.execute("SELECT id FROM emails WHERE email=lower(%s)", [email])
email_id = cur.fetchone()[0] email_id = cur.fetchone()[0]
# Get user? # Get user?
@ -35,6 +37,8 @@ def create_email(cur, email):
raise EmailAlreadyExists("Used as an user.") raise EmailAlreadyExists("Used as an user.")
else: else:
cur.execute("INSERT INTO emails (email) VALUES (%s) RETURNING id", [email]) cur.execute(
"INSERT INTO emails (email) VALUES (lower(%s)) RETURNING id", [email]
)
email_id = cur.fetchone()[0] email_id = cur.fetchone()[0]
return email_id return email_id

View File

@ -7,15 +7,20 @@ CREATE TABLE IF NOT EXISTS emails (
email varchar(255) NOT NULL UNIQUE email varchar(255) NOT NULL UNIQUE
); );
-- Make emails unique by lower-case comparison
CREATE UNIQUE INDEX IF NOT EXISTS email_unique_idx on emails (LOWER(email));
CREATE TABLE IF NOT EXISTS users ( CREATE TABLE IF NOT EXISTS users (
id serial PRIMARY KEY, id serial PRIMARY KEY,
email_id integer REFERENCES emails(id) ON DELETE RESTRICT NOT NULL UNIQUE, email_id integer REFERENCES emails(id) ON DELETE RESTRICT NOT NULL UNIQUE,
passwordhash varchar(255) NOT NULL, passwordhash varchar(255) NOT NULL,
enabled boolean NOT NULL DEFAULT TRUE, enabled boolean NOT NULL DEFAULT TRUE,
is_admin boolean NOT NULL DEFAULT FALSE, is_admin boolean NOT NULL DEFAULT FALSE,
can_use_different_alias_domain boolean NOT NULL DEFAULT FALSE,
note text NOT NULL DEFAULT '' note text NOT NULL DEFAULT ''
); );
-- source=me2@finn.st destination=me@finn.st: me2 is an alias of me
CREATE TABLE IF NOT EXISTS aliases ( CREATE TABLE IF NOT EXISTS aliases (
id serial PRIMARY KEY, id serial PRIMARY KEY,
source_email_id integer REFERENCES emails(id) ON DELETE RESTRICT NOT NULL, source_email_id integer REFERENCES emails(id) ON DELETE RESTRICT NOT NULL,
@ -24,10 +29,3 @@ CREATE TABLE IF NOT EXISTS aliases (
note text, note text,
UNIQUE(source_email_id, destination_user_id) 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$$;

View File

@ -12,6 +12,7 @@ class User(NoteLinesMixin):
passwordhash: str passwordhash: str
enabled: bool enabled: bool
is_admin: bool is_admin: bool
can_use_different_alias_domain: bool
note: str note: str
@ -46,13 +47,13 @@ def get_user(cur, query, param):
@with_cursor @with_cursor
def get_users(cur): def get_users(cur):
cur.execute(base_user_query) cur.execute(f"{base_user_query} ORDER BY e.email ASC")
return [User(*row) for row in cur.fetchall()] return [User(*row) for row in cur.fetchall()]
@with_cursor @with_cursor
def update_user(cur, id, **fields): 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( cur.execute(
"UPDATE users SET " "UPDATE users SET "
+ ", ".join(f"{field}=%s" for field in fields.keys()) + ", ".join(f"{field}=%s" for field in fields.keys())
@ -62,13 +63,23 @@ def update_user(cur, id, **fields):
@with_cursor @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) email_id = create_email(cur, email)
cur.execute( cur.execute(
"""\ """\
INSERT INTO users (email_id, passwordhash, enabled, is_admin, note) INSERT INTO users
VALUES (%s, %s, %s, %s, %s)""", (email_id, passwordhash, enabled, is_admin, can_use_different_alias_domain, note)
[email_id, passwordhash, enabled, is_admin, 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]) cur.execute("INSERT INTO emails (email) VALUES (%s) RETURNING id", [email])
email_id = cur.fetchone()[0] email_id = cur.fetchone()[0]
cur.execute( cur.execute(
"INSERT INTO users (email_id, passwordhash, enabled, is_admin, note) VALUES (%s, %s, %s, %s, %s)", # noqa "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, "Created by commandline"], [email_id, hash, True, is_admin, is_admin, "Created by commandline"],
) )
user_type = "admin" if is_admin else "user" user_type = "admin" if is_admin else "user"
print(f"Created {user_type} {email}") print(f"Created {user_type} {email}")

View File

@ -22,7 +22,7 @@ class BaseMiddleware:
class LoggingMiddleware(BaseMiddleware): class LoggingMiddleware(BaseMiddleware):
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
real_ip = request.headers.get("X-RealIp", None) real_ip = request.headers.get("X-Real-IP", None)
if real_ip is None and self.app.debug: if real_ip is None and self.app.debug:
real_ip = request.remote_addr real_ip = request.remote_addr
setattr(request, "real_remote_addr", real_ip) setattr(request, "real_remote_addr", real_ip)
@ -58,7 +58,7 @@ class ExceptionMiddleware(BaseMiddleware):
def __call__(self, *args, **kwargs): def __call__(self, *args, **kwargs):
try: try:
return self.handler(*args, **kwargs) return self.handler(*args, **kwargs)
except (NotFound, PermissionDenied) as e: except (NotFound, PermissionDenied):
notFoundView = NotFoundView(**kwargs) notFoundView = NotFoundView(**kwargs)
return notFoundView.render(), 404 return notFoundView.render(), 404

View File

@ -1,6 +1,6 @@
from max.permissions import AllowAnyAccess from max.permissions import AllowAnyAccess
from max.views import BaseTemplateGetView
from max.translations import t from max.translations import t
from max.views import BaseTemplateGetView
class NotFoundView(AllowAnyAccess, BaseTemplateGetView): class NotFoundView(AllowAnyAccess, BaseTemplateGetView):

View File

@ -1,5 +1,4 @@
{ {
"test.test": "LOL",
"Hi": "Moin", "Hi": "Moin",
"You are not logged in": "Du bist nicht angemeldet", "You are not logged in": "Du bist nicht angemeldet",
"Not found": "Nicht gefunden", "Not found": "Nicht gefunden",
@ -41,10 +40,9 @@
"Edit note for alias {alias}": "Notiz für Alias {alias} bearbeiten", "Edit note for alias {alias}": "Notiz für Alias {alias} bearbeiten",
"Email or password was wrong!": "Email oder Password ist falsch!", "Email or password was wrong!": "Email oder Password ist falsch!",
"This account is not enabled!": "Dieser Account ist nicht aktiviert!", "This account is not enabled!": "Dieser Account ist nicht aktiviert!",
"Login was sucessfull!": "Erfolgreich anbemeldet!", "Login was sucessfull!": "Erfolgreich angemeldet!",
"Logout was sucessfull!": "Erfolgreich abgemeldet!", "Logout was sucessfull!": "Erfolgreich abgemeldet!",
"The email is not valid": "Die Email ist nicht valide", "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", "Email must be given": "Die Email muss gegeben sein",
"Enabled is not set": "Aktiviert ist nicht gesetzt", "Enabled is not set": "Aktiviert ist nicht gesetzt",
"Note must be a string": "Die Notiz muss eine Zeichenkette sein", "Note must be a string": "Die Notiz muss eine Zeichenkette sein",
@ -64,5 +62,8 @@
"Email already exists.": "Email existiert bereits.", "Email already exists.": "Email existiert bereits.",
"Email already exists: {msg}": "Email existiert bereits: {msg}", "Email already exists: {msg}": "Email existiert bereits: {msg}",
"Used as an user.": "Wird für einen Nutzer verwendet", "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"
} }

View File

@ -15,12 +15,16 @@ from .user.detail import UserDetail
from .user.edit_note import UserEditNote from .user.edit_note import UserEditNote
from .user.list import UserList from .user.list import UserList
from .user.toggle_admin import UserToggleAdmin from .user.toggle_admin import UserToggleAdmin
from .user.toggle_can_use_different_alias_domain import (
UserToggleCanUseDifferentAliasDomain,
)
from .user.toggle_enabled import UserToggleEnabled from .user.toggle_enabled import UserToggleEnabled
def init_routes(app): def init_routes(app):
app.add_url_rule("/", "own-user-detail", UserDetail.as_view()) app.add_url_rule("/", "own-user-detail", UserDetail.as_view())
app.add_url_rule("/list", "user-list", UserList.as_view()) app.add_url_rule("/list", "user-list", UserList.as_view())
app.add_url_rule("/user/<int:user_id>", "user-detail", UserDetail.as_view()) app.add_url_rule("/user/<int:user_id>", "user-detail", UserDetail.as_view())
app.add_url_rule("/user/create", "user-create", UserCreate.as_view()) app.add_url_rule("/user/create", "user-create", UserCreate.as_view())
app.add_url_rule( app.add_url_rule(
@ -41,10 +45,16 @@ def init_routes(app):
"user-toggle-admin", "user-toggle-admin",
UserToggleAdmin.as_view(), UserToggleAdmin.as_view(),
) )
app.add_url_rule(
"/user/<int:user_id>/ser-toggle-can-use-different-alias-domain",
"user-toggle-can-use-different-alias-domain",
UserToggleCanUseDifferentAliasDomain.as_view(),
)
app.add_url_rule("/user/<int:user_id>/delete", "user-delete", UserDelete.as_view()) app.add_url_rule("/user/<int:user_id>/delete", "user-delete", UserDelete.as_view())
app.add_url_rule( app.add_url_rule(
"/user/<int:user_id>/aliases/create", "alias-create", AliasCreate.as_view() "/user/<int:user_id>/aliases/create", "alias-create", AliasCreate.as_view()
) )
app.add_url_rule("/alias/<int:alias_id>", "alias-detail", AliasDetail.as_view()) app.add_url_rule("/alias/<int:alias_id>", "alias-detail", AliasDetail.as_view())
app.add_url_rule( app.add_url_rule(
"/alias/<int:alias_id>/delete", "alias-delete", AliasDelete.as_view() "/alias/<int:alias_id>/delete", "alias-delete", AliasDelete.as_view()

View File

@ -1,6 +1,6 @@
from flask import flash 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.permissions import AllowAdminOrSelf
from max.translations import t from max.translations import t
@ -26,7 +26,21 @@ class AliasCreate(
else: else:
self.email = email 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 error = True
enabled = self.request.form.get("enabled") enabled = self.request.form.get("enabled")
@ -49,7 +63,7 @@ class AliasCreate(
return # force re-rendering return # force re-rendering
try: 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: except EmailAlreadyExists as e:
if self.auth_user.is_admin: if self.auth_user.is_admin:
flash( flash(
@ -61,14 +75,24 @@ class AliasCreate(
return return
flash( flash(
t("Creation of alias {email} was successful!", email=email), t("Creation of alias {email} was successful!", email=full_email),
category="success", category="success",
) )
return self.redirect() return self.redirect()
def get_context(self): def get_context(self):
context = super().get_context() context = super().get_context()
for key in ("email", "enabled", "note"): for key in ("email", "domain", "enabled", "note"):
if hasattr(self, key): if hasattr(self, key):
context[key] = getattr(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 return context
def get_user_domain(self):
return self.user.email.split("@")[1]

View File

@ -6,26 +6,14 @@ from max.db import get_domains
from max.translations import t from max.translations import t
EMAIL_REGEX = re.compile(r"^[a-zA-Z0-9+_-]+@(?P<domain>[a-zA-Z0-9-]+\.[a-z]{2,61})$")
class CheckEmailMixin: class CheckEmailMixin:
def is_email_valid(self, email: str): def is_email_valid(self, email: str):
"""Checks for the email being valid and that it has a valid domain.""" """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: if not match:
flash(t("The email is not valid"), category="error") flash(t("The email is not valid"), category="error")
return False 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 return True

View File

@ -1,7 +1,7 @@
from flask import flash from flask import flash
from max.auth import hash_password 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.permissions import AllowAdmin
from max.translations import t from max.translations import t
from max.views import BaseTemplateGetView from max.views import BaseTemplateGetView
@ -22,7 +22,18 @@ class UserCreate(AllowAdmin, CheckEmailMixin, BackToUsersMixin, BaseTemplateGetV
else: else:
self.email = email 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 error = True
pw1 = self.request.form.get("password1") pw1 = self.request.form.get("password1")
@ -57,6 +68,17 @@ class UserCreate(AllowAdmin, CheckEmailMixin, BackToUsersMixin, BaseTemplateGetV
flash(t("Admin is not set"), category="error") flash(t("Admin is not set"), category="error")
error = True 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", "") note = self.request.form.get("note", "")
if not isinstance(note, str): if not isinstance(note, str):
flash(t("Note must be a string"), category="error") flash(t("Note must be a string"), category="error")
@ -70,7 +92,12 @@ class UserCreate(AllowAdmin, CheckEmailMixin, BackToUsersMixin, BaseTemplateGetV
passwordhash = hash_password(self.password) passwordhash = hash_password(self.password)
try: try:
create_user( 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: except EmailAlreadyExists as e:
if self.auth_user.is_admin: if self.auth_user.is_admin:
@ -82,14 +109,26 @@ class UserCreate(AllowAdmin, CheckEmailMixin, BackToUsersMixin, BaseTemplateGetV
flash(t("Email already exists.")) flash(t("Email already exists."))
return 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() return self.redirect()
def get_context(self): def get_context(self):
context = super().get_context() 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): if hasattr(self, key):
context[key] = getattr(self, key) context[key] = getattr(self, key)
context["domains"] = get_domains()
return context return context
def get_webpagetitle(self): def get_webpagetitle(self):

View File

@ -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)

View File

@ -144,14 +144,21 @@ form {
margin-bottom: 8px; margin-bottom: 8px;
} }
.detail b { .detail b {
width: 4.5em;
display: inline-block; display: inline-block;
} }
.detail .note a {
margin-right: 1em;
margin-left: 1em;
vertical-align: top;
}
.detail .note b { .detail .note b {
margin-right: 1em;
margin-top: 0.5em;
vertical-align: top; vertical-align: top;
} }
.detail .note div { .detail .note div {
display: inline-block; display: inline-block;
margin-top: 0.5em;
} }
.detail form { .detail form {
margin: 0; margin: 0;
@ -174,6 +181,9 @@ form {
margin-top: 0; margin-top: 0;
margin-bottom: 0.5em; margin-bottom: 0.5em;
} }
.connection-properties .indent {
margin-left: 1.5em;
}
.checkbox { .checkbox {
padding: 8px; padding: 8px;
@ -183,6 +193,10 @@ form {
line-height: 1em; line-height: 1em;
} }
.domain-selector {
display: inline-block;
}
@media (max-width: 1200px) { @media (max-width: 1200px) {
body { body {
font-size: 13px; font-size: 13px;

View File

@ -7,7 +7,18 @@
<form id="form" class="pure-form pure-form-aligned" action="{{ url_for('alias-create', user_id=user.id) }}" method="post"> <form id="form" class="pure-form pure-form-aligned" action="{{ url_for('alias-create', user_id=user.id) }}" method="post">
<div class="pure-control-group"> <div class="pure-control-group">
<label for="email">{{ t("Email") }}</label> <label for="email">{{ t("Email") }}</label>
<input id="email" type="email" name="email" {% if email is defined %}value="{{ email }}"{% endif %} placeholder="{{ t("Email") }}"> <input id="email" type="text" name="email" {% if email is defined %}value="{{ email }}"{% endif %} placeholder="{{ t("Email") }}">
<div class="domain-selector">
{% if can_use_different_alias_domain %}
@
<select id="domain" name="domain">
{% for d in domains %}<option{% if d == domain %} selected{% endif %}>{{ d }}</option>{% endfor %}
</select>
{% else %}
@{{ domain }}
{% endif %}
</div>
</div> </div>
<div class="pure-controls checkbox"> <div class="pure-controls checkbox">

View File

@ -8,13 +8,15 @@
<div> <div>
<b>{{ t("Enabled") }}:</b> <b>{{ t("Enabled") }}:</b>
{{ macros.checkbox(alias, "enabled", url_for('alias-toggle-enabled', alias_id=alias.id, return='detail')) }} {{ macros.checkbox(alias, "enabled", url_for('alias-toggle-enabled', alias_id=alias.id, return='detail')) }}
</div><div class="note"> </div>
<div class="note">
<b>{{ t("Note") }}:</b><div>{{ macros.format_note(alias) }}</div> <b>{{ t("Note") }}:</b><div>{{ macros.format_note(alias) }}</div>
<a href="{{ url_for('alias-edit-note', alias_id=alias.id, return='detail') }}" class="button-small pure-button optional"> <a href="{{ url_for('alias-edit-note', alias_id=alias.id, return='detail') }}" class="button-small pure-button optional">
<i class="fa fa-pen"></i> <i class="fa fa-pen"></i>
{{ t("Edit") }} {{ t("Edit") }}
</a> </a>
</div><div> </div>
<div>
<a href="{{ url_for('alias-delete', alias_id=alias.id, return='detail') }}" class="button-small button-error pure-button"> <a href="{{ url_for('alias-delete', alias_id=alias.id, return='detail') }}" class="button-small button-error pure-button">
<i class="fa fa-trash"></i> <i class="fa fa-trash"></i>
{{ t("Delete") }} {{ t("Delete") }}

View File

@ -7,7 +7,14 @@
<form id="form" class="pure-form pure-form-aligned" action="{{ url_for('user-create') }}" method="post"> <form id="form" class="pure-form pure-form-aligned" action="{{ url_for('user-create') }}" method="post">
<div class="pure-control-group"> <div class="pure-control-group">
<label for="email">{{ t("Email") }}</label> <label for="email">{{ t("Email") }}</label>
<input id="email" type="email" name="email" {% if email is defined %}value="{{ email }}"{% endif %} placeholder="{{ t("Email") }}"> <input id="email" type="text" name="email" {% if email is defined %}value="{{ email }}"{% endif %} placeholder="{{ t("Email") }}">
<div class="domain-selector">
@
<select id="domain" name="domain">
{% for d in domains %}<option{% if d == domain %} selected{% endif %}>{{ d }}</option>{% endfor %}
</select>
</div>
</div> </div>
<div class="pure-control-group"> <div class="pure-control-group">
@ -34,6 +41,13 @@
</label> </label>
</div> </div>
<div class="pure-controls checkbox">
<label for="can_use_different_alias_domain">
<input id="can_use_different_alias_domain" type="checkbox" name="can_use_different_alias_domain" {% if can_use_different_alias_domain %}checked{% endif %}>
{{ t("Can use different alias domains") }}
</label>
</div>
<div class="pure-control-group"> <div class="pure-control-group">
<label for="note">{{ t("Note") }}</label> <label for="note">{{ t("Note") }}</label>
<textarea id="note" name="note" form="form">{% if note is defined %}{{ note }}{% endif %}</textarea> <textarea id="note" name="note" form="form">{% if note is defined %}{{ note }}{% endif %}</textarea>

View File

@ -24,6 +24,10 @@
<b>{{ t("Admin") }}:</b> <b>{{ t("Admin") }}:</b>
{{ macros.checkbox(user, "is_admin", url_for('user-toggle-admin', user_id=user.id, return='detail')) }} {{ macros.checkbox(user, "is_admin", url_for('user-toggle-admin', user_id=user.id, return='detail')) }}
</div> </div>
<div>
<b>{{ t("Can use different alias domains") }}:</b>
{{ macros.checkbox(user, "can_use_different_alias_domain", url_for('user-toggle-can-use-different-alias-domain', user_id=user.id, return='detail')) }}
</div>
<div class="note"> <div class="note">
<b>{{ t("Note") }}:</b><div>{{ macros.format_note(user) }}</div> <b>{{ t("Note") }}:</b><div>{{ macros.format_note(user) }}</div>
<a href="{{ url_for('user-edit-note', user_id=user.id, return='detail') }}" class="button-small pure-button optional"> <a href="{{ url_for('user-edit-note', user_id=user.id, return='detail') }}" class="button-small pure-button optional">
@ -53,10 +57,14 @@
<details class="connection-properties"> <details class="connection-properties">
<summary>{{ t("Email connection properties") }}</summary> <summary>{{ t("Email connection properties") }}</summary>
<p>IMAP: mail.finn.st, Port: 993</p> <p>IMAP: finn.st, Port 993</p>
<p>username: {{ user.email }}</p> <p class="indent">Username: {{ user.email }}</p>
<p>Connection security: SSL/TLS</p> <p class="indent">Connection security: SSL/TLS</p>
<p>Authentication method: password</p> <p class="indent">Authentication method: password</p>
<p>SMTP: finn.st, Port 587</p>
<p class="indent">Username: {{ user.email }}</p>
<p class="indent">Connection security: STARTTLS</p>
<p class="indent">Authentication method: password</p>
</details> </details>
{% if aliases|length > 0 %} {% if aliases|length > 0 %}

View File

@ -5,6 +5,7 @@ set -e
source db-setup.sh source db-setup.sh
flask add-domain finn.st flask add-domain finn.st
flask add-domain stutzenste.in
flask reset-admin admin@finn.st admin flask reset-admin admin@finn.st admin
exec "$@" exec "$@"

2
scripts/psql.sh Executable file
View File

@ -0,0 +1,2 @@
#!/bin/bash
PGPASSWORD="$PGSQL_PASSWORD" psql -h "$PGSQL_HOST" -p "$PGSQL_PORT" -U "$PGSQL_USER" -d "$PGSQL_NAME"