Merge pull request #234 from simple-login/random-alias-domain

Random alias domain
This commit is contained in:
Son Nguyen Kim 2020-07-04 23:34:33 +02:00 committed by GitHub
commit 5e464a824c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 169 additions and 39 deletions

View File

@ -150,24 +150,19 @@
<button class="btn btn-outline-primary">Update</button> <button class="btn btn-outline-primary">Update</button>
</form> </form>
{% if current_user.has_custom_domain() %} <div class="mt-3 mb-1">Select the domain for random aliases.</div>
<div class="mt-3 mb-1">Select the domain for random aliases.</div> <form method="post" action="#random-alias" class="form-inline">
<form method="post" action="#random-alias" class="form-inline"> <input type="hidden" name="form-name" value="change-random-alias-default-domain">
<input type="hidden" name="form-name" value="change-random-alias-default-domain"> <select class="form-control mr-sm-2" name="random-alias-default-domain">
<select class="form-control mr-sm-2" name="random-alias-default-domain"> {% for is_public, domain in current_user.available_domains_for_random_alias() %}
<option value="" {% if not current_user.default_random_alias_domain_id %} selected {% endif %}> <option value="{{ domain }}"
{{ FIRST_ALIAS_DOMAIN }} (SimpleLogin domain) {% if current_user.default_random_alias_domain() == domain %} selected {% endif %} >
</option> {{ domain }} ({% if is_public %} SimpleLogin domain {% else %} your domain {% endif %})
{% for domain in current_user.custom_domains() %} </option>
<option value="{{ domain.id }}"
{% if current_user.default_random_alias_domain_id == domain.id %} selected {% endif %} >
{{ domain.domain }} (your domain)
</option>
{% endfor %} {% endfor %}
</select> </select>
<button class="btn btn-outline-primary">Update</button> <button class="btn btn-outline-primary">Update</button>
</form> </form>
{% endif %}
</div> </div>
</div> </div>

View File

@ -31,6 +31,7 @@ from app.models import (
AliasGeneratorEnum, AliasGeneratorEnum,
ManualSubscription, ManualSubscription,
SenderFormatEnum, SenderFormatEnum,
PublicDomain,
) )
from app.utils import random_string from app.utils import random_string
@ -162,20 +163,28 @@ def setting():
elif request.form.get("form-name") == "change-random-alias-default-domain": elif request.form.get("form-name") == "change-random-alias-default-domain":
default_domain = request.form.get("random-alias-default-domain") default_domain = request.form.get("random-alias-default-domain")
if default_domain: if default_domain:
default_domain_id = int(default_domain) custom_domain = CustomDomain.get_by(domain=default_domain)
# sanity check if custom_domain:
domain = CustomDomain.get(default_domain_id) # sanity check
if ( if (
not domain custom_domain.user_id != current_user.id
or domain.user_id != current_user.id or not custom_domain.verified
or not domain.verified ):
): LOG.error(
flash( "%s cannot use domain %s", current_user, default_domain
"Something went wrong, sorry for the inconvenience. Please retry. ", )
"error", else:
) # make sure only default_random_alias_domain_id or default_random_alias_public_domain_id is set
return redirect(url_for("dashboard.setting")) current_user.default_random_alias_domain_id = custom_domain.id
current_user.default_random_alias_domain_id = default_domain_id current_user.default_random_alias_public_domain_id = None
else:
public_domain = PublicDomain.get_by(domain=default_domain)
if public_domain:
# make sure only default_random_alias_domain_id or default_random_alias_public_domain_id is set
current_user.default_random_alias_public_domain_id = (
public_domain.id
)
current_user.default_random_alias_domain_id = None
else: else:
current_user.default_random_alias_domain_id = None current_user.default_random_alias_domain_id = None

View File

@ -2,7 +2,7 @@ import enum
import random import random
import uuid import uuid
from email.utils import formataddr from email.utils import formataddr
from typing import List from typing import List, Tuple
import arrow import arrow
import bcrypt import bcrypt
@ -166,8 +166,18 @@ class User(db.Model, ModelMixin, UserMixin):
# Fields for WebAuthn # Fields for WebAuthn
fido_uuid = db.Column(db.String(), nullable=True, unique=True) fido_uuid = db.Column(db.String(), nullable=True, unique=True)
# the default domain that's used when user creates a new random alias
# default_random_alias_domain_id XOR default_random_alias_public_domain_id
default_random_alias_domain_id = db.Column( default_random_alias_domain_id = db.Column(
db.ForeignKey("custom_domain.id"), nullable=True, default=None db.ForeignKey("custom_domain.id", ondelete="SET NULL"),
nullable=True,
default=None,
)
default_random_alias_public_domain_id = db.Column(
db.ForeignKey("public_domain.id", ondelete="SET NULL"),
nullable=True,
default=None,
) )
# some users could have lifetime premium # some users could have lifetime premium
@ -452,6 +462,49 @@ class User(db.Model, ModelMixin, UserMixin):
def custom_domains(self): def custom_domains(self):
return CustomDomain.filter_by(user_id=self.id, verified=True).all() return CustomDomain.filter_by(user_id=self.id, verified=True).all()
def available_domains_for_random_alias(self) -> List[Tuple[bool, str]]:
"""Return available domains for user to create random aliases
Each result record contains:
- whether the domain is public (i.e. belongs to SimpleLogin)
- the domain
"""
res = []
for public_domain in PublicDomain.query.all():
res.append((True, public_domain.domain))
for custom_domain in CustomDomain.filter_by(
user_id=self.id, verified=True
).all():
res.append((False, custom_domain.domain))
return res
def default_random_alias_domain(self) -> str:
"""return the domain used for the random alias"""
if self.default_random_alias_domain_id:
custom_domain = CustomDomain.get(self.default_random_alias_domain_id)
# sanity check
if (
not custom_domain
or not custom_domain.verified
or custom_domain.user_id != self.id
):
LOG.error("Problem with %s default random alias domain", self)
return FIRST_ALIAS_DOMAIN
return custom_domain.domain
if self.default_random_alias_public_domain_id:
public_domain = PublicDomain.get(self.default_random_alias_public_domain_id)
# sanity check
if not public_domain:
LOG.error("Problem with %s public random alias domain", self)
return FIRST_ALIAS_DOMAIN
return public_domain.domain
return FIRST_ALIAS_DOMAIN
def fido_enabled(self) -> bool: def fido_enabled(self) -> bool:
if self.fido_uuid is not None: if self.fido_uuid is not None:
return True return True
@ -824,12 +877,17 @@ class Alias(db.Model, ModelMixin):
note: str = None, note: str = None,
): ):
"""create a new random alias""" """create a new random alias"""
domain = None custom_domain = None
if user.default_random_alias_domain_id: if user.default_random_alias_domain_id:
domain = CustomDomain.get(user.default_random_alias_domain_id) custom_domain = CustomDomain.get(user.default_random_alias_domain_id)
random_email = generate_email( random_email = generate_email(
scheme=scheme, in_hex=in_hex, alias_domain=domain.domain scheme=scheme, in_hex=in_hex, alias_domain=custom_domain.domain
)
elif user.default_random_alias_public_domain_id:
public_domain = PublicDomain.get(user.default_random_alias_public_domain_id)
random_email = generate_email(
scheme=scheme, in_hex=in_hex, alias_domain=public_domain.domain
) )
else: else:
random_email = generate_email(scheme=scheme, in_hex=in_hex) random_email = generate_email(scheme=scheme, in_hex=in_hex)
@ -841,8 +899,8 @@ class Alias(db.Model, ModelMixin):
note=note, note=note,
) )
if domain: if custom_domain:
alias.custom_domain_id = domain.id alias.custom_domain_id = custom_domain.id
return alias return alias
@ -1610,3 +1668,9 @@ class Notification(db.Model, ModelMixin):
# whether user has marked the notification as read # whether user has marked the notification as read
read = db.Column(db.Boolean, nullable=False, default=False) read = db.Column(db.Boolean, nullable=False, default=False)
class PublicDomain(db.Model, ModelMixin):
"""SimpleLogin domains that all users can use"""
domain = db.Column(db.String(128), unique=True, nullable=False)

View File

@ -1,5 +1,6 @@
"""Initial loading script""" """Initial loading script"""
from app.models import Mailbox, Contact from app.config import ALIAS_DOMAINS
from app.models import Mailbox, Contact, PublicDomain
from app.log import LOG from app.log import LOG
from app.extensions import db from app.extensions import db
from app.pgp_utils import load_public_key from app.pgp_utils import load_public_key
@ -32,8 +33,20 @@ def load_pgp_public_keys():
LOG.d("Finish load_pgp_public_keys") LOG.d("Finish load_pgp_public_keys")
def add_public_domains():
for alias_domain in ALIAS_DOMAINS:
if PublicDomain.get_by(domain=alias_domain):
LOG.d("%s is already a public domain", alias_domain)
else:
LOG.info("Add %s to public domain", alias_domain)
PublicDomain.create(domain=alias_domain)
db.session.commit()
if __name__ == "__main__": if __name__ == "__main__":
app = create_app() app = create_app()
with app.app_context(): with app.app_context():
load_pgp_public_keys() load_pgp_public_keys()
add_public_domains()

View File

@ -0,0 +1,44 @@
"""empty message
Revision ID: 270d598c51e3
Revises: 7128f87af701
Create Date: 2020-07-04 23:32:25.297082
"""
import sqlalchemy_utils
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '270d598c51e3'
down_revision = '7128f87af701'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('public_domain',
sa.Column('id', sa.Integer(), autoincrement=True, nullable=False),
sa.Column('created_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=False),
sa.Column('updated_at', sqlalchemy_utils.types.arrow.ArrowType(), nullable=True),
sa.Column('domain', sa.String(length=128), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('domain')
)
op.add_column('users', sa.Column('default_random_alias_public_domain_id', sa.Integer(), nullable=True))
op.drop_constraint('users_default_random_alias_domain_id_fkey', 'users', type_='foreignkey')
op.create_foreign_key(None, 'users', 'custom_domain', ['default_random_alias_domain_id'], ['id'], ondelete='SET NULL')
op.create_foreign_key(None, 'users', 'public_domain', ['default_random_alias_public_domain_id'], ['id'], ondelete='SET NULL')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'users', type_='foreignkey')
op.drop_constraint(None, 'users', type_='foreignkey')
op.create_foreign_key('users_default_random_alias_domain_id_fkey', 'users', 'custom_domain', ['default_random_alias_domain_id'], ['id'])
op.drop_column('users', 'default_random_alias_public_domain_id')
op.drop_table('public_domain')
# ### end Alembic commands ###

View File

@ -64,6 +64,7 @@ from app.models import (
Referral, Referral,
AliasMailbox, AliasMailbox,
Notification, Notification,
PublicDomain,
) )
from app.monitor.base import monitor_bp from app.monitor.base import monitor_bp
from app.oauth.base import oauth_bp from app.oauth.base import oauth_bp
@ -286,6 +287,10 @@ def fake_data():
) )
db.session.commit() db.session.commit()
for d in ["d1.localhost", "d2.localhost"]:
PublicDomain.create(domain=d)
db.session.commit()
@login_manager.user_loader @login_manager.user_loader
def load_user(user_id): def load_user(user_id):