1
0
Fork 0
forked from 2sys/shoutdotdev
shoutdotdev/src/projects_router.rs

251 lines
8.1 KiB
Rust
Raw Normal View History

2025-03-14 13:04:57 -07:00
use std::collections::HashSet;
use anyhow::Context as _;
use askama::Template;
use axum::{
extract::{Path, State},
response::{Html, IntoResponse, Redirect},
routing::{get, post},
Router,
};
use axum_extra::extract::Form;
use diesel::prelude::*;
use serde::Deserialize;
use uuid::Uuid;
use crate::{
api_keys::ApiKey,
app_error::AppError,
app_state::{AppState, DbConn},
channel_selections::ChannelSelection,
channels::Channel,
csrf::generate_csrf_token,
guards,
2025-04-04 13:42:10 -07:00
nav::{BreadcrumbTrail, Navbar, NavbarBuilder, NAVBAR_ITEM_PROJECTS},
2025-03-14 13:04:57 -07:00
projects::Project,
schema::channel_selections,
settings::Settings,
users::CurrentUser,
};
pub fn new_router() -> Router<AppState> {
Router::new()
.route("/teams/{team_id}/projects", get(projects_page))
.route("/teams/{team_id}/projects/{project_id}", get(project_page))
.route(
"/teams/{team_id}/projects/{project_id}/update-enabled-channels",
post(update_enabled_channels),
)
}
async fn projects_page(
State(Settings { base_path, .. }): State<Settings>,
2025-04-04 13:42:10 -07:00
State(navbar_template): State<NavbarBuilder>,
2025-03-14 13:04:57 -07:00
DbConn(db_conn): DbConn,
Path(team_id): Path<Uuid>,
CurrentUser(current_user): CurrentUser,
) -> Result<impl IntoResponse, AppError> {
let team = guards::require_team_membership(&current_user, &team_id, &db_conn).await?;
let (api_keys, projects) = {
let team = team.clone();
db_conn
.interact(move |conn| {
2025-04-01 13:29:00 -07:00
diesel::QueryResult::Ok((
team.api_keys().load(conn)?,
Project::all()
.filter(Project::with_team(&team.id))
.load(conn)?,
))
2025-03-14 13:04:57 -07:00
})
.await
.unwrap()?
};
mod filters {
use uuid::Uuid;
pub fn compact_uuid(id: &Uuid) -> askama::Result<String> {
Ok(crate::api_keys::compact_uuid(id))
}
pub fn redact(value: &str) -> askama::Result<String> {
Ok(format!(
"********{}",
&value[value.char_indices().nth_back(3).unwrap().0..]
))
}
}
#[derive(Template)]
#[template(path = "projects.html")]
struct ResponseTemplate {
base_path: String,
2025-04-04 13:42:10 -07:00
breadcrumbs: BreadcrumbTrail,
2025-03-14 13:04:57 -07:00
csrf_token: String,
keys: Vec<ApiKey>,
2025-04-04 13:42:10 -07:00
navbar: Navbar,
2025-03-14 13:04:57 -07:00
projects: Vec<Project>,
}
let csrf_token = generate_csrf_token(&db_conn, Some(current_user.id)).await?;
Ok(Html(
ResponseTemplate {
2025-04-04 13:42:10 -07:00
breadcrumbs: BreadcrumbTrail::from_base_path(&base_path)
.with_i18n_slug("en")
.push_slug("Teams", "teams")
.push_slug(&team.name, &team.id.simple().to_string())
.push_slug("Projects", "projects"),
2025-03-14 13:04:57 -07:00
base_path,
csrf_token,
2025-04-04 13:42:10 -07:00
navbar: navbar_template
.with_param("team_id", &team.id.simple().to_string())
.with_active_item(NAVBAR_ITEM_PROJECTS)
.build(),
2025-03-14 13:04:57 -07:00
projects,
keys: api_keys,
}
.render()?,
)
.into_response())
}
async fn project_page(
State(Settings { base_path, .. }): State<Settings>,
2025-04-04 13:42:10 -07:00
State(navbar_template): State<NavbarBuilder>,
2025-03-14 13:04:57 -07:00
DbConn(db_conn): DbConn,
Path((team_id, project_id)): Path<(Uuid, Uuid)>,
CurrentUser(current_user): CurrentUser,
) -> Result<impl IntoResponse, AppError> {
let team = guards::require_team_membership(&current_user, &team_id, &db_conn).await?;
let project = db_conn
.interact(move |conn| {
match Project::all()
.filter(Project::with_id(&project_id))
.filter(Project::with_team(&team_id))
.first(conn)
{
diesel::QueryResult::Err(diesel::NotFound) => Err(AppError::NotFound(
2025-03-14 13:04:57 -07:00
"Project with that team and ID not found.".to_string(),
)),
other => other
.context("failed to load project")
.map_err(|err| err.into()),
}
})
.await
.unwrap()?;
let selected_channels_query = project.selected_channels();
let enabled_channel_ids: HashSet<Uuid> = db_conn
.interact(move |conn| selected_channels_query.load(conn))
.await
.unwrap()
.context("failed to load selected channels")?
.iter()
.map(|channel| channel.id)
.collect();
let team_channels = db_conn
.interact(move |conn| {
Channel::all()
.filter(Channel::with_team(&team_id))
.load(conn)
})
.await
.unwrap()
.context("failed to load team channels")?;
let csrf_token = generate_csrf_token(&db_conn, Some(current_user.id)).await?;
#[derive(Template)]
#[template(path = "project.html")]
struct ResponseTemplate {
base_path: String,
2025-04-04 13:42:10 -07:00
breadcrumbs: BreadcrumbTrail,
2025-03-14 13:04:57 -07:00
csrf_token: String,
enabled_channel_ids: HashSet<Uuid>,
2025-04-04 13:42:10 -07:00
navbar: Navbar,
2025-03-14 13:04:57 -07:00
project: Project,
team_channels: Vec<Channel>,
}
Ok(Html(
ResponseTemplate {
2025-04-04 13:42:10 -07:00
breadcrumbs: BreadcrumbTrail::from_base_path(&base_path)
.with_i18n_slug("en")
.push_slug("Teams", "teams")
.push_slug(&team.name, &team.id.simple().to_string())
.push_slug("Projects", "projects")
.push_slug(&project.name, &project.id.simple().to_string()),
2025-03-14 13:04:57 -07:00
base_path,
csrf_token,
enabled_channel_ids,
project,
2025-04-04 13:42:10 -07:00
navbar: navbar_template
.with_param("team_id", &team.id.simple().to_string())
.with_active_item(NAVBAR_ITEM_PROJECTS)
.build(),
2025-03-14 13:04:57 -07:00
team_channels,
}
.render()?,
))
}
#[derive(Deserialize)]
struct UpdateEnabledChannelsFormBody {
csrf_token: String,
#[serde(default)]
enabled_channels: Vec<Uuid>,
}
async fn update_enabled_channels(
State(Settings { base_path, .. }): State<Settings>,
DbConn(db_conn): DbConn,
Path((team_id, project_id)): Path<(Uuid, Uuid)>,
CurrentUser(current_user): CurrentUser,
Form(form_body): Form<UpdateEnabledChannelsFormBody>,
) -> Result<impl IntoResponse, AppError> {
guards::require_valid_csrf_token(&form_body.csrf_token, &current_user, &db_conn).await?;
guards::require_team_membership(&current_user, &team_id, &db_conn).await?;
db_conn
.interact(move |conn| -> Result<(), AppError> {
let project = match Project::all()
.filter(Project::with_id(&project_id))
.filter(Project::with_team(&team_id))
.first(conn)
{
diesel::QueryResult::Err(diesel::NotFound) => Err(AppError::NotFound(
2025-03-14 13:04:57 -07:00
"Project with that team and ID not found.".to_string(),
)),
other => other
.context("failed to load project")
.map_err(|err| err.into()),
}?;
diesel::delete(
channel_selections::table
.filter(ChannelSelection::with_project(&project.id))
.filter(channel_selections::channel_id.ne_all(&form_body.enabled_channels)),
)
.execute(conn)
.context("failed to remove unset channel selections")?;
for channel_id in form_body.enabled_channels {
diesel::insert_into(channel_selections::table)
.values((
channel_selections::project_id.eq(&project.id),
channel_selections::channel_id.eq(channel_id),
))
.on_conflict_do_nothing()
.execute(conn)
.context("failed to insert channel selections")?;
}
Ok(())
})
.await
.unwrap()?;
Ok(Redirect::to(&format!(
"{}/en/teams/{}/projects/{}",
2025-03-14 13:04:57 -07:00
base_path, team_id, project_id
))
.into_response())
}