This commit is contained in:
2023-01-17 12:00:04 +00:00
parent 92f4ad2237
commit 0aea62c222
32 changed files with 367 additions and 157 deletions

View File

@ -7,6 +7,7 @@ from app.dashboard.base import dashboard_bp
from app.dashboard.views.enter_sudo import sudo_required
from app.db import Session
from app.models import ApiKey
from app.utils import CSRFValidationForm
class NewApiKeyForm(FlaskForm):
@ -23,9 +24,13 @@ def api_key():
.all()
)
csrf_form = CSRFValidationForm()
new_api_key_form = NewApiKeyForm()
if request.method == "POST":
if not csrf_form.validate():
flash("Invalid request", "warning")
return redirect(request.url)
if request.form.get("form-name") == "delete":
api_key_id = request.form.get("api-key-id")
@ -62,5 +67,8 @@ def api_key():
return redirect(url_for("dashboard.api_key"))
return render_template(
"dashboard/api_key.html", api_keys=api_keys, new_api_key_form=new_api_key_form
"dashboard/api_key.html",
api_keys=api_keys,
new_api_key_form=new_api_key_form,
csrf_form=csrf_form,
)

View File

@ -34,7 +34,7 @@ def batch_import_route():
if request.method == "POST":
if not csrf_form.validate():
flash("Invalid request", "warning")
redirect(request.url)
return redirect(request.url)
if len(batch_imports) > 10:
flash(
"You have too many imports already. Wait until some get cleaned up",

View File

@ -3,6 +3,7 @@ from flask_login import login_required, current_user
from flask_wtf import FlaskForm
from wtforms import StringField, validators
from app import parallel_limiter
from app.config import EMAIL_SERVERS_WITH_PRIORITY
from app.dashboard.base import dashboard_bp
from app.db import Session
@ -19,6 +20,7 @@ class NewCustomDomainForm(FlaskForm):
@dashboard_bp.route("/custom_domain", methods=["GET", "POST"])
@login_required
@parallel_limiter.lock(only_when=lambda: request.method == "POST")
def custom_domain():
custom_domains = CustomDomain.filter_by(
user_id=current_user.id, is_sl_subdomain=False

View File

@ -9,6 +9,7 @@ from wtforms import (
IntegerField,
)
from app import parallel_limiter
from app.config import (
EMAIL_DOMAIN,
ALIAS_DOMAINS,
@ -45,6 +46,7 @@ class DeleteDirForm(FlaskForm):
@dashboard_bp.route("/directory", methods=["GET", "POST"])
@login_required
@parallel_limiter.lock(only_when=lambda: request.method == "POST")
def directory():
dirs = (
Directory.filter_by(user_id=current_user.id)

View File

@ -2,10 +2,11 @@ import arrow
from flask import render_template, request, redirect, url_for, flash
from flask_login import login_required, current_user
from flask_wtf import FlaskForm
from itsdangerous import Signer
from wtforms import validators
from itsdangerous import TimestampSigner
from wtforms import validators, IntegerField
from wtforms.fields.html5 import EmailField
from app import parallel_limiter
from app.config import MAILBOX_SECRET, URL, JOB_DELETE_MAILBOX
from app.dashboard.base import dashboard_bp
from app.db import Session
@ -27,8 +28,16 @@ class NewMailboxForm(FlaskForm):
)
class DeleteMailboxForm(FlaskForm):
mailbox_id = IntegerField(
validators=[validators.DataRequired()],
)
transfer_mailbox_id = IntegerField()
@dashboard_bp.route("/mailbox", methods=["GET", "POST"])
@login_required
@parallel_limiter.lock(only_when=lambda: request.method == "POST")
def mailbox_route():
mailboxes = (
Mailbox.filter_by(user_id=current_user.id)
@ -38,28 +47,53 @@ def mailbox_route():
new_mailbox_form = NewMailboxForm()
csrf_form = CSRFValidationForm()
delete_mailbox_form = DeleteMailboxForm()
if request.method == "POST":
if not csrf_form.validate():
flash("Invalid request", "warning")
return redirect(request.url)
if request.form.get("form-name") == "delete":
mailbox_id = request.form.get("mailbox-id")
mailbox = Mailbox.get(mailbox_id)
if not delete_mailbox_form.validate():
flash("Invalid request", "warning")
return redirect(request.url)
mailbox = Mailbox.get(delete_mailbox_form.mailbox_id.data)
if not mailbox or mailbox.user_id != current_user.id:
flash("Unknown error. Refresh the page", "warning")
flash("Invalid mailbox. Refresh the page", "warning")
return redirect(url_for("dashboard.mailbox_route"))
if mailbox.id == current_user.default_mailbox_id:
flash("You cannot delete default mailbox", "error")
return redirect(url_for("dashboard.mailbox_route"))
transfer_mailbox_id = delete_mailbox_form.transfer_mailbox_id.data
if transfer_mailbox_id and transfer_mailbox_id > 0:
transfer_mailbox = Mailbox.get(transfer_mailbox_id)
if not transfer_mailbox or transfer_mailbox.user_id != current_user.id:
flash("You must transfer the aliases to a mailbox you own.")
return redirect(url_for("dashboard.mailbox_route"))
if transfer_mailbox.id == mailbox.id:
flash(
"You can not transfer the aliases to the mailbox you want to delete."
)
return redirect(url_for("dashboard.mailbox_route"))
if not transfer_mailbox.verified:
flash("Your new mailbox is not verified")
return redirect(url_for("dashboard.mailbox_route"))
# Schedule delete account job
LOG.w("schedule delete mailbox job for %s", mailbox)
LOG.w(
f"schedule delete mailbox job for {mailbox.id} with transfer to mailbox {transfer_mailbox_id}"
)
Job.create(
name=JOB_DELETE_MAILBOX,
payload={"mailbox_id": mailbox.id},
payload={
"mailbox_id": mailbox.id,
"transfer_mailbox_id": transfer_mailbox_id
if transfer_mailbox_id > 0
else None,
},
run_at=arrow.now(),
commit=True,
)
@ -72,7 +106,10 @@ def mailbox_route():
return redirect(url_for("dashboard.mailbox_route"))
if request.form.get("form-name") == "set-default":
mailbox_id = request.form.get("mailbox-id")
if not csrf_form.validate():
flash("Invalid request", "warning")
return redirect(request.url)
mailbox_id = request.form.get("mailbox_id")
mailbox = Mailbox.get(mailbox_id)
if not mailbox or mailbox.user_id != current_user.id:
@ -110,12 +147,12 @@ def mailbox_route():
elif not email_can_be_used_as_mailbox(mailbox_email):
flash(f"You cannot use {mailbox_email}.", "error")
else:
new_mailbox = Mailbox.create(
transfer_mailbox = Mailbox.create(
email=mailbox_email, user_id=current_user.id
)
Session.commit()
send_verification_email(current_user, new_mailbox)
send_verification_email(current_user, transfer_mailbox)
flash(
f"You are going to receive an email to confirm {mailbox_email}.",
@ -124,7 +161,8 @@ def mailbox_route():
return redirect(
url_for(
"dashboard.mailbox_detail_route", mailbox_id=new_mailbox.id
"dashboard.mailbox_detail_route",
mailbox_id=transfer_mailbox.id,
)
)
@ -132,38 +170,13 @@ def mailbox_route():
"dashboard/mailbox.html",
mailboxes=mailboxes,
new_mailbox_form=new_mailbox_form,
delete_mailbox_form=delete_mailbox_form,
csrf_form=csrf_form,
)
def delete_mailbox(mailbox_id: int):
from server import create_light_app
with create_light_app().app_context():
mailbox = Mailbox.get(mailbox_id)
if not mailbox:
return
mailbox_email = mailbox.email
user = mailbox.user
Mailbox.delete(mailbox_id)
Session.commit()
LOG.d("Mailbox %s %s deleted", mailbox_id, mailbox_email)
send_email(
user.email,
f"Your mailbox {mailbox_email} has been deleted",
f"""Mailbox {mailbox_email} along with its aliases are deleted successfully.
Regards,
SimpleLogin team.
""",
)
def send_verification_email(user, mailbox):
s = Signer(MAILBOX_SECRET)
s = TimestampSigner(MAILBOX_SECRET)
mailbox_id_signed = s.sign(str(mailbox.id)).decode()
verification_url = (
URL + "/dashboard/mailbox_verify" + f"?mailbox_id={mailbox_id_signed}"
@ -188,11 +201,11 @@ def send_verification_email(user, mailbox):
@dashboard_bp.route("/mailbox_verify")
def mailbox_verify():
s = Signer(MAILBOX_SECRET)
s = TimestampSigner(MAILBOX_SECRET)
mailbox_id = request.args.get("mailbox_id")
try:
r_id = int(s.unsign(mailbox_id))
r_id = int(s.unsign(mailbox_id, max_age=900))
except Exception:
flash("Invalid link. Please delete and re-add your mailbox", "error")
return redirect(url_for("dashboard.mailbox_route"))

View File

@ -4,7 +4,7 @@ from email_validator import validate_email, EmailNotValidError
from flask import render_template, request, redirect, url_for, flash
from flask_login import login_required, current_user
from flask_wtf import FlaskForm
from itsdangerous import Signer
from itsdangerous import TimestampSigner
from wtforms import validators
from wtforms.fields.html5 import EmailField
@ -210,7 +210,7 @@ def mailbox_detail_route(mailbox_id):
def verify_mailbox_change(user, mailbox, new_email):
s = Signer(MAILBOX_SECRET)
s = TimestampSigner(MAILBOX_SECRET)
mailbox_id_signed = s.sign(str(mailbox.id)).decode()
verification_url = (
f"{URL}/dashboard/mailbox/confirm_change?mailbox_id={mailbox_id_signed}"
@ -262,11 +262,11 @@ def cancel_mailbox_change_route(mailbox_id):
@dashboard_bp.route("/mailbox/confirm_change")
def mailbox_confirm_change_route():
s = Signer(MAILBOX_SECRET)
s = TimestampSigner(MAILBOX_SECRET)
signed_mailbox_id = request.args.get("mailbox_id")
try:
mailbox_id = int(s.unsign(signed_mailbox_id))
mailbox_id = int(s.unsign(signed_mailbox_id, max_age=900))
except Exception:
flash("Invalid link", "error")
return redirect(url_for("dashboard.index"))

View File

@ -5,6 +5,7 @@ from app.dashboard.base import dashboard_bp
from app.dashboard.views.enter_sudo import sudo_required
from app.db import Session
from app.models import RecoveryCode
from app.utils import CSRFValidationForm
@dashboard_bp.route("/mfa_cancel", methods=["GET", "POST"])
@ -15,8 +16,13 @@ def mfa_cancel():
flash("you don't have MFA enabled", "warning")
return redirect(url_for("dashboard.index"))
csrf_form = CSRFValidationForm()
# user cancels TOTP
if request.method == "POST":
if not csrf_form.validate():
flash("Invalid request", "warning")
return redirect(request.url)
current_user.enable_otp = False
current_user.otp_secret = None
Session.commit()
@ -28,4 +34,4 @@ def mfa_cancel():
flash("TOTP is now disabled", "warning")
return redirect(url_for("dashboard.index"))
return render_template("dashboard/mfa_cancel.html")
return render_template("dashboard/mfa_cancel.html", csrf_form=csrf_form)

View File

@ -2,7 +2,10 @@ import re
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 import parallel_limiter
from app.config import MAX_NB_SUBDOMAIN
from app.dashboard.base import dashboard_bp
from app.errors import SubdomainInTrashError
@ -13,8 +16,18 @@ from app.models import CustomDomain, Mailbox, SLDomain
_SUBDOMAIN_PATTERN = r"[0-9a-z-]{1,}"
class NewSubdomainForm(FlaskForm):
domain = StringField(
"domain", validators=[validators.DataRequired(), validators.Length(max=64)]
)
subdomain = StringField(
"subdomain", validators=[validators.DataRequired(), validators.Length(max=64)]
)
@dashboard_bp.route("/subdomain", methods=["GET", "POST"])
@login_required
@parallel_limiter.lock(only_when=lambda: request.method == "POST")
def subdomain_route():
if not current_user.subdomain_is_available():
flash("Unknown error, redirect to the home page", "error")
@ -26,9 +39,13 @@ def subdomain_route():
).all()
errors = {}
new_subdomain_form = NewSubdomainForm()
if request.method == "POST":
if request.form.get("form-name") == "create":
if not new_subdomain_form.validate():
flash("Invalid new subdomain", "warning")
return redirect(url_for("dashboard.subdomain_route"))
if not current_user.is_premium():
flash("Only premium plan can add subdomain", "warning")
return redirect(request.url)
@ -39,8 +56,8 @@ def subdomain_route():
)
return redirect(request.url)
subdomain = request.form.get("subdomain").lower().strip()
domain = request.form.get("domain").lower().strip()
subdomain = new_subdomain_form.subdomain.data.lower().strip()
domain = new_subdomain_form.domain.data.lower().strip()
if len(subdomain) < 3:
flash("Subdomain must have at least 3 characters", "error")
@ -108,4 +125,5 @@ def subdomain_route():
sl_domains=sl_domains,
errors=errors,
subdomains=subdomains,
new_subdomain_form=new_subdomain_form,
)