diff --git a/app/.gitignore b/app/.gitignore index de5ebfe..e7cea4d 100644 --- a/app/.gitignore +++ b/app/.gitignore @@ -15,3 +15,4 @@ venv/ .coverage htmlcov adhoc +.env.* \ No newline at end of file diff --git a/app/Dockerfile b/app/Dockerfile index 2b8916f..61638f7 100644 --- a/app/Dockerfile +++ b/app/Dockerfile @@ -2,7 +2,7 @@ FROM node:10.17.0-alpine AS npm WORKDIR /code COPY ./static/package*.json /code/static/ -RUN cd /code/static && npm install +RUN cd /code/static && npm ci # Main image FROM python:3.10 diff --git a/app/app/api/views/mailbox.py b/app/app/api/views/mailbox.py index bb1e94e..e99fd82 100644 --- a/app/app/api/views/mailbox.py +++ b/app/app/api/views/mailbox.py @@ -78,12 +78,15 @@ def delete_mailbox(mailbox_id): Delete mailbox Input: mailbox_id: in url + (optional) transfer_aliases_to: in body. Id of the new mailbox for the aliases. + If omitted or the value is set to -1, + the aliases of the mailbox will be deleted too. Output: 200 if deleted successfully """ user = g.user - mailbox = Mailbox.get(mailbox_id) + mailbox = Mailbox.get(id=mailbox_id) if not mailbox or mailbox.user_id != user.id: return jsonify(error="Forbidden"), 403 @@ -91,11 +94,36 @@ def delete_mailbox(mailbox_id): if mailbox.id == user.default_mailbox_id: return jsonify(error="You cannot delete the default mailbox"), 400 + data = request.get_json() or {} + transfer_mailbox_id = data.get("transfer_aliases_to") + if transfer_mailbox_id and int(transfer_mailbox_id) >= 0: + transfer_mailbox = Mailbox.get(transfer_mailbox_id) + + if not transfer_mailbox or transfer_mailbox.user_id != user.id: + return ( + jsonify(error="You must transfer the aliases to a mailbox you own."), + 403, + ) + + if transfer_mailbox_id == mailbox_id: + return ( + jsonify( + error="You can not transfer the aliases to the mailbox you want to delete." + ), + 400, + ) + + if not transfer_mailbox.verified: + return jsonify(error="Your new mailbox is not verified"), 400 + # Schedule delete account job LOG.w("schedule delete mailbox job for %s", mailbox) Job.create( name=JOB_DELETE_MAILBOX, - payload={"mailbox_id": mailbox.id}, + payload={ + "mailbox_id": mailbox.id, + "transfer_mailbox_id": transfer_mailbox_id, + }, run_at=arrow.now(), commit=True, ) diff --git a/app/app/dashboard/views/api_key.py b/app/app/dashboard/views/api_key.py index fe6a655..67dfc9c 100644 --- a/app/app/dashboard/views/api_key.py +++ b/app/app/dashboard/views/api_key.py @@ -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, ) diff --git a/app/app/dashboard/views/batch_import.py b/app/app/dashboard/views/batch_import.py index 6d064a7..7a23d37 100644 --- a/app/app/dashboard/views/batch_import.py +++ b/app/app/dashboard/views/batch_import.py @@ -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", diff --git a/app/app/dashboard/views/custom_domain.py b/app/app/dashboard/views/custom_domain.py index dfea5e8..875b5db 100644 --- a/app/app/dashboard/views/custom_domain.py +++ b/app/app/dashboard/views/custom_domain.py @@ -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 diff --git a/app/app/dashboard/views/directory.py b/app/app/dashboard/views/directory.py index e8c9a7a..e32f8ec 100644 --- a/app/app/dashboard/views/directory.py +++ b/app/app/dashboard/views/directory.py @@ -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) diff --git a/app/app/dashboard/views/mailbox.py b/app/app/dashboard/views/mailbox.py index 57bbfb0..e6f41ed 100644 --- a/app/app/dashboard/views/mailbox.py +++ b/app/app/dashboard/views/mailbox.py @@ -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")) diff --git a/app/app/dashboard/views/mailbox_detail.py b/app/app/dashboard/views/mailbox_detail.py index fee26d4..161e276 100644 --- a/app/app/dashboard/views/mailbox_detail.py +++ b/app/app/dashboard/views/mailbox_detail.py @@ -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")) diff --git a/app/app/dashboard/views/mfa_cancel.py b/app/app/dashboard/views/mfa_cancel.py index c6c4964..484a2d2 100644 --- a/app/app/dashboard/views/mfa_cancel.py +++ b/app/app/dashboard/views/mfa_cancel.py @@ -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) diff --git a/app/app/dashboard/views/subdomain.py b/app/app/dashboard/views/subdomain.py index bca190e..04f7880 100644 --- a/app/app/dashboard/views/subdomain.py +++ b/app/app/dashboard/views/subdomain.py @@ -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, ) diff --git a/app/app/handler/unsubscribe_encoder.py b/app/app/handler/unsubscribe_encoder.py index 8d74829..bc07942 100644 --- a/app/app/handler/unsubscribe_encoder.py +++ b/app/app/handler/unsubscribe_encoder.py @@ -42,9 +42,11 @@ class UnsubscribeLink: class UnsubscribeEncoder: @staticmethod def encode( - action: UnsubscribeAction, data: Union[int, UnsubscribeOriginalData] + action: UnsubscribeAction, + data: Union[int, UnsubscribeOriginalData], + force_web: bool = False, ) -> UnsubscribeLink: - if config.UNSUBSCRIBER: + if config.UNSUBSCRIBER and not force_web: return UnsubscribeLink(UnsubscribeEncoder.encode_mailto(action, data), True) return UnsubscribeLink(UnsubscribeEncoder.encode_url(action, data), False) diff --git a/app/app/handler/unsubscribe_handler.py b/app/app/handler/unsubscribe_handler.py index 4a58b3d..143cde6 100644 --- a/app/app/handler/unsubscribe_handler.py +++ b/app/app/handler/unsubscribe_handler.py @@ -49,7 +49,7 @@ class UnsubscribeHandler: return status.E507 mailbox = Mailbox.get_by(email=envelope.mail_from) if not mailbox: - LOG.w("Unknown mailbox %s", msg[headers.SUBJECT]) + LOG.w("Unknown mailbox %s", envelope.mail_from) return status.E507 if unsub_data.action == UnsubscribeAction.DisableAlias: diff --git a/app/app/newsletter_utils.py b/app/app/newsletter_utils.py index 2a00c33..3be21ce 100644 --- a/app/app/newsletter_utils.py +++ b/app/app/newsletter_utils.py @@ -27,13 +27,15 @@ def send_newsletter_to_user(newsletter, user) -> (bool, str): comm_alias_id = comm_alias.id unsubscribe_oneclick = unsubscribe_link - if via_email: + if via_email and comm_alias_id > -1: unsubscribe_oneclick = UnsubscribeEncoder.encode( - UnsubscribeAction.DisableAlias, comm_alias_id - ) + UnsubscribeAction.DisableAlias, + comm_alias_id, + force_web=True, + ).link send_email( - comm_alias.email, + comm_email, newsletter.subject, text_template.render( user=user, diff --git a/app/app/redis_services.py b/app/app/redis_services.py index 22c32b8..674d24f 100644 --- a/app/app/redis_services.py +++ b/app/app/redis_services.py @@ -7,7 +7,7 @@ from app.session import RedisSessionStore def initialize_redis_services(app: flask.Flask, redis_url: str): - if redis_url.startswith("redis://"): + if redis_url.startswith("redis://") or redis_url.startswith("rediss://"): storage = limits.storage.RedisStorage(redis_url) app.session_interface = RedisSessionStore(storage.storage, storage.storage, app) set_redis_concurrent_lock(storage) diff --git a/app/docs/api.md b/app/docs/api.md index fa9864b..9f9011f 100644 --- a/app/docs/api.md +++ b/app/docs/api.md @@ -387,7 +387,7 @@ Input: - `Authentication` header that contains the api key - (Optional but recommended) `hostname` passed in query string -- (Optional) mode: either `uuid` or `word`. By default, use the user setting when creating new random alias. +- (Optional) mode: either `uuid` or `word` passed in query string. By default, use the user setting when creating new random alias. - Request Message Body in json (`Content-Type` is `application/json`) - (Optional) note: alias note @@ -764,6 +764,7 @@ Input: - `Authentication` header that contains the api key - `mailbox_id`: in url +- (optional) `transfer_aliases_to`: in body as json. id of the new mailbox for the aliases. If omitted or set to -1, the aliases will be delete with the mailbox. Output: diff --git a/app/job_runner.py b/app/job_runner.py index d408106..0a710c3 100644 --- a/app/job_runner.py +++ b/app/job_runner.py @@ -124,6 +124,58 @@ def welcome_proton(user): ) +def delete_mailbox_job(job: Job): + mailbox_id = job.payload.get("mailbox_id") + mailbox = Mailbox.get(mailbox_id) + if not mailbox: + return + + transfer_mailbox_id = job.payload.get("transfer_mailbox_id") + alias_transferred_to = None + if transfer_mailbox_id: + transfer_mailbox = Mailbox.get(transfer_mailbox_id) + if transfer_mailbox: + alias_transferred_to = transfer_mailbox.email + + for alias in mailbox.aliases: + if alias.mailbox_id == mailbox.id: + alias.mailbox_id = transfer_mailbox.id + if transfer_mailbox in alias._mailboxes: + alias._mailboxes.remove(transfer_mailbox) + else: + alias._mailboxes.remove(mailbox) + if transfer_mailbox not in alias._mailboxes: + alias._mailboxes.append(transfer_mailbox) + Session.commit() + + mailbox_email = mailbox.email + user = mailbox.user + Mailbox.delete(mailbox_id) + Session.commit() + LOG.d("Mailbox %s %s deleted", mailbox_id, mailbox_email) + + if alias_transferred_to: + send_email( + user.email, + f"Your mailbox {mailbox_email} has been deleted", + f"""Mailbox {mailbox_email} and its alias have been transferred to {alias_transferred_to}. + Regards, + SimpleLogin team. + """, + retries=3, + ) + else: + send_email( + user.email, + f"Your mailbox {mailbox_email} has been deleted", + f"""Mailbox {mailbox_email} along with its aliases have been deleted successfully. + Regards, + SimpleLogin team. + """, + retries=3, + ) + + def process_job(job: Job): if job.name == config.JOB_ONBOARDING_1: user_id = job.payload.get("user_id") @@ -178,27 +230,7 @@ def process_job(job: Job): retries=3, ) elif job.name == config.JOB_DELETE_MAILBOX: - mailbox_id = job.payload.get("mailbox_id") - 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. -""", - retries=3, - ) + delete_mailbox_job(job) elif job.name == config.JOB_DELETE_DOMAIN: custom_domain_id = job.payload.get("custom_domain_id") diff --git a/app/local_data/test_words.txt b/app/local_data/test_words.txt index 2f45af8..1d96cec 100644 --- a/app/local_data/test_words.txt +++ b/app/local_data/test_words.txt @@ -3552,7 +3552,6 @@ impute inaner inborn inbred -incest inched inches incing diff --git a/app/local_data/words.txt b/app/local_data/words.txt index 7cd573f..fba2835 100644 --- a/app/local_data/words.txt +++ b/app/local_data/words.txt @@ -1494,7 +1494,6 @@ youth pressure submitted boston -incest debt keywords medium @@ -45883,7 +45882,6 @@ yue piu oligo chairpersons -incesto spca zapper materialized @@ -69506,7 +69504,6 @@ shaadi lovehoney austrians annemarie -incesttaboo fml craves teleportation @@ -77183,7 +77180,6 @@ schwul objectivist aftershocks ornette -incestuous antic worland abed @@ -78300,7 +78296,6 @@ acheive pochette mutcd kirschner -incestquest tras babor shirin @@ -82745,7 +82740,6 @@ snagging viviun iniquities oav -inceststories incinerated ornstein matc @@ -84958,7 +84952,6 @@ repartee pharmacie skus lyttelton -inceste barska comida ciated @@ -93685,7 +93678,6 @@ amundson tinta withholds wfn -incestcartoons westpoint cancelable houseplant @@ -118454,7 +118446,6 @@ jtr zeilenga arenaria pazza -incests upmann jezabel dowlnoad @@ -119737,7 +119728,6 @@ therrien spysweeper psrc polgar -incestgrrl dunnville speeders redraws @@ -176740,7 +176730,6 @@ cebas tenebril rcsdiff leclercq -incestual gouse anga peats @@ -279833,7 +279822,6 @@ freshies ceravolo caespitosa streeet -incestincest huwag disordering burdur @@ -296614,7 +296602,6 @@ outwits oblog mulqueen menck -incestlinks imputable guandong gorgan @@ -316826,7 +316813,6 @@ wartung portinatx orfeon observar -incesticide herro didt comosus @@ -322556,7 +322542,6 @@ manorhaven lounsbery linuxtracker liberales -incestos haramayn greyer goflo diff --git a/app/local_data/words_alpha.txt b/app/local_data/words_alpha.txt index 22990c7..868fcf9 100644 --- a/app/local_data/words_alpha.txt +++ b/app/local_data/words_alpha.txt @@ -149803,11 +149803,6 @@ incessant incessantly incessantness incession -incest -incests -incestuous -incestuously -incestuousness incgrporate inch inchain @@ -204633,9 +204628,6 @@ nonincandescent nonincandescently nonincarnate nonincarnated -nonincestuous -nonincestuously -nonincestuousness nonincident nonincidental nonincidentally @@ -344408,8 +344400,6 @@ unincarnated unincensed uninceptive uninceptively -unincestuous -unincestuously uninchoative unincidental unincidentally @@ -370100,4 +370090,4 @@ zwinglianism zwinglianist zwitter zwitterion -zwitterionic \ No newline at end of file +zwitterionic diff --git a/app/templates/dashboard/api_key.html b/app/templates/dashboard/api_key.html index 50d499d..0412e2b 100644 --- a/app/templates/dashboard/api_key.html +++ b/app/templates/dashboard/api_key.html @@ -43,6 +43,7 @@