Implement alias generator schemes

This commit is contained in:
doanguyen 2019-12-26 12:21:28 +01:00 committed by Son Nguyen Kim
parent 1848e5a944
commit e2e9cc6b5d
7 changed files with 128 additions and 27 deletions

View File

@ -24,7 +24,7 @@
</form>
</div>
<div class="col-lg-3 offset-lg-6 pr-0 mt-1">
<div class="col-lg-4 offset-lg-5 pr-0 mt-1">
<div class="btn-group float-right" role="group">
<form method="post">
<input type="hidden" name="form-name" value="create-custom-email">
@ -33,13 +33,34 @@
class="btn btn-primary mr-2">New Email Alias
</button>
</form>
<form method="post">
<input type="hidden" name="form-name" value="create-random-email">
<button data-toggle="tooltip"
title="Create a totally random alias"
class="btn btn-success">Random Alias
</button>
</form>
<div class="btn-group" role="group">
<form method="post">
<input type="hidden" name="form-name" value="create-random-email">
<button data-toggle="tooltip"
title="Create a totally random alias"
class="btn btn-success">Random Alias
</button>
</form>
<button id="btnGroupDrop1" type="button" class="btn btn-success dropdown-toggle"
data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="btnGroupDrop1">
<div class="">
<form method="post">
<input type="hidden" name="form-name" value="create-random-email">
<input type="hidden" name="generator_scheme" value="{{ AliasGeneratorEnum.word.value }}">
<button class="dropdown-item">By random words</button>
</form>
</div>
<div class="">
<form method="post">
<input type="hidden" name="form-name" value="create-random-email">
<input type="hidden" name="generator_scheme" value="{{ AliasGeneratorEnum.uuid.value }}">
<button class="dropdown-item">By UUID</button>
</form>
</div>
</div>
</div>
</div>
</div>

View File

@ -65,6 +65,18 @@
<input type="hidden" name="form-name" value="change-password">
<button class="btn btn-outline-primary">Change password</button>
</form>
<hr>
<h3 class="mb-0">Random Alias</h3>
<div class="small-text mb-3">Choose how to create your email alias by default</div>
<form method="post" class="form-inline">
<input type="hidden" name="form-name" value="change-alias-generator">
<select class="custom-select mr-sm-2" name="alias-generator-scheme" id="alias-generator-scheme">
<option value="{{ AliasGeneratorEnum.word.value }}" {% if current_user.alias_generator == AliasGeneratorEnum.word.value %} selected {% endif %} >Based on Random {{ AliasGeneratorEnum.word.name.capitalize() }}</option>
<option value="{{ AliasGeneratorEnum.uuid.value }}" {% if current_user.alias_generator == AliasGeneratorEnum.uuid.value %} selected {% endif %} >Based on {{ AliasGeneratorEnum.uuid.name.upper() }}</option>
</select>
<button class="btn btn-outline-primary">Update Preference</button>
</form>
<hr>
<h3 class="mb-0">Export Data</h3>

View File

@ -9,7 +9,7 @@ from app.config import HIGHLIGHT_GEN_EMAIL_ID
from app.dashboard.base import dashboard_bp
from app.extensions import db
from app.log import LOG
from app.models import GenEmail, ClientUser, ForwardEmail, ForwardEmailLog, DeletedAlias
from app.models import GenEmail, ClientUser, ForwardEmail, ForwardEmailLog, DeletedAlias, AliasGeneratorEnum
@dataclass
@ -57,7 +57,10 @@ def index():
elif request.form.get("form-name") == "create-random-email":
if current_user.can_create_new_alias():
gen_email = GenEmail.create_new_random(user_id=current_user.id)
scheme = int(request.form.get("generator_scheme") or current_user.alias_generator)
if not scheme or not AliasGeneratorEnum.has_value(scheme):
scheme = current_user.alias_generator
gen_email = GenEmail.create_new_random(user_id=current_user.id, scheme=scheme)
db.session.commit()
LOG.d("generate new email %s for user %s", gen_email, current_user)
@ -99,9 +102,9 @@ def index():
client_users = (
ClientUser.filter_by(user_id=current_user.id)
.options(joinedload(ClientUser.client))
.options(joinedload(ClientUser.gen_email))
.all()
.options(joinedload(ClientUser.client))
.options(joinedload(ClientUser.gen_email))
.all()
)
sorted(client_users, key=lambda cu: cu.client.name)
@ -112,6 +115,7 @@ def index():
aliases=get_alias_info(current_user.id, query, highlight_gen_email_id),
highlight_gen_email_id=highlight_gen_email_id,
query=query,
AliasGeneratorEnum=AliasGeneratorEnum
)
@ -150,8 +154,8 @@ def get_alias_info(user_id, query=None, highlight_gen_email_id=None) -> [AliasIn
# also add alias that has no forward email or log
q = (
db.session.query(GenEmail)
.filter(GenEmail.email.notin_(aliases.keys()))
.filter(GenEmail.user_id == user_id)
.filter(GenEmail.email.notin_(aliases.keys()))
.filter(GenEmail.user_id == user_id)
)
if query:

View File

@ -23,7 +23,7 @@ from app.models import (
DeletedAlias,
CustomDomain,
Client,
)
AliasGeneratorEnum)
from app.utils import random_string
@ -121,6 +121,13 @@ def setting():
logout_user()
return redirect(url_for("auth.register"))
elif request.form.get("form-name") == "change-alias-generator":
scheme = int(request.form.get("alias-generator-scheme"))
if AliasGeneratorEnum.has_value(scheme):
current_user.alias_generator = scheme
db.session.commit()
flash("Your preference has been updated", "success")
elif request.form.get("form-name") == "export-data":
data = {
"email": current_user.email,
@ -157,6 +164,7 @@ def setting():
PlanEnum=PlanEnum,
promo_form=promo_form,
pending_email=pending_email,
AliasGeneratorEnum=AliasGeneratorEnum
)

View File

@ -1,5 +1,6 @@
import enum
import random
import uuid
import arrow
import bcrypt
@ -83,6 +84,15 @@ class PlanEnum(enum.Enum):
yearly = 3
class AliasGeneratorEnum(enum.Enum):
word = 1 # aliases are generated based on random words
uuid = 2 # aliases are generated based on uuid
@classmethod
def has_value(cls, value: int) -> bool:
return value in set(item.value for item in cls)
class User(db.Model, ModelMixin, UserMixin):
__tablename__ = "users"
email = db.Column(db.String(128), unique=True, nullable=False)
@ -90,6 +100,7 @@ class User(db.Model, ModelMixin, UserMixin):
password = db.Column(db.String(128), nullable=False)
name = db.Column(db.String(128), nullable=False)
is_admin = db.Column(db.Boolean, nullable=False, default=False)
alias_generator = db.Column(db.Integer, nullable=True, default=AliasGeneratorEnum.word.value)
activated = db.Column(db.Boolean, default=False, nullable=False)
@ -310,8 +321,8 @@ class Client(db.Model, ModelMixin):
def last_user_login(self) -> "ClientUser":
client_user = (
ClientUser.query.filter(ClientUser.client_id == self.id)
.order_by(ClientUser.updated_at)
.first()
.order_by(ClientUser.updated_at)
.first()
)
if client_user:
return client_user
@ -367,20 +378,27 @@ class OauthToken(db.Model, ModelMixin):
return self.expired < arrow.now()
def generate_email() -> str:
"""generate an email address that does not exist before"""
random_email = random_words() + "@" + EMAIL_DOMAIN
def generate_email(scheme: int = 1, in_hex: bool = False) -> str:
"""generate an email address that does not exist before
:param scheme: int, value of AliasGeneratorEnum, indicate how the email is generated
:type in_hex: bool, if the generate scheme is uuid, is hex favorable?
"""
if scheme == AliasGeneratorEnum.uuid.value:
name = uuid.uuid4().hex if in_hex else uuid.uuid4().__str__()
random_email = name + "@" + EMAIL_DOMAIN
else:
random_email = random_words() + "@" + EMAIL_DOMAIN
# check that the client does not exist yet
if not GenEmail.get_by(email=random_email) and not DeletedAlias.get_by(
email=random_email
email=random_email
):
LOG.debug("generate email %s", random_email)
return random_email
# Rerun the function
LOG.warning("email %s already exists, generate a new email", random_email)
return generate_email()
return generate_email(scheme=scheme, in_hex=in_hex)
class GenEmail(db.Model, ModelMixin):
@ -413,9 +431,9 @@ class GenEmail(db.Model, ModelMixin):
return GenEmail.create(user_id=user_id, email=email)
@classmethod
def create_new_random(cls, user_id):
def create_new_random(cls, user_id, scheme: int = 1, in_hex: bool = False):
"""create a new random alias"""
random_email = generate_email()
random_email = generate_email(scheme=scheme, in_hex=in_hex)
return GenEmail.create(user_id=user_id, email=random_email)
def __repr__(self):
@ -552,8 +570,8 @@ class ForwardEmail(db.Model, ModelMixin):
"""return the most recent reply"""
return (
ForwardEmailLog.query.filter_by(forward_id=self.id, is_reply=True)
.order_by(desc(ForwardEmailLog.created_at))
.first()
.order_by(desc(ForwardEmailLog.created_at))
.first()
)

View File

@ -0,0 +1,29 @@
"""alias generator scheme
Revision ID: 8f53e718c79a
Revises: 9e1b06b9df13
Create Date: 2019-12-25 21:21:27.573197
"""
import sqlalchemy_utils
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = '8f53e718c79a'
down_revision = '9e1b06b9df13'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.add_column('users', sa.Column('alias_generator', sa.Integer(), nullable=True))
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_column('users', 'alias_generator')
# ### end Alembic commands ###

View File

@ -1,4 +1,7 @@
from uuid import UUID
import arrow
import pytest
from app.config import EMAIL_DOMAIN, MAX_NB_EMAIL_FREE_PLAN
from app.extensions import db
@ -9,6 +12,12 @@ def test_generate_email(flask_client):
email = generate_email()
assert email.endswith("@" + EMAIL_DOMAIN)
with pytest.raises(ValueError):
UUID(email.split("@")[0], version=4)
email_uuid = generate_email(scheme=2)
assert UUID(email_uuid.split("@")[0], version=4)
def test_profile_picture_url(flask_client):
user = User.create(