Flask Full-Stack

File Uploads, Email, Background Work and Payments

40 min

Safe file uploads

import os, uuid
from werkzeug.utils import secure_filename

ALLOWED = {"png", "jpg", "jpeg", "webp", "pdf"}
app.config["MAX_CONTENT_LENGTH"] = 10 * 1024 * 1024        # 10 MB, enforced by Flask

def save_upload(file, subdir):
    ext = file.filename.rsplit(".", 1)[-1].lower() if "." in file.filename else ""
    if ext not in ALLOWED:
        raise ValueError("File type not allowed")
    name = f"{uuid.uuid4().hex}.{ext}"                       # never trust the original name
    rel = os.path.join(subdir, name)
    file.save(os.path.join(app.config["UPLOAD_ROOT"], rel))
    return rel                                               # store the relative path
  • Store files outside static/ and serve them through a route that checks permissions.
  • Check content, not just the extension (for images, open them with Pillow).
  • On platforms with ephemeral disks, use object storage (S3, R2) instead.

Transactional email

Use a provider API (Resend, Postmark, SendGrid) rather than raw SMTP:

import resend
resend.api_key = os.environ["RESEND_API_KEY"]
resend.Emails.send({
    "from": "PandaDev <hello@mail.pandadevhub.com>",
    "to": [user.email],
    "subject": "Verify your email",
    "html": render_template("emails/verify.html", link=link),
})

Email tokens (verification, password reset) must be random, single-use, hashed in the database and time-limited. The Email Setup track covers DNS (SPF, DKIM, DMARC).

Work outside the request

A request should finish in well under a few seconds. Send email, resize images and call slow APIs in the background:

  • Simple: a thread or concurrent.futures for fire-and-forget tasks (lost on restart).
  • Robust: a task queue (RQ or Celery with Redis), or the platform's background workers and cron jobs.

Payments with Stripe Checkout

import stripe
stripe.api_key = os.environ["STRIPE_SECRET_KEY"]

@shop_bp.post("/checkout/<int:product_id>")
@login_required
def checkout(product_id):
    product = db.get_or_404(Product, product_id)
    session = stripe.checkout.Session.create(
        mode="payment",
        line_items=[{"price_data": {"currency": "usd", "unit_amount": product.price_cents,
                                    "product_data": {"name": product.name}}, "quantity": 1}],
        success_url=url_for("shop.success", _external=True) + "?session_id={CHECKOUT_SESSION_ID}",
        cancel_url=url_for("shop.product", product_id=product.id, _external=True),
        metadata={"user_id": current_user.id, "product_id": product.id},
    )
    return redirect(session.url, code=303)

Grant access in the webhook (checkout.session.completed), verified with the signing secret, not on the success page, which users can open without paying. The Stripe Payments Integration track covers this fully.

Exercise

Add avatar uploads (images only, 2 MB max, random names, served through a permission-checked route) and a "verify your email" flow with a hashed, expiring token.

Check yourself

  • Why rename uploaded files?
  • Why should slow work leave the request cycle?
  • Where should paid access be granted, and why?

Your progress is saved in this browser only.

Submit for review (10 points)