Practical Examples
Documentation / Practical Examples / Forms: sign up, sign in, search

Forms: sign up, sign in, search

The examples below follow the code of the project runique new creates (compiled on every CI run). See Forms for the full API.

Manual form: sign in

A manual form declares its fields by hand. Use one when it maps to no table (sign in, search, contact).

// src/forms/login.rs
use runique::prelude::*;
use serde::Serialize;

#[derive(Serialize, Debug, Clone)]
#[serde(transparent)]
pub struct LoginForm {
    pub form: Forms,
}

impl FormAccess for LoginForm {
    fn register_fields(form: &mut Forms) {
        form.field(&TextField::text("username").label("Username").required());
        form.field(&TextField::password("password").label("Password").required());
    }
    impl_form_access!();
}

impl RuniqueForm for LoginForm {}

Serialize (transparent) is needed to hand the form to the template. Don't put such forms in src/entities/: runique makemigrations looks for models there.

Sign-in handler

pub async fn sign_in(mut request: Request) -> AppResult<Response> {
    let form: LoginForm = request.form("login");
    if request.user.is_some() {
        return Ok(Redirect::to("/").into_response());
    }

    let template = "login.html";
    let validated = match ValidationForm::try_form(form, &request).await {
        Ok(validated) => validated,
        Err(form) => {
            context_update!(request => {
                "title" => "Sign in",
                "login_form" => &form,
            });
            return request.render(template);
        }
    };

    let username = validated.cleaned::<String>("username").unwrap_or_default();
    let password = validated.cleaned::<String>("password").unwrap_or_default();
    // Checks the password even for an unknown username (same timing either
    // way) and refuses an inactive or never activated account.
    if let Some(user) = authenticate_user(&request.engine.db, &username, &password).await
        && login(&request.session, &user, None, false).await.is_ok()
    {
        success!(request.notices => format!("Welcome back {}!", user.username));
        return Ok(Redirect::to("/").into_response());
    }

    context_update!(request => {
        "title" => "Sign in",
        "login_form" => &*validated,
        "messages" => flash_now!(error => "Invalid username or password."),
    });
    request.render(template)
}

request.form("login") names the form, and ValidationForm::try_form(form, &request) validates it only when the request sends that name back: a GET, or another form of the same page being sent, comes back as Err(form) with no field error. The CSRF token is checked when the form is read.


Model-based form: sign up

#[form(schema = …, fields = […])] generates the struct and FormAccess from the model; you only write RuniqueForm, with its hooks.

// src/forms/signup.rs
use runique::prelude::*;

#[form(schema = runique_users, fields = [username, email, password])]
pub struct SignupForm;

#[async_trait]
impl RuniqueForm for SignupForm {
    async fn clean(&mut self) -> Result<(), StrMap> {
        let username = self.cleaned::<String>("username").unwrap_or_default();
        let mut errors = StrMap::new();
        if username.len() < 5 {
            errors.insert("username".to_string(), "At least 5 characters.".to_string());
        }
        if errors.is_empty() { Ok(()) } else { Err(errors) }
    }
}

#[async_trait] is only needed when you override an async hook (clean, clean_field); with no hook, impl RuniqueForm for SignupForm {} is enough.

Saving an account

validated.save(&db) saves the row, password included (its hash). But an account is born inactive: it can't sign in before being activated. The generated project therefore writes its own save, which creates the account then activates it at once:

impl SignupForm {
    pub async fn save(&self, db: &ADb) -> Result<runique_users::Model, DbErr> {
        use runique::prelude::runique_users::ActiveModel;
        let now = chrono::Utc::now().naive_utc();
        let user = ActiveModel {
            username: Set(self.cleaned::<String>("username").unwrap_or_default()),
            email: Set(self.cleaned::<String>("email").unwrap_or_default()),
            // After is_valid(), the password field already holds the hash.
            password: Set(self.cleaned::<String>("password").unwrap_or_default()),
            is_active: Set(false),
            is_superuser: Set(false),
            is_dirigeant: Set(false),
            is_admin: Set(false),
            created_at: Set(Some(now)),
            updated_at: Set(Some(now)),
            ..Default::default()
        }
        .insert(db)
        .await?;

        // An account is created inactive. Activating it is a business
        // decision: here at once; to confirm the email first, send a link and
        // call `activate_account` when it's opened.
        BuiltinUserEntity::activate_account(db, user.id)
            .await?
            .ok_or_else(|| DbErr::Custom("account already activated".into()))
    }
}

Sign-up handler

pub async fn sign_up(mut request: Request) -> AppResult<Response> {
    let form: SignupForm = request.form("signup");
    if request.user.is_some() {
        return Ok(Redirect::to("/").into_response());
    }

    let template = "signup.html";
    let mut form = match ValidationForm::try_form(form, &request).await {
        Ok(validated) => validated.into_form(),
        Err(form) => {
            context_update!(request => {
                "title" => "Sign up",
                "signup_form" => &form,
            });
            return request.render(template);
        }
    };

    match form.save(&request.engine.db).await {
        Ok(user) => {
            if login(&request.session, &user, None, false).await.is_ok() {
                success!(request.notices => format!("Welcome {}!", user.username));
            }
            Ok(Redirect::to("/").into_response())
        }
        Err(err) => {
            // A username or email already taken shows on its field.
            form.forms_mut().database_error(&err);
            context_update!(request => {
                "title" => "Sign up",
                "signup_form" => &form,
                "messages" => flash_now!(error => "Signing up failed."),
            });
            request.render(template)
        }
    }
}

Template

{% extends "base.html" %}

{% block content %}
  <h1>{{ title }}</h1>
  {% messages %}
  <form method="post" action='{% link "signup" %}'>
    {% form.signup_form %}
    <button type="submit">Sign up</button>
  </form>
{% endblock %}

{% form.signup_form %} writes the fields, their errors, the CSRF token and the form's name.


Sign out

A POST, never a GET. The view reads no form, so it checks the CSRF token itself:

pub async fn sign_out(request: Request) -> AppResult<Response> {
    if !request.prisme.csrf_valid {
        return Ok(StatusCode::FORBIDDEN.into_response());
    }
    logout(&request.session, None).await?;
    Ok(Redirect::to("/").into_response())
}
// url.rs
"/logout" => post(sign_out), name = "logout",
<form method="post" action='{% link "logout" %}'>
  {% csrf %}
  <button type="submit">Sign out</button>
</form>

Search form (GET)

By default a form never validates on a GET: GETs carry no CSRF token, so a forged link could otherwise trigger its effects. A search form, which only reads, allows it explicitly:

#[derive(Serialize, Debug, Clone)]
#[serde(transparent)]
pub struct SearchForm {
    pub form: Forms,
}

impl FormAccess for SearchForm {
    fn register_fields(form: &mut Forms) {
        form.field(&TextField::text("username").label("User").required());
    }
    impl_form_access!();
}

impl RuniqueForm for SearchForm {
    fn allow_get(&self, _request: &Request) -> bool {
        self.is_submitted()
    }
}
pub async fn search_user(mut request: Request) -> AppResult<Response> {
    let form: SearchForm = request.form("search");
    let template = "search.html";

    let form = match ValidationForm::try_form(form, &request).await {
        Ok(validated) => {
            let username = validated.cleaned::<String>("username").unwrap_or_default();
            let found = runique_users::Entity::find()
                .filter(runique_users::Column::Username.eq(&username))
                .one(&*request.engine.db)
                .await?;
            context_update!(request => { "found_user" => &found.map(|u| u.username) });
            validated.into_form()
        }
        Err(form) => form,
    };

    context_update!(request => { "title" => "Search", "search_form" => &form });
    request.render(template)
}
<form method="get">
  {% form.search_form %}
  <button type="submit">Search</button>
</form>
{% if found_user %}<p>Found: {{ found_user }}</p>{% endif %}

Never hand a whole account model to the template: it holds the password hash. Keep only the columns to show.