# Buggy code
def make_blueprint(config):
    view = Blueprint("main", __name__)

    @view.route("/")
    def index():
        return render_template("index.html")

    @view.route("/generate", methods=("GET", "POST"))
    def generate():
        if logged_in():
            flash(
                gettext(
                    "You were redirected because you are already logged in. "
                    "If you want to create a new account, you should log out "
                    "first."
                ),
                "notification",
            )
            return redirect(url_for(".lookup"))

        codename = generate_unique_codename(config)

        # Generate a unique id for each browser tab and associate the codename with this id.
        # This will allow retrieval of the codename displayed in the tab from which the source has
        # clicked to proceed to /generate (ref. issue #4458)
        tab_id = urlsafe_b64encode(os.urandom(64)).decode()
        codenames = session.get("codenames", {})
        codenames[tab_id] = codename
        session["codenames"] = codenames

        session["new_user"] = True
        return render_template("generate.html", codename=codename, tab_id=tab_id)

    @view.route("/org-logo")
    def select_logo():
        if os.path.exists(
            os.path.join(current_app.static_folder, "i", "custom_logo.png")
        ):
            return redirect(url_for("static", filename="i/custom_logo.png"))
        else:
            return redirect(url_for("static", filename="i/logo.png"))

    @view.route("/create", methods=["POST"])
    def create():
        if session.get("logged_in", False):
            flash(
                gettext(
                    "You are already logged in. Please verify your codename above as it "
                    + "may differ from the one displayed on the previous page."
                ),
                "notification",
            )
        else:
            tab_id = request.form["tab_id"]
            codename = session["codenames"][tab_id]
            session["codename"] = codename

            del session["codenames"]

            filesystem_id = current_app.crypto_util.hash_codename(codename)
            try:
                source = Source(filesystem_id, current_app.crypto_util.display_id())
            except ValueError as e:
                current_app.logger.error(e)
                flash(
                    gettext(
                        "There was a temporary problem creating your account. "
                        "Please try again."
                    ),
                    "error",
                )
                return redirect(url_for(".index"))

            db.session.add(source)
            try:
                db.session.commit()
            except IntegrityError as e:
                db.session.rollback()
                current_app.logger.error(
                    "Attempt to create a source with duplicate codename: %s" % (e,)
                )

                # Issue 2386: don't log in on duplicates
                del session["codename"]

                # Issue 4361: Delete 'logged_in' if it's in the session
                try:
                    del session["logged_in"]
                except KeyError:
                    pass

                abort(500)
            else:
                os.mkdir(current_app.storage.path(filesystem_id))

            session["logged_in"] = True
        return redirect(url_for(".lookup"))

    @view.route("/lookup", methods=("GET",))
    @login_required
    def lookup():
        replies = []
        source_inbox = (
            Reply.query.filter(Reply.source_id == g.source.id)
            .filter(Reply.deleted_by_source == False)
            .all()
        )  # noqa

        for reply in source_inbox:
            reply_path = current_app.storage.path(
                g.filesystem_id,
                reply.filename,
            )
            try:
                with io.open(reply_path, "rb") as f:
                    contents = f.read()
                reply_obj = current_app.crypto_util.decrypt(g.codename, contents)
                reply.decrypted = reply_obj
            except UnicodeDecodeError:
                current_app.logger.error("Could not decode reply %s" % reply.filename)
            else:
                reply.date = datetime.utcfromtimestamp(os.stat(reply_path).st_mtime)
                replies.append(reply)

        # Sort the replies by date
        replies.sort(key=operator.attrgetter("date"), reverse=True)

        # Generate a keypair to encrypt replies from the journalist
        # Only do this if the journalist has flagged the source as one
        # that they would like to reply to. (Issue #140.)
        if (
            not current_app.crypto_util.get_fingerprint(g.filesystem_id)
            and g.source.flagged
        ):
            db_uri = current_app.config["SQLALCHEMY_DATABASE_URI"]
            async_genkey(current_app.crypto_util, db_uri, g.filesystem_id, g.codename)

        return render_template(
            "lookup.html",
            allow_document_uploads=current_app.instance_config.allow_document_uploads,
            codename=g.codename,
            replies=replies,
            flagged=g.source.flagged,
            new_user=session.get("new_user", None),
            haskey=current_app.crypto_util.get_fingerprint(g.filesystem_id),
            form=SubmissionForm(),
        )

    @view.route("/submit", methods=("POST",))
    @login_required
    def submit():
        allow_document_uploads = current_app.instance_config.allow_document_uploads
        form = SubmissionForm()
        if not form.validate():
            for field, errors in form.errors.items():
                for error in errors:
                    flash(error, "error")
            return redirect(url_for("main.lookup"))

        msg = request.form["msg"]
        fh = None
        if allow_document_uploads and "fh" in request.files:
            fh = request.files["fh"]

        # Don't submit anything if it was an "empty" submission. #878
        if not (msg or fh):
            if allow_document_uploads:
                flash(
                    gettext("You must enter a message or choose a file to submit."),
                    "error",
                )
            else:
                flash(gettext("You must enter a message."), "error")
            return redirect(url_for("main.lookup"))

        fnames = []
        journalist_filename = g.source.journalist_filename
        first_submission = g.source.interaction_count == 0

        if msg:
            g.source.interaction_count += 1
            fnames.append(
                current_app.storage.save_message_submission(
                    g.filesystem_id,
                    g.source.interaction_count,
                    journalist_filename,
                    msg,
                )
            )
        if fh:
            g.source.interaction_count += 1
            fnames.append(
                current_app.storage.save_file_submission(
                    g.filesystem_id,
                    g.source.interaction_count,
                    journalist_filename,
                    fh.filename,
                    fh.stream,
                )
            )

        if first_submission:
            flash_message = render_template("first_submission_flashed_message.html")
            flash(Markup(flash_message), "success")

        else:
            if msg and not fh:
                html_contents = gettext("Thanks! We received your message.")
            elif fh and not msg:
                html_contents = gettext("Thanks! We received your document.")
            else:
                html_contents = gettext(
                    "Thanks! We received your message and document."
                )

            flash_message = render_template(
                "next_submission_flashed_message.html", html_contents=html_contents
            )
            flash(Markup(flash_message), "success")

        new_submissions = []
        for fname in fnames:
            submission = Submission(g.source, fname)
            db.session.add(submission)
            new_submissions.append(submission)

        if g.source.pending:
            g.source.pending = False

            # Generate a keypair now, if there's enough entropy (issue #303)
            # (gpg reads 300 bytes from /dev/random)
            entropy_avail = get_entropy_estimate()
            if entropy_avail >= 2400:
                db_uri = current_app.config["SQLALCHEMY_DATABASE_URI"]

                async_genkey(
                    current_app.crypto_util, db_uri, g.filesystem_id, g.codename
                )
                current_app.logger.info(
                    "generating key, entropy: {}".format(entropy_avail)
                )
            else:
                current_app.logger.warn(
                    "skipping key generation. entropy: {}".format(entropy_avail)
                )

        g.source.last_updated = datetime.utcnow()
        db.session.commit()

        for sub in new_submissions:
            store.async_add_checksum_for_file(sub)

        normalize_timestamps(g.filesystem_id)

        return redirect(url_for("main.lookup"))

    @view.route("/delete", methods=("POST",))
    @login_required
    def delete():
        """This deletes the reply from the source's inbox, but preserves
        the history for journalists such that they can view conversation
        history.
        """

        query = Reply.query.filter_by(
            filename=request.form["reply_filename"], source_id=g.source.id
        )
        reply = get_one_or_else(query, current_app.logger, abort)
        reply.deleted_by_source = True
        db.session.add(reply)
        db.session.commit()

        flash(gettext("Reply deleted"), "notification")
        return redirect(url_for(".lookup"))

    @view.route("/delete-all", methods=("POST",))
    @login_required
    def batch_delete():
        replies = (
            Reply.query.filter(Reply.source_id == g.source.id)
            .filter(Reply.deleted_by_source == False)
            .all()
        )  # noqa
        if len(replies) == 0:
            current_app.logger.error("Found no replies when at least one was expected")
            return redirect(url_for(".lookup"))

        for reply in replies:
            reply.deleted_by_source = True
            db.session.add(reply)
        db.session.commit()

        flash(gettext("All replies have been deleted"), "notification")
        return redirect(url_for(".lookup"))

    @view.route("/login", methods=("GET", "POST"))
    def login():
        form = LoginForm()
        if form.validate_on_submit():
            codename = request.form["codename"].strip()
            if valid_codename(codename):
                session.update(codename=codename, logged_in=True)
                return redirect(url_for(".lookup", from_login="1"))
            else:
                current_app.logger.info("Login failed for invalid codename")
                flash(gettext("Sorry, that is not a recognized codename."), "error")
        return render_template("login.html", form=form)

    @view.route("/logout")
    def logout():
        """
        If a user is logged in, show them a logout page that prompts them to
        click the New Identity button in Tor Browser to complete their session.
        Otherwise redirect to the main Source Interface page.
        """
        if logged_in():
            # Clear the session after we render the message so it's localized
            # If a user specified a locale, save it and restore it
            user_locale = g.locale
            session.clear()
            session["locale"] = user_locale

            return render_template("logout.html")
        else:
            return redirect(url_for(".index"))

    return view


# Traceback (FileNotFoundError)
[Wed Jul 22 22:10:08.442888 2020] [wsgi:error] [pid 11154:tid 115647289255680] [remote 127.0.0.1:40108] [2020-07-22 22:10:08,442] ERROR in app: Exception on /lookup [GET]
[Wed Jul 22 22:10:08.442937 2020] [wsgi:error] [pid 11154:tid 115647289255680] [remote 127.0.0.1:40108] Traceback (most recent call last):
[Wed Jul 22 22:10:08.442946 2020] [wsgi:error] [pid 11154:tid 115647289255680] [remote 127.0.0.1:40108]   File "/opt/venvs/securedrop-app-code/lib/python3.5/site-packages/flask/app.py", line 2292, in wsgi_app
[Wed Jul 22 22:10:08.442953 2020] [wsgi:error] [pid 11154:tid 115647289255680] [remote 127.0.0.1:40108]     response = self.full_dispatch_request()
[Wed Jul 22 22:10:08.442959 2020] [wsgi:error] [pid 11154:tid 115647289255680] [remote 127.0.0.1:40108]   File "/opt/venvs/securedrop-app-code/lib/python3.5/site-packages/flask/app.py", line 1815, in full_dispatch_request
[Wed Jul 22 22:10:08.442966 2020] [wsgi:error] [pid 11154:tid 115647289255680] [remote 127.0.0.1:40108]     rv = self.handle_user_exception(e)
[Wed Jul 22 22:10:08.442972 2020] [wsgi:error] [pid 11154:tid 115647289255680] [remote 127.0.0.1:40108]   File "/opt/venvs/securedrop-app-code/lib/python3.5/site-packages/flask/app.py", line 1718, in handle_user_exception
[Wed Jul 22 22:10:08.442979 2020] [wsgi:error] [pid 11154:tid 115647289255680] [remote 127.0.0.1:40108]     reraise(exc_type, exc_value, tb)
[Wed Jul 22 22:10:08.442985 2020] [wsgi:error] [pid 11154:tid 115647289255680] [remote 127.0.0.1:40108]   File "/opt/venvs/securedrop-app-code/lib/python3.5/site-packages/flask/_compat.py", line 35, in reraise
[Wed Jul 22 22:10:08.442992 2020] [wsgi:error] [pid 11154:tid 115647289255680] [remote 127.0.0.1:40108]     raise value
[Wed Jul 22 22:10:08.442998 2020] [wsgi:error] [pid 11154:tid 115647289255680] [remote 127.0.0.1:40108]   File "/opt/venvs/securedrop-app-code/lib/python3.5/site-packages/flask/app.py", line 1813, in full_dispatch_request
[Wed Jul 22 22:10:08.443005 2020] [wsgi:error] [pid 11154:tid 115647289255680] [remote 127.0.0.1:40108]     rv = self.dispatch_request()
[Wed Jul 22 22:10:08.443011 2020] [wsgi:error] [pid 11154:tid 115647289255680] [remote 127.0.0.1:40108]   File "/opt/venvs/securedrop-app-code/lib/python3.5/site-packages/flask/app.py", line 1799, in dispatch_request
[Wed Jul 22 22:10:08.443017 2020] [wsgi:error] [pid 11154:tid 115647289255680] [remote 127.0.0.1:40108]     return self.view_functions[rule.endpoint](**req.view_args)
[Wed Jul 22 22:10:08.443023 2020] [wsgi:error] [pid 11154:tid 115647289255680] [remote 127.0.0.1:40108]   File "/var/www/securedrop/source_app/decorators.py", line 12, in decorated_function
[Wed Jul 22 22:10:08.443029 2020] [wsgi:error] [pid 11154:tid 115647289255680] [remote 127.0.0.1:40108]     return f(*args, **kwargs)
[Wed Jul 22 22:10:08.443060 2020] [wsgi:error] [pid 11154:tid 115647289255680] [remote 127.0.0.1:40108]   File "/var/www/securedrop/source_app/main.py", line 115, in lookup
[Wed Jul 22 22:10:08.443067 2020] [wsgi:error] [pid 11154:tid 115647289255680] [remote 127.0.0.1:40108]     with io.open(reply_path, "rb") as f:
[Wed Jul 22 22:10:08.443076 2020] [wsgi:error] [pid 11154:tid 115647289255680] [remote 127.0.0.1:40108] FileNotFoundError: [Errno 2] No such file or directory: '/var/lib/securedrop/store/YJSI6RQBP5JTHYYMZ4L4MUTQWAI7MNTK5ZUU4K2OXSD34PALIFT7DK6LTAYT43VLGAEOPWBBNV6JWDTHQPFITD6UFPLNYN25RHMJJOY=/2-damp_burner-reply.gpg'
[Wed Jul 22 22:10:08.443111 2020] [wsgi:error] [pid 11154:tid 115647289255680] [remote 127.0.0.1:40108]

# Fixed code
def make_blueprint(config):
    view = Blueprint("main", __name__)

    @view.route("/")
    def index():
        return render_template("index.html")

    @view.route("/generate", methods=("GET", "POST"))
    def generate():
        if logged_in():
            flash(
                gettext(
                    "You were redirected because you are already logged in. "
                    "If you want to create a new account, you should log out "
                    "first."
                ),
                "notification",
            )
            return redirect(url_for(".lookup"))

        codename = generate_unique_codename(config)

        # Generate a unique id for each browser tab and associate the codename with this id.
        # This will allow retrieval of the codename displayed in the tab from which the source has
        # clicked to proceed to /generate (ref. issue #4458)
        tab_id = urlsafe_b64encode(os.urandom(64)).decode()
        codenames = session.get("codenames", {})
        codenames[tab_id] = codename
        session["codenames"] = codenames

        session["new_user"] = True
        return render_template("generate.html", codename=codename, tab_id=tab_id)

    @view.route("/org-logo")
    def select_logo():
        if os.path.exists(
            os.path.join(current_app.static_folder, "i", "custom_logo.png")
        ):
            return redirect(url_for("static", filename="i/custom_logo.png"))
        else:
            return redirect(url_for("static", filename="i/logo.png"))

    @view.route("/create", methods=["POST"])
    def create():
        if session.get("logged_in", False):
            flash(
                gettext(
                    "You are already logged in. Please verify your codename above as it "
                    + "may differ from the one displayed on the previous page."
                ),
                "notification",
            )
        else:
            tab_id = request.form["tab_id"]
            codename = session["codenames"][tab_id]
            session["codename"] = codename

            del session["codenames"]

            filesystem_id = current_app.crypto_util.hash_codename(codename)
            try:
                source = Source(filesystem_id, current_app.crypto_util.display_id())
            except ValueError as e:
                current_app.logger.error(e)
                flash(
                    gettext(
                        "There was a temporary problem creating your account. "
                        "Please try again."
                    ),
                    "error",
                )
                return redirect(url_for(".index"))

            db.session.add(source)
            try:
                db.session.commit()
            except IntegrityError as e:
                db.session.rollback()
                current_app.logger.error(
                    "Attempt to create a source with duplicate codename: %s" % (e,)
                )

                # Issue 2386: don't log in on duplicates
                del session["codename"]

                # Issue 4361: Delete 'logged_in' if it's in the session
                try:
                    del session["logged_in"]
                except KeyError:
                    pass

                abort(500)
            else:
                os.mkdir(current_app.storage.path(filesystem_id))

            session["logged_in"] = True
        return redirect(url_for(".lookup"))

    @view.route("/lookup", methods=("GET",))
    @login_required
    def lookup():
        replies = []
        source_inbox = (
            Reply.query.filter(Reply.source_id == g.source.id)
            .filter(Reply.deleted_by_source == False)
            .all()
        )  # noqa

        for reply in source_inbox:
            reply_path = current_app.storage.path(
                g.filesystem_id,
                reply.filename,
            )
            try:
                with io.open(reply_path, "rb") as f:
                    contents = f.read()
                reply_obj = current_app.crypto_util.decrypt(g.codename, contents)
                reply.decrypted = reply_obj
            except UnicodeDecodeError:
                current_app.logger.error("Could not decode reply %s" % reply.filename)
            except FileNotFoundError:
                current_app.logger.error("Reply file missing: %s" % reply.filename)
            else:
                reply.date = datetime.utcfromtimestamp(os.stat(reply_path).st_mtime)
                replies.append(reply)

        # Sort the replies by date
        replies.sort(key=operator.attrgetter("date"), reverse=True)

        # Generate a keypair to encrypt replies from the journalist
        # Only do this if the journalist has flagged the source as one
        # that they would like to reply to. (Issue #140.)
        if (
            not current_app.crypto_util.get_fingerprint(g.filesystem_id)
            and g.source.flagged
        ):
            db_uri = current_app.config["SQLALCHEMY_DATABASE_URI"]
            async_genkey(current_app.crypto_util, db_uri, g.filesystem_id, g.codename)

        return render_template(
            "lookup.html",
            allow_document_uploads=current_app.instance_config.allow_document_uploads,
            codename=g.codename,
            replies=replies,
            flagged=g.source.flagged,
            new_user=session.get("new_user", None),
            haskey=current_app.crypto_util.get_fingerprint(g.filesystem_id),
            form=SubmissionForm(),
        )

    @view.route("/submit", methods=("POST",))
    @login_required
    def submit():
        allow_document_uploads = current_app.instance_config.allow_document_uploads
        form = SubmissionForm()
        if not form.validate():
            for field, errors in form.errors.items():
                for error in errors:
                    flash(error, "error")
            return redirect(url_for("main.lookup"))

        msg = request.form["msg"]
        fh = None
        if allow_document_uploads and "fh" in request.files:
            fh = request.files["fh"]

        # Don't submit anything if it was an "empty" submission. #878
        if not (msg or fh):
            if allow_document_uploads:
                flash(
                    gettext("You must enter a message or choose a file to submit."),
                    "error",
                )
            else:
                flash(gettext("You must enter a message."), "error")
            return redirect(url_for("main.lookup"))

        fnames = []
        journalist_filename = g.source.journalist_filename
        first_submission = g.source.interaction_count == 0

        if msg:
            g.source.interaction_count += 1
            fnames.append(
                current_app.storage.save_message_submission(
                    g.filesystem_id,
                    g.source.interaction_count,
                    journalist_filename,
                    msg,
                )
            )
        if fh:
            g.source.interaction_count += 1
            fnames.append(
                current_app.storage.save_file_submission(
                    g.filesystem_id,
                    g.source.interaction_count,
                    journalist_filename,
                    fh.filename,
                    fh.stream,
                )
            )

        if first_submission:
            flash_message = render_template("first_submission_flashed_message.html")
            flash(Markup(flash_message), "success")

        else:
            if msg and not fh:
                html_contents = gettext("Thanks! We received your message.")
            elif fh and not msg:
                html_contents = gettext("Thanks! We received your document.")
            else:
                html_contents = gettext(
                    "Thanks! We received your message and document."
                )

            flash_message = render_template(
                "next_submission_flashed_message.html", html_contents=html_contents
            )
            flash(Markup(flash_message), "success")

        new_submissions = []
        for fname in fnames:
            submission = Submission(g.source, fname)
            db.session.add(submission)
            new_submissions.append(submission)

        if g.source.pending:
            g.source.pending = False

            # Generate a keypair now, if there's enough entropy (issue #303)
            # (gpg reads 300 bytes from /dev/random)
            entropy_avail = get_entropy_estimate()
            if entropy_avail >= 2400:
                db_uri = current_app.config["SQLALCHEMY_DATABASE_URI"]

                async_genkey(
                    current_app.crypto_util, db_uri, g.filesystem_id, g.codename
                )
                current_app.logger.info(
                    "generating key, entropy: {}".format(entropy_avail)
                )
            else:
                current_app.logger.warn(
                    "skipping key generation. entropy: {}".format(entropy_avail)
                )

        g.source.last_updated = datetime.utcnow()
        db.session.commit()

        for sub in new_submissions:
            store.async_add_checksum_for_file(sub)

        normalize_timestamps(g.filesystem_id)

        return redirect(url_for("main.lookup"))

    @view.route("/delete", methods=("POST",))
    @login_required
    def delete():
        """This deletes the reply from the source's inbox, but preserves
        the history for journalists such that they can view conversation
        history.
        """

        query = Reply.query.filter_by(
            filename=request.form["reply_filename"], source_id=g.source.id
        )
        reply = get_one_or_else(query, current_app.logger, abort)
        reply.deleted_by_source = True
        db.session.add(reply)
        db.session.commit()

        flash(gettext("Reply deleted"), "notification")
        return redirect(url_for(".lookup"))

    @view.route("/delete-all", methods=("POST",))
    @login_required
    def batch_delete():
        replies = (
            Reply.query.filter(Reply.source_id == g.source.id)
            .filter(Reply.deleted_by_source == False)
            .all()
        )  # noqa
        if len(replies) == 0:
            current_app.logger.error("Found no replies when at least one was expected")
            return redirect(url_for(".lookup"))

        for reply in replies:
            reply.deleted_by_source = True
            db.session.add(reply)
        db.session.commit()

        flash(gettext("All replies have been deleted"), "notification")
        return redirect(url_for(".lookup"))

    @view.route("/login", methods=("GET", "POST"))
    def login():
        form = LoginForm()
        if form.validate_on_submit():
            codename = request.form["codename"].strip()
            if valid_codename(codename):
                session.update(codename=codename, logged_in=True)
                return redirect(url_for(".lookup", from_login="1"))
            else:
                current_app.logger.info("Login failed for invalid codename")
                flash(gettext("Sorry, that is not a recognized codename."), "error")
        return render_template("login.html", form=form)

    @view.route("/logout")
    def logout():
        """
        If a user is logged in, show them a logout page that prompts them to
        click the New Identity button in Tor Browser to complete their session.
        Otherwise redirect to the main Source Interface page.
        """
        if logged_in():
            # Clear the session after we render the message so it's localized
            # If a user specified a locale, save it and restore it
            user_locale = g.locale
            session.clear()
            session["locale"] = user_locale

            return render_template("logout.html")
        else:
            return redirect(url_for(".index"))

    return view