forked from 2sys/shoutdotdev
259 lines
8.5 KiB
Rust
259 lines
8.5 KiB
Rust
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::{self, ChannelSelection},
|
|
channels::Channel,
|
|
csrf::generate_csrf_token,
|
|
guards,
|
|
nav::{BreadcrumbTrail, Navbar, NavbarBuilder, NAVBAR_ITEM_PROJECTS},
|
|
projects::{self, Project},
|
|
settings::Settings,
|
|
users::CurrentUser,
|
|
watchdogs::{self, Watchdog},
|
|
};
|
|
|
|
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,
|
|
frontend_host,
|
|
..
|
|
}): State<Settings>,
|
|
State(navbar_template): State<NavbarBuilder>,
|
|
DbConn(db_conn): DbConn,
|
|
Path(team_id): Path<Uuid>,
|
|
CurrentUser(current_user): CurrentUser,
|
|
) -> Result<impl IntoResponse, AppError> {
|
|
let team = guards::require_team_membership(¤t_user, &team_id, &db_conn).await?;
|
|
|
|
let (api_keys, projects) = {
|
|
let team = team.clone();
|
|
db_conn
|
|
.interact(move |conn| {
|
|
diesel::QueryResult::Ok((
|
|
team.api_keys().load(conn)?,
|
|
Project::all()
|
|
.filter(Project::with_team(&team.id))
|
|
.load(conn)?,
|
|
))
|
|
})
|
|
.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,
|
|
breadcrumbs: BreadcrumbTrail,
|
|
csrf_token: String,
|
|
frontend_host: String,
|
|
keys: Vec<ApiKey>,
|
|
navbar: Navbar,
|
|
projects: Vec<Project>,
|
|
}
|
|
let csrf_token = generate_csrf_token(&db_conn, Some(current_user.id)).await?;
|
|
Ok(Html(
|
|
ResponseTemplate {
|
|
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"),
|
|
base_path,
|
|
csrf_token,
|
|
frontend_host,
|
|
keys: api_keys,
|
|
navbar: navbar_template
|
|
.with_param("team_id", &team.id.simple().to_string())
|
|
.with_active_item(NAVBAR_ITEM_PROJECTS)
|
|
.build(),
|
|
projects,
|
|
}
|
|
.render()?,
|
|
)
|
|
.into_response())
|
|
}
|
|
|
|
async fn project_page(
|
|
State(Settings {
|
|
base_path,
|
|
frontend_host,
|
|
..
|
|
}): State<Settings>,
|
|
State(navbar_template): State<NavbarBuilder>,
|
|
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(¤t_user, &team_id, &db_conn).await?;
|
|
|
|
let (project, maybe_watchdog, enabled_channel_ids, team_channels) = db_conn
|
|
.interact(move |conn| -> Result<_, AppError> {
|
|
// Queried together to save a round trip to the database.
|
|
let (project, maybe_watchdog) = projects::table
|
|
.left_outer_join(watchdogs::table)
|
|
.select(<(Project, Option<Watchdog>)>::as_select())
|
|
.filter(Project::with_id(&project_id))
|
|
.filter(Project::with_team(&team_id))
|
|
.first(conn)
|
|
.optional()
|
|
.context("failed to load project")?
|
|
.ok_or(AppError::NotFound(
|
|
"Project with that team and ID not found.".to_string(),
|
|
))?;
|
|
let enabled_channel_ids: HashSet<Uuid> = project
|
|
.selected_channels()
|
|
.load(conn)
|
|
.context("failed to load selected channels")?
|
|
.iter()
|
|
.map(|channel| channel.id)
|
|
.collect();
|
|
let team_channels = Channel::all()
|
|
.filter(Channel::with_team(&team_id))
|
|
.load(conn)
|
|
.context("failed to load team channels")?;
|
|
Ok((project, maybe_watchdog, enabled_channel_ids, team_channels))
|
|
})
|
|
.await
|
|
.unwrap()?;
|
|
|
|
let csrf_token = generate_csrf_token(&db_conn, Some(current_user.id)).await?;
|
|
|
|
#[derive(Template)]
|
|
#[template(path = "project.html")]
|
|
struct ResponseTemplate {
|
|
base_path: String,
|
|
breadcrumbs: BreadcrumbTrail,
|
|
csrf_token: String,
|
|
enabled_channel_ids: HashSet<Uuid>,
|
|
frontend_host: String,
|
|
navbar: Navbar,
|
|
project: Project,
|
|
team_channels: Vec<Channel>,
|
|
watchdog: Option<Watchdog>,
|
|
}
|
|
Ok(Html(
|
|
ResponseTemplate {
|
|
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()),
|
|
base_path,
|
|
csrf_token,
|
|
enabled_channel_ids,
|
|
frontend_host,
|
|
project,
|
|
navbar: navbar_template
|
|
.with_param("team_id", &team.id.simple().to_string())
|
|
.with_active_item(NAVBAR_ITEM_PROJECTS)
|
|
.build(),
|
|
team_channels,
|
|
watchdog: maybe_watchdog,
|
|
}
|
|
.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, ¤t_user, &db_conn).await?;
|
|
guards::require_team_membership(¤t_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(
|
|
"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::dsl::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::dsl::project_id.eq(&project.id),
|
|
channel_selections::dsl::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/{}",
|
|
base_path, team_id, project_id
|
|
))
|
|
.into_response())
|
|
}
|