Forms need two things
- CSRF protection — prevents attackers from submitting forms on behalf of your users
- 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.