Beta documentation. This is an early preview — content is still in active development. Feedback helps shape the final release. Share your thoughts or join the discussion.

Forms & CSRF Protection

1. **CSRF protection** — prevents attackers from submitting forms on behalf of your users

On this page

Forms need two things

  1. CSRF protection — prevents attackers from submitting forms on behalf of your users
  2. User feedback — show success/error messages after submission

Wapka gives you both out of the box.

CSRF in templates

Every <form> that uses POST, PUT, or DELETE must include a CSRF token:

<form method="post" action="/profile">
    <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
    <input name="username" value="{{ user.username }}">
    <button type="submit">Save</button>
</form>

The csrf_token() function generates a token AND sets a cookie. The "csrf" middleware validates them match. If they don't, the request is rejected with 403.

CSRF protection flow

local app = framework()
app:use("csrf")    -- enable globally

-- This form is protected automatically
app:post("/profile", updateProfile)

-- Webhooks and APIs can opt out
app:post("/webhook/stripe", stripeHandler, { csrf = false })
app:post("/api/data", apiHandler, { csrf = false })

Flash messages

Flash messages survive exactly one redirect. Set them before redirecting, display them on the next page:

app:post("/contact", function(ctx)
    local v = validator(req.post, {
        name = "required|string|min:2",
        email = "required|email",
        message = "required|string|min:10"
    })

    if v:fails() then
        ctx:flash("error", v:first())          -- survives the redirect
        ctx:flash("old_name", req.post.name)   -- preserve form input
        ctx:flash("old_email", req.post.email)
        return ctx:redirect("/contact")
    end

    api.messages.send("admin", "New contact from " .. v:valid().email)
    ctx:flash("success", "Thanks! We'll reply soon.")
    return ctx:redirect("/contact")
end)

Displaying flash in templates

flash() and has_flash() are available as both Lua ctx methods (ctx:flash(), ctx:has_flash()) and Twig template globals — use them directly in templates without a prefix:

{% if has_flash("success") %}
    <div class="alert alert-success">{{ flash("success") }}</div>
{% endif %}

{% if has_flash("error") %}
    <div class="alert alert-error">{{ flash("error") }}</div>
{% endif %}

has_flash(key) checks without consuming — flash(key) gets AND consumes the message (it won't appear on the next request).

Preserving form values

Nothing worse than losing your form input after an error:

-- In the handler, save old values as flash
ctx:flash("old_name", req.post.name)
ctx:flash("old_email", req.post.email)
ctx:flash("errors", v:errors())

-- In the template, restore them
<input name="name" value="{{ flash("old_name")|e }}">
<input name="email" value="{{ flash("old_email")|e }}">

{% set errors = flash("errors") %}
{% if errors.name %}
    <span class="error">{{ errors.name }}</span>
{% endif %}

Complete contact form

Lua handler

app:get("/contact", function(ctx)
    return ctx:render("contact", {
        page_title = "Contact Us"
    })
end)

app:post("/contact", function(ctx)
    local v = validator(req.post, {
        name = "required|string|min:2",
        email = "required|email",
        subject = "required|string|min:3",
        message = "required|string|min:10"
    })

    if v:fails() then
        ctx:flash("error", v:first())
        ctx:flash("errors", v:errors())
        ctx:flash("old", req.post)
        return ctx:redirect("/contact")
    end

    local data = v:valid()
    api.messages.send("admin",
        string.format("New contact from %s (%s)\n\n%s", data.name, data.email, data.message)
    )
    log.info("Contact form submitted by " .. data.email)
    ctx:flash("success", "Message sent! We'll get back to you within 24 hours.")
    return ctx:redirect("/contact")
end)

Twig template (contact page, type=0)

{% extends "layout" %}
{% block content %}
    <h1>{{ page_title }}</h1>

    {% if has_flash("success") %}
        <div class="alert success">{{ flash("success") }}</div>
    {% endif %}

    {% if has_flash("error") %}
        <div class="alert error">{{ flash("error") }}</div>
    {% endif %}

    {% set old = flash("old") %}
    <form method="post">
        <input type="hidden" name="csrf_token" value="{{ csrf_token() }}">

        <input name="name" placeholder="Your name"
               value="{{ old.name|e }}" required>
        <input name="email" type="email" placeholder="Email"
               value="{{ old.email|e }}" required>
        <input name="subject" placeholder="Subject"
               value="{{ old.subject|e }}" required>
        <textarea name="message" placeholder="Your message" required>{{ old.message|e }}</textarea>

        <button type="submit">Send Message</button>
    </form>
{% endblock %}

Next: Explore the API Library to work with users, posts, files, and data.

Previous Inheritance & Includes