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,
domain_detail,
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"
)
# 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)
@classmethod
@ -725,3 +730,16 @@ class CustomDomain(db.Model, ModelMixin):
class LifetimeCoupon(db.Model, ModelMixin):
code = db.Column(db.String(128), nullable=False, unique=True)
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.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 server import create_app
@ -110,22 +110,46 @@ class MailHandler:
gen_email = GenEmail.get_by(email=alias)
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
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)
# try to see if alias could be created on-the-fly
on_the_fly = False
gen_email = GenEmail.create(
email=alias,
user_id=custom_domain.user_id,
custom_domain_id=custom_domain.id,
automatic_creation=True,
)
db.session.commit()
# check if alias belongs to a directory, ie having directory/anything@EMAIL_DOMAIN format
if alias.endswith(EMAIL_DOMAIN):
if "/" in alias:
directory_name = alias[: alias.find("/")]
LOG.d("directory_name %s", directory_name)
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:
# 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"
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>
</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">
<a href="{{ url_for('discover.index') }}"