Merge pull request #25 from simple-login/directory

Directory
This commit is contained in:
Son Nguyen Kim 2020-01-09 10:43:22 +01:00 committed by GitHub
commit fea60c7386
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 270 additions and 14 deletions

View File

@ -13,4 +13,5 @@ from .views import (
mfa_cancel, mfa_cancel,
domain_detail, domain_detail,
lifetime_licence, lifetime_licence,
directory,
) )

View File

@ -0,0 +1,93 @@
{% extends 'default.html' %}
{% set active_page = "directory" %}
{% block title %}
Directory
{% endblock %}
{% block default_content %}
<div class="row">
<div class="col-md-8 offset-md-2">
<h1 class="h3"> Directories </h1>
<div class="alert alert-primary" role="alert">
Directory allows you to create aliases <b>on the fly</b>. <br>
Simply use <em>directory/<b>anything</b>@{{ EMAIL_DOMAIN }}</em>
next time you need an email address. <br>
The alias will be created the first time it receives an email.
</div>
{% for dir in dirs %}
<div class="card" style="max-width: 50rem">
<div class="card-body">
<h5 class="card-title">
{{ dir.name }}
</h5>
<h6 class="card-subtitle mb-2 text-muted">
Created {{ dir.created_at | dt }} <br>
<span class="font-weight-bold">{{ dir.nb_alias() }}</span> aliases.
</h6>
</div>
<div class="card-footer p-0">
<div class="row">
<div class="col">
<form method="post">
<input type="hidden" name="form-name" value="delete">
<input type="hidden" class="dir-name" value="{{ dir.name }}">
<input type="hidden" name="dir-id" value="{{ dir.id }}">
<span class="card-link btn btn-link float-right delete-dir">
Delete
</span>
</form>
</div>
</div>
</div>
</div>
{% endfor %}
{% if dirs|length > 0 %}
<hr>
{% endif %}
<form method="post" class="mt-6">
{{ new_dir_form.csrf_token }}
<input type="hidden" name="form-name" value="create">
<div class="font-weight-bold">Directory Name</div>
<div class="small-text">
Directory name must be at least 3 characters.
Only letter, number, dash (-), underscore (_) can be used.
</div>
{{ new_dir_form.name(class="form-control", placeholder="my-directory", pattern="[0-9A-Za-z-_]{3,}",
title="Only letter, number, dash (-), underscore (_) can be used. Directory name must be at least 3 characters.",
autofocus="1") }}
{{ render_field_errors(new_dir_form.name) }}
<button class="btn btn-lg btn-success mt-2">Create</button>
</form>
</div>
</div>
{% endblock %}
{% block script %}
<script>
$(".delete-dir").on("click", function (e) {
let directory = $(this).parent().find(".dir-name").val();
notie.confirm({
text: `All aliases associated with <b>${directory}</b> directory will be also deleted, ` +
" please confirm.",
cancelCallback: () => {
// nothing to do
},
submitCallback: () => {
$(this).closest("form").submit();
}
});
});
</script>
{% endblock %}

View File

@ -0,0 +1,70 @@
from flask import render_template, request, redirect, url_for, flash
from flask_login import login_required, current_user
from flask_wtf import FlaskForm
from wtforms import StringField, validators
from app.config import EMAIL_DOMAIN
from app.dashboard.base import dashboard_bp
from app.extensions import db
from app.models import Directory
class NewDirForm(FlaskForm):
name = StringField(
"name", validators=[validators.DataRequired(), validators.Length(min=3)]
)
@dashboard_bp.route("/directory", methods=["GET", "POST"])
@login_required
def directory():
# only premium user can add directory
if not current_user.is_premium():
flash("Only premium user can add directories", "warning")
return redirect(url_for("dashboard.index"))
dirs = Directory.query.filter_by(user_id=current_user.id).all()
new_dir_form = NewDirForm()
if request.method == "POST":
if request.form.get("form-name") == "delete":
dir_id = request.form.get("dir-id")
dir = Directory.get(dir_id)
if not dir:
flash("Unknown error. Refresh the page", "warning")
return redirect(url_for("dashboard.directory"))
elif dir.user_id != current_user.id:
flash("You cannot delete this directory", "warning")
return redirect(url_for("dashboard.directory"))
name = dir.name
Directory.delete(dir_id)
db.session.commit()
flash(f"Directory {name} has been deleted", "success")
return redirect(url_for("dashboard.directory"))
elif request.form.get("form-name") == "create":
if new_dir_form.validate():
new_dir_name = new_dir_form.name.data
if Directory.get_by(name=new_dir_name):
flash(f"{new_dir_name} already added", "warning")
else:
new_dir = Directory.create(
name=new_dir_name, user_id=current_user.id
)
db.session.commit()
flash(f"Directory {new_dir.name} is created", "success")
return redirect(url_for("dashboard.directory",))
return render_template(
"dashboard/directory.html",
dirs=dirs,
new_dir_form=new_dir_form,
EMAIL_DOMAIN=EMAIL_DOMAIN,
)

View File

@ -439,6 +439,11 @@ class GenEmail(db.Model, ModelMixin):
db.Boolean, nullable=False, default=False, server_default="0" db.Boolean, nullable=False, default=False, server_default="0"
) )
# to know whether an alias belongs to a directory
directory_id = db.Column(
db.ForeignKey("directory.id", ondelete="cascade"), nullable=True
)
user = db.relationship(User) user = db.relationship(User)
@classmethod @classmethod
@ -725,3 +730,16 @@ class CustomDomain(db.Model, ModelMixin):
class LifetimeCoupon(db.Model, ModelMixin): class LifetimeCoupon(db.Model, ModelMixin):
code = db.Column(db.String(128), nullable=False, unique=True) code = db.Column(db.String(128), nullable=False, unique=True)
nb_used = db.Column(db.Integer, nullable=False) nb_used = db.Column(db.Integer, nullable=False)
class Directory(db.Model, ModelMixin):
user_id = db.Column(db.ForeignKey(User.id, ondelete="cascade"), nullable=False)
name = db.Column(db.String(128), unique=True, nullable=False)
user = db.relationship(User)
def nb_alias(self):
return GenEmail.filter_by(directory_id=self.id).count()
def __repr__(self):
return f"<Directory {self.name}>"

View File

@ -50,7 +50,7 @@ from app.email_utils import (
) )
from app.extensions import db from app.extensions import db
from app.log import LOG from app.log import LOG
from app.models import GenEmail, ForwardEmail, ForwardEmailLog, CustomDomain from app.models import GenEmail, ForwardEmail, ForwardEmailLog, CustomDomain, Directory
from app.utils import random_string from app.utils import random_string
from server import create_app from server import create_app
@ -110,22 +110,46 @@ class MailHandler:
gen_email = GenEmail.get_by(email=alias) gen_email = GenEmail.get_by(email=alias)
if not gen_email: if not gen_email:
LOG.d("alias %s not exist", alias) LOG.d("alias %s not exist. Try to see if it can created on the fly", alias)
# check if alias is custom-domain alias and if the custom-domain has catch-all enabled # try to see if alias could be created on-the-fly
alias_domain = get_email_domain_part(alias) on_the_fly = False
custom_domain = CustomDomain.get_by(domain=alias_domain)
if custom_domain and custom_domain.catch_all:
LOG.d("create alias %s for domain %s", alias, custom_domain)
gen_email = GenEmail.create( # check if alias belongs to a directory, ie having directory/anything@EMAIL_DOMAIN format
email=alias, if alias.endswith(EMAIL_DOMAIN):
user_id=custom_domain.user_id, if "/" in alias:
custom_domain_id=custom_domain.id, directory_name = alias[: alias.find("/")]
automatic_creation=True, LOG.d("directory_name %s", directory_name)
)
db.session.commit() directory = Directory.get_by(name=directory_name)
if directory:
LOG.d("create alias %s for directory %s", alias, directory)
on_the_fly = True
gen_email = GenEmail.create(
email=alias,
user_id=directory.user_id,
directory_id=directory.id,
)
db.session.commit()
else: else:
# check if alias is custom-domain alias and if the custom-domain has catch-all enabled
alias_domain = get_email_domain_part(alias)
custom_domain = CustomDomain.get_by(domain=alias_domain)
if custom_domain and custom_domain.catch_all:
LOG.d("create alias %s for domain %s", alias, custom_domain)
on_the_fly = True
gen_email = GenEmail.create(
email=alias,
user_id=custom_domain.user_id,
custom_domain_id=custom_domain.id,
automatic_creation=True,
)
db.session.commit()
if not on_the_fly:
LOG.d("alias %s not exist, return 510", alias)
return "510 Email not exist" return "510 Email not exist"
user_email = gen_email.user.email user_email = gen_email.user.email

View File

@ -0,0 +1,42 @@
"""empty message
Revision ID: ba6f13ccbabb
Revises: d29cca963221
Create Date: 2020-01-08 21:23:06.288453
"""
import sqlalchemy_utils
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'ba6f13ccbabb'
down_revision = 'd29cca963221'
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('directory',
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('user_id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=128), nullable=False),
sa.ForeignKeyConstraint(['user_id'], ['users.id'], ondelete='cascade'),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.add_column('gen_email', sa.Column('directory_id', sa.Integer(), nullable=True))
op.create_foreign_key(None, 'gen_email', 'directory', ['directory_id'], ['id'], ondelete='cascade')
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_constraint(None, 'gen_email', type_='foreignkey')
op.drop_column('gen_email', 'directory_id')
op.drop_table('directory')
# ### end Alembic commands ###

View File

@ -30,6 +30,14 @@
</a> </a>
</li> </li>
<li class="nav-item">
<a href="{{ url_for('dashboard.directory') }}"
class="nav-link {{ 'active' if active_page == 'directory' }}">
<i class="fe fe-folder"></i> Alias Directory
<span class="badge badge-info" style="font-size: .5rem; top: 5px">Beta</span>
</a>
</li>
<!-- <!--
<li class="nav-item"> <li class="nav-item">
<a href="{{ url_for('discover.index') }}" <a href="{{ url_for('discover.index') }}"