Login
4 branches 0 tags
Ben (Desktop/Arch) Minor styling improvements ac29844 1 month ago 63 Commits
rubhub / src / pages / user.rs
use axum::{
    Form,
    extract::State,
    http::StatusCode,
    response::{Html, IntoResponse, Redirect, Response},
};
use sea_orm::{
    ActiveModelTrait, ColumnTrait, Condition, EntityTrait, QueryFilter, Set, TransactionTrait,
};
use serde::Deserialize;
use tower_cookies::Cookies;

use crate::{
    app,
    entities::{ssh_key, user},
    services::{
        session as session_service,
        user::replace_ssh_keys,
        validation::{validate_uri, validate_username},
    },
    state::GlobalState,
};

#[derive(Debug, Deserialize)]
pub struct SettingsForm {
    pub slug: String,
    pub name: String,
    pub email: String,
    pub pronouns: String,
    pub organization: String,
    pub location: String,
    pub website: String,
    pub description: String,
    pub default_main_branch: String,
    pub ssh_keys: Option<String>,
}

pub async fn settings_page(
    State(state): State<GlobalState>,
    cookies: Cookies,
) -> Result<Html<String>, Redirect> {
    let current_user = match session_service::current_user(&state, &cookies).await {
        Ok(user) => user,
        Err(_) => return Err(Redirect::to("/login")),
    };

    let keys = ssh_key::Entity::find()
        .filter(ssh_key::Column::UserId.eq(current_user.id))
        .all(&state.db)
        .await
        .unwrap_or_default();

    let ssh_keys: Vec<String> = keys
        .into_iter()
        .map(|k| {
            if k.hostname.is_empty() {
                k.public_key
            } else {
                format!("{} {}", k.public_key, k.hostname)
            }
        })
        .collect();

    Ok(render_settings_page(&cookies, current_user, &ssh_keys, None).await)
}

async fn internal_error(
    cookies: &tower_cookies::Cookies,
    user: user::Model,
    ssh_keys: &[String],
    err: &str,
) -> (axum::http::StatusCode, Html<String>) {
    eprintln!("auth error: {err}");
    (
        axum::http::StatusCode::INTERNAL_SERVER_ERROR,
        render_settings_page(cookies, user, ssh_keys, Some(err)).await,
    )
}

pub async fn handle_settings(
    State(state): State<GlobalState>,
    cookies: Cookies,
    Form(form): Form<SettingsForm>,
) -> Result<Response, (axum::http::StatusCode, Html<String>)> {
    let current_user = match session_service::current_user(&state, &cookies).await {
        Ok(user) => user,
        Err(_) => return Ok(Redirect::to("/login").into_response()),
    };

    let name = form.name.trim();
    let slug = form.slug.trim();
    let default_main_branch = form.slug.trim();
    let email = form.email.trim().to_owned();
    let website = form.website.trim();
    let ssh_keys: Vec<String> = form
        .ssh_keys
        .unwrap_or_default()
        .lines()
        .map(str::trim)
        .filter(|line| !line.is_empty())
        .map(ToOwned::to_owned)
        .collect();

    if slug.is_empty() {
        return Err((
            StatusCode::BAD_REQUEST,
            render_settings_page(
                &cookies,
                current_user,
                &ssh_keys,
                Some("Username is required."),
            )
            .await,
        ));
    }

    if default_main_branch.is_empty() {
        return Err((
            StatusCode::BAD_REQUEST,
            render_settings_page(
                &cookies,
                current_user,
                &ssh_keys,
                Some("Default main branch is required"),
            )
            .await,
        ));
    }

    if let Err(msg) = validate_username(slug) {
        return Err((
            StatusCode::BAD_REQUEST,
            render_settings_page(&cookies, current_user, &ssh_keys, Some(msg)).await,
        ));
    }

    if !website.is_empty()
        && let Err(msg) = validate_uri(website)
    {
        return Err((
            StatusCode::BAD_REQUEST,
            render_settings_page(&cookies, current_user, &ssh_keys, Some(msg)).await,
        ));
    }

    if let Ok(Some(_)) = user::Entity::find()
        .filter(
            Condition::all()
                .add(user::Column::Slug.eq(slug))
                .add(user::Column::Id.ne(current_user.id)),
        )
        .one(&state.db)
        .await
    {
        return Err((
            StatusCode::CONFLICT,
            render_settings_page(
                &cookies,
                current_user,
                &ssh_keys,
                Some("That username is already taken."),
            )
            .await,
        ));
    }

    let txn = match state.db.begin().await {
        Ok(txn) => txn,
        Err(err) => {
            return Err(internal_error(&cookies, current_user, &ssh_keys, &err.to_string()).await);
        }
    };

    let mut user_active: user::ActiveModel = current_user.clone().into();
    user_active.name = Set(name.to_owned());
    user_active.email = Set(email.to_owned());
    user_active.pronouns = Set(form.pronouns.trim().to_owned());
    user_active.organization = Set(form.organization.trim().to_owned());
    user_active.location = Set(form.location.trim().to_owned());
    user_active.website = Set(website.to_owned());
    user_active.description = Set(form.description.trim().to_owned());
    user_active.default_main_branch = Set(form.default_main_branch.trim().to_owned());

    if let Err(err) = user_active.update(&txn).await {
        return Err(internal_error(&cookies, current_user, &ssh_keys, &err.to_string()).await);
    }

    if let Err(err) = replace_ssh_keys(&txn, current_user.id, &ssh_keys).await {
        return Err(internal_error(&cookies, current_user, &ssh_keys, &err.to_string()).await);
    }

    if let Err(err) = txn.commit().await {
        return Err(internal_error(&cookies, current_user, &ssh_keys, &err.to_string()).await);
    }

    session_service::set_user_cookie(&cookies, current_user.id, slug);

    Ok(
        render_settings_page(&cookies, current_user, &ssh_keys, Some("Settings updated."))
            .await
            .into_response(),
    )
}

async fn render_settings_page(
    _cookies: &tower_cookies::Cookies,
    user: user::Model,
    ssh_keys: &[String],
    message: Option<&str>,
) -> Html<String> {
    Html(app::settings(user, ssh_keys, message).await)
}