ocre-cli 0.2.0

Command-line tool for Ocre: create, generate, migrate, run and deploy apps.
//! Sign-up, the account page and account deletion (HTML). Generated by
//! `ocre g auth`.

use askama::Template;
use axum::{
    Form, Router,
    extract::State,
    http::{HeaderMap, StatusCode, Uri},
    response::{Html, IntoResponse, Redirect, Response},
    routing::{get, post},
};
use ocre::{Ctx, Error, FieldError, Flash, Result, Session, render};
use serde::Deserialize;

use crate::{
    auth::{CurrentUser, sign_in, sign_out},
    auth_api::throttle,
    confirmations::send_confirmation,
    models::user::{self, NewUser, User},
};

pub fn routes() -> Router<Ctx> {
    Router::new()
        .route("/signup", get(new).post(create))
        .route("/account", get(show))
        .route("/account/delete", post(destroy))
}

#[derive(Template)]
#[template(path = "auth/signup.html")]
struct SignupView {
    email: String,
    errors: Vec<FieldError>,
}

#[derive(Template)]
#[template(path = "auth/account.html")]
struct AccountView {
    flash: Flash,
    user: User,
}

/// The account deletion form: the password, or the email for users without one.
#[derive(Clone, Default, Deserialize)]
#[serde(default)]
pub struct DeleteForm {
    pub confirmation: String,
}

async fn new() -> Result<Html<String>> {
    render(&SignupView { email: String::new(), errors: vec![] })
}

async fn create(
    State(ctx): State<Ctx>,
    session: Session,
    headers: HeaderMap,
    uri: Uri,
    Form(new): Form<NewUser>,
) -> Result<Response> {
    throttle(&ctx, &headers, "signup").await?;
    let email = new.email.clone();
    match user::create(&ctx, new).await {
        Ok(user) => {
            send_confirmation(&ctx, &uri, &user).await?;
            let next = sign_in(&ctx, &session, &headers, &user, false).await?;
            session.flash("notice", "Welcome! Your account is ready. Check your email to confirm your address.")?;
            Ok(Redirect::to(&next).into_response())
        }
        Err(Error::Invalid(errors)) => {
            Ok((StatusCode::UNPROCESSABLE_ENTITY, render(&SignupView { email, errors })?).into_response())
        }
        Err(err) => Err(err),
    }
}

/// A protected page: `CurrentUser` redirects visitors to /login.
async fn show(CurrentUser(user): CurrentUser, flash: Flash) -> Result<Html<String>> {
    render(&AccountView { flash, user })
}

/// Deletes the account after checking the password (or the email typed
/// again), then signs out. Tokens, API keys and sessions are deleted with it.
async fn destroy(
    State(ctx): State<Ctx>,
    CurrentUser(user): CurrentUser,
    session: Session,
    headers: HeaderMap,
    Form(form): Form<DeleteForm>,
) -> Result<Redirect> {
    throttle(&ctx, &headers, "account_delete").await?;
    if !user::deletion_confirmed(&user, &form.confirmation).await? {
        let what = if user.has_password() { "password" } else { "email address" };
        session.flash("alert", format!("That is not your {what}: your account was not deleted."))?;
        return Ok(Redirect::to("/account"));
    }
    sign_out(&ctx, &session).await?;
    user::delete(&ctx, user.id).await?;
    session.flash("notice", "Your account has been deleted.")?;
    Ok(Redirect::to("/"))
}