text/x-rust
•
6.74 KB
•
236 lines
use std::sync::Arc;
use askama::Template;
use axum::{
Form,
extract::State,
http::StatusCode,
response::{Html, IntoResponse, Redirect, Response},
};
use russh::keys::ssh_key;
use serde::Deserialize;
use tower_cookies::Cookies;
use rubhub_auth_store::SshKey;
use crate::{
GlobalState, Project, User, UserModel,
models::ContentPage,
services::{session as session_service, validation::validate_username},
views::ThemedRender,
};
#[derive(Debug, Deserialize)]
pub struct UserSettingsForm {
pub name: String,
pub email: String,
pub default_main_branch: String,
pub ssh_keys: Option<String>,
}
#[derive(Template)]
#[template(path = "user/settings.html")]
struct UserSettingsTemplate<'a> {
user: Arc<User>,
ssh_keys: &'a [String],
message: Option<&'a str>,
logged_in_user: Option<Arc<User>>,
sidebar_projects: Vec<Project>,
content_pages: Vec<ContentPage>,
}
/// Validate all SSH keys and return invalid ones
fn find_invalid_ssh_keys(keys: &[String]) -> Vec<String> {
keys.iter()
.filter(|key| !key.is_empty() && ssh_key::PublicKey::from_openssh(key).is_err())
.cloned()
.collect()
}
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 ssh_keys: Vec<String> = state
.auth
.get_ssh_keys_for_user(¤t_user.slug)
.iter()
.map(|k| k.to_authorized_keys_line())
.collect();
Ok(render_settings_page(&state, current_user, &ssh_keys, None).await)
}
async fn internal_error(
state: &GlobalState,
user: Arc<User>,
ssh_keys: &[String],
err: &str,
) -> (StatusCode, Html<String>) {
eprintln!("auth error: {err}");
(
StatusCode::INTERNAL_SERVER_ERROR,
render_settings_page(state, user, ssh_keys, Some(err)).await,
)
}
pub async fn handle_settings(
State(state): State<GlobalState>,
cookies: Cookies,
Form(form): Form<UserSettingsForm>,
) -> Result<Response, (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 default_main_branch = form.default_main_branch.trim();
let email = form.email.trim().to_owned();
let ssh_keys_raw: Vec<String> = form
.ssh_keys
.unwrap_or_default()
.lines()
.map(str::trim)
.filter(|line| !line.is_empty())
.map(ToOwned::to_owned)
.collect();
// Validate SSH keys
let invalid_keys = find_invalid_ssh_keys(&ssh_keys_raw);
if !invalid_keys.is_empty() {
let first_invalid = &invalid_keys[0];
let preview = if first_invalid.len() > 40 {
format!("{}...", &first_invalid[..40])
} else {
first_invalid.clone()
};
return Err((
StatusCode::BAD_REQUEST,
render_settings_page(
&state,
current_user,
&ssh_keys_raw,
Some(&format!("Invalid SSH key format: {}", preview)),
)
.await,
));
}
if default_main_branch.is_empty() {
return Err((
StatusCode::BAD_REQUEST,
render_settings_page(
&state,
current_user,
&ssh_keys_raw,
Some("Default main branch is required"),
)
.await,
));
}
if let Err(msg) = validate_username(name) {
return Err((
StatusCode::BAD_REQUEST,
render_settings_page(&state, current_user, &ssh_keys_raw, Some(msg)).await,
));
}
// Parse submitted keys
let submitted_keys: Vec<SshKey> = ssh_keys_raw
.iter()
.filter_map(|line| SshKey::from_authorized_keys_line(line, current_user.slug.clone()))
.collect();
// Get current keys for this user
let current_keys = state.auth.get_ssh_keys_for_user(¤t_user.slug);
let current_key_data: std::collections::HashSet<&str> =
current_keys.iter().map(|k| k.public_key.as_str()).collect();
let submitted_key_data: std::collections::HashSet<&str> = submitted_keys
.iter()
.map(|k| k.public_key.as_str())
.collect();
// Check for duplicate keys owned by other users
for key in &submitted_keys {
if let Some(existing) = state.auth.get_ssh_key(&key.public_key)
&& existing.user_slug != current_user.slug
{
return Err((
StatusCode::BAD_REQUEST,
render_settings_page(
&state,
current_user,
&ssh_keys_raw,
Some("One of the SSH keys is already registered to another user"),
)
.await,
));
}
}
// Delete removed keys
for key in ¤t_keys {
if !submitted_key_data.contains(key.public_key.as_str())
&& let Err(err) = SshKey::delete(&state.auth, key.public_key.clone())
{
return Err(
internal_error(&state, current_user, &ssh_keys_raw, &err.to_string()).await,
);
}
}
// Add new keys
for key in submitted_keys {
if !current_key_data.contains(key.public_key.as_str())
&& let Err(err) = key.save(&state.auth)
{
return Err(
internal_error(&state, current_user, &ssh_keys_raw, &err.to_string()).await,
);
}
}
// Update user (without ssh_keys field)
let mut new_user = current_user.as_ref().clone();
new_user.name = name.to_owned();
new_user.email = email.to_owned();
new_user.default_main_branch = default_main_branch.to_owned();
if let Err(err) = new_user.save(&state.auth) {
return Err(internal_error(&state, current_user, &ssh_keys_raw, &err.to_string()).await);
}
Ok(render_settings_page(
&state,
current_user,
&ssh_keys_raw,
Some("Settings updated."),
)
.await
.into_response())
}
async fn render_settings_page(
state: &GlobalState,
user: Arc<User>,
ssh_keys: &[String],
message: Option<&str>,
) -> Html<String> {
let sidebar_projects = user.sidebar_projects(state).await;
let template = UserSettingsTemplate {
user: user.clone(),
ssh_keys,
message,
logged_in_user: Some(user),
sidebar_projects,
content_pages: state.config.content_pages.clone(),
};
Html(template.render_with_theme())
}