mirror of
https://github.com/simple-login/app.git
synced 2024-09-30 05:31:30 +02:00
commit
fea60c7386
@ -13,4 +13,5 @@ from .views import (
|
||||
mfa_cancel,
|
||||
domain_detail,
|
||||
lifetime_licence,
|
||||
directory,
|
||||
)
|
||||
|
93
app/dashboard/templates/dashboard/directory.html
Normal file
93
app/dashboard/templates/dashboard/directory.html
Normal 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 %}
|
70
app/dashboard/views/directory.py
Normal file
70
app/dashboard/views/directory.py
Normal 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,
|
||||
)
|
@ -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}>"
|
||||
|
@ -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
|
||||
|
42
migrations/versions/2020_010821_ba6f13ccbabb_.py
Normal file
42
migrations/versions/2020_010821_ba6f13ccbabb_.py
Normal 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 ###
|
@ -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') }}"
|
||||
|
Loading…
Reference in New Issue
Block a user