Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error cleanup #384

Closed
wants to merge 9 commits into from
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 12 additions & 9 deletions backend/src/handlers_prelude/github_hook.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,22 +2,25 @@

use axum::routing::post;
use axum::{extract::State, http::HeaderMap, Router};
use tracing::{debug, error, info};
use tracing::{debug, info};

use crate::handlers_prelude::ApiError;
use crate::AppState;

pub async fn github_hook_handler(State(state): State<AppState>, headers: HeaderMap) {
pub async fn github_hook_handler(
State(state): State<AppState>,
headers: HeaderMap,
) -> Result<(), ApiError> {
let event_type = headers.get("x-github-event").unwrap().to_str().unwrap();
debug!("Received Github webhook event of type {event_type:?}");

debug!("Received Github webhook event of type {:?}", event_type);

if event_type == "push" {
info!("New changes pushed to Github, pulling changes...");
match state.git.pull() {
Ok(_) => {}
Err(e) => {
error!("Failed to auto-pull changes with error: {e:?}");
}
}
state.git.pull()?;
}

Ok(())
}

pub async fn create_github_route() -> Router<AppState> {
Expand Down
109 changes: 30 additions & 79 deletions backend/src/handlers_prelude/groups.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,10 @@ use axum::{
};
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use tracing::error;

use crate::{
db::{Database, Group},
eyre_to_axum_err,
handlers_prelude::ApiError,
perms::Permission,
require_perms, AppState,
};
Expand All @@ -33,16 +32,9 @@ pub struct GroupResponse {
pub async fn create_group_response(
db: &Database,
group: Group,
) -> Result<GroupResponse, (StatusCode, String)> {
let permissions = db
.get_group_permissions(group.id)
.await
.map_err(eyre_to_axum_err)?;

let members = db
.get_group_members(group.id)
.await
.map_err(eyre_to_axum_err)?;
) -> Result<GroupResponse, ApiError> {
let permissions = db.get_group_permissions(group.id).await?;
let members = db.get_group_members(group.id).await?;

Ok(GroupResponse {
id: group.id,
Expand All @@ -62,29 +54,17 @@ pub async fn create_group_response(
pub async fn get_groups_handler(
State(state): State<AppState>,
headers: HeaderMap,
) -> Result<Json<Vec<GroupResponse>>, (StatusCode, String)> {
) -> Result<Json<Vec<GroupResponse>>, ApiError> {
require_perms(State(&state), headers, &[Permission::ManageUsers]).await?;

match state.db.get_all_groups().await {
Ok(groups) => {
let mut get_groups_response = Vec::new();

for group in groups {
get_groups_response.push(create_group_response(&state.db, group).await?);
}

Ok(Json(get_groups_response))
}
Err(e) => {
error!("An error was encountered fetching all groups: {e:?}");
Err((
StatusCode::INTERNAL_SERVER_ERROR,
"An internal error was encountered fetching all groups, \
check server logs for more info"
.to_owned(),
))
}
let groups = state.db.get_all_groups().await?;
let mut get_groups_response = Vec::with_capacity(groups.len());

for group in groups {
get_groups_response.push(create_group_response(&state.db, group).await?);
}

Ok(Json(get_groups_response))
}

#[derive(Serialize, Deserialize)]
Expand All @@ -96,20 +76,13 @@ pub async fn post_group_handler(
State(state): State<AppState>,
headers: HeaderMap,
Json(body): Json<CreateGroupRequestBody>,
) -> Result<Json<GroupResponse>, (StatusCode, String)> {
) -> Result<Json<GroupResponse>, ApiError> {
require_perms(State(&state), headers, &[Permission::ManageUsers]).await?;

Ok(Json(
create_group_response(
&state.db,
state
.db
.create_group(body.group_name)
.await
.map_err(eyre_to_axum_err)?,
)
.await?,
))
let group = state.db.create_group(body.group_name).await?;
let response = create_group_response(&state.db, group).await?;

Ok(Json(response))
}

#[derive(Serialize, Deserialize)]
Expand All @@ -122,15 +95,10 @@ pub async fn put_group_permissions_handler(
headers: HeaderMap,
Path(group_id): Path<i64>,
Json(body): Json<UpdateGroupPermissionsRequestBody>,
) -> Result<Json<GroupResponse>, (StatusCode, String)> {
) -> Result<Json<GroupResponse>, ApiError> {
require_perms(State(&state), headers, &[Permission::ManageUsers]).await?;

let current_permissions = state
.db
.get_group_permissions(group_id)
.await
.map_err(eyre_to_axum_err)?;

let current_permissions = state.db.get_group_permissions(group_id).await?;
let new_permissions = body.permissions;

let permissions_to_remove = current_permissions
Expand All @@ -144,47 +112,30 @@ pub async fn put_group_permissions_handler(
.collect::<Vec<_>>();

for perm in permissions_to_remove {
state
.db
.remove_group_permission(group_id, *perm)
.await
.map_err(eyre_to_axum_err)?;
state.db.remove_group_permission(group_id, *perm).await?;
}

for perm in permissions_to_add {
state
.db
.add_group_permission(group_id, *perm)
.await
.map_err(eyre_to_axum_err)?;
state.db.add_group_permission(group_id, *perm).await?;
}

Ok(Json(
create_group_response(
&state.db,
state
.db
.get_group(group_id)
.await
.map_err(eyre_to_axum_err)?
.unwrap(),
)
.await?,
))
let updated_group = state.db.get_group(group_id).await?.ok_or_else(|| {
ApiError::from((StatusCode::NOT_FOUND, "Group not found in the database".to_string()))
})?;

Ok(Json(create_group_response(&state.db, updated_group).await?))
}

pub async fn delete_group_handler(
State(state): State<AppState>,
headers: HeaderMap,
Path(group_id): Path<i64>,
) -> Result<(), (StatusCode, String)> {
) -> Result<(), ApiError> {
require_perms(State(&state), headers, &[Permission::ManageUsers]).await?;

state
.db
.delete_group(group_id)
.await
.map_err(eyre_to_axum_err)
state.db.delete_group(group_id).await?;

Ok(())
}

pub async fn create_group_route() -> Router<AppState> {
Expand Down
99 changes: 65 additions & 34 deletions backend/src/handlers_prelude/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -27,42 +27,74 @@ use color_eyre::{
Report,
};
use reqwest::StatusCode;
use tracing::{debug, error, trace};
use tracing::{debug, trace};

use crate::{db::User, perms::Permission, AppState};

pub struct ApiError(eyre::Error);
pub struct ApiError {
status: Option<StatusCode>,
error: Report,
}

impl ApiError {
pub fn new(status: Option<StatusCode>, error: impl Into<Report>) -> Self {
Self {
status,
error: error.into(),
}
}
}

impl IntoResponse for ApiError {
fn into_response(self) -> Response {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Something went wrong: {}", self.0),
)
.into_response()
let status = self.status.unwrap_or(StatusCode::INTERNAL_SERVER_ERROR);
(status, self.error.to_string()).into_response()
}
}

impl From<eyre::Error> for ApiError {
fn from(err: eyre::Error) -> Self {
Self(err)
impl From<String> for ApiError {
fn from(message: String) -> Self {
Self {
status: Some(StatusCode::INTERNAL_SERVER_ERROR),
error: eyre::eyre!(message),
}
}
}

impl From<String> for ApiError {
fn from(err: String) -> Self {
Self(eyre::eyre!(err))
impl From<(StatusCode, String)> for ApiError {
fn from((status, message): (StatusCode, String)) -> Self {
Self {
status: Some(status),
error: eyre::eyre!(message),
}
}
}

/// Quick and dirty way to convert an eyre error to a (StatusCode, message) response, meant for use with `map_err`, so that errors can be propagated out of
/// axum handlers with `?`.
pub fn eyre_to_axum_err(e: Report) -> (StatusCode, String) {
error!("An error was encountered in an axum handler: {e:?}");
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("An error was encountered, check server logs for more info: {e}"),
)
impl From<&str> for ApiError {
fn from(message: &str) -> Self {
Self {
status: Some(StatusCode::INTERNAL_SERVER_ERROR),
error: eyre::eyre!(message.to_string()),
}
}
}

impl From<(StatusCode, &str)> for ApiError {
fn from((status, message): (StatusCode, &str)) -> Self {
Self {
status: Some(status),
error: eyre::eyre!(message.to_string()),
}
}
}

impl From<Report> for ApiError {
fn from(error: Report) -> Self {
Self {
status: Some(StatusCode::INTERNAL_SERVER_ERROR),
error,
}
}
}

/// The output of a find_user call, used to differentiate between expired users and valid users
Expand Down Expand Up @@ -118,13 +150,13 @@ pub async fn require_perms(
State(state): State<&AppState>,
headers: HeaderMap,
perms: &[Permission],
) -> Result<User, (StatusCode, String)> {
let maybe_user = find_user(state, headers).await.map_err(eyre_to_axum_err)?;
) -> Result<User, ApiError> {
let maybe_user = find_user(state, headers).await?;
match maybe_user {
Some(user) => match user {
FoundUser::ExpiredUser(u) => Err((
StatusCode::UNAUTHORIZED,
format!(
FoundUser::ExpiredUser(u) => Err(ApiError::new(
Some(StatusCode::UNAUTHORIZED),
eyre::eyre!(
"The access token has expired for the user {}, they must authenticate again.",
u.username
),
Expand All @@ -133,25 +165,24 @@ pub async fn require_perms(
let user_perms = &state
.db
.get_user_permissions(u.id)
.await
.map_err(eyre_to_axum_err)?;
.await?;
let has_permissions = perms.iter().all(|perm| user_perms.contains(perm));
if has_permissions {
Ok(u)
} else {
Err((
StatusCode::FORBIDDEN,
format!(
Err(ApiError::new(
Some(StatusCode::FORBIDDEN),
eyre::eyre!(
"User {:?} lacks the permission to edit documents.",
u.username
),
))
}
}
},
None => Err((
StatusCode::UNAUTHORIZED,
"No valid user is authenticated, perhaps you forgot to add `{credentials: \"include\"}` in your fetch options?.".to_string(),
None => Err(ApiError::new(
Some(StatusCode::UNAUTHORIZED),
eyre::eyre!("No valid user is authenticated, perhaps you forgot to add `{{credentials: \"include\"}}` in your fetch options?"),
)),
}
}
7 changes: 3 additions & 4 deletions backend/src/handlers_prelude/reclone.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,16 @@
use axum::routing::post;
use axum::{extract::State, http::HeaderMap, Router};
use reqwest::StatusCode;

use crate::{perms::Permission, AppState};

use super::{eyre_to_axum_err, require_perms};
use super::{ApiError, require_perms};

pub async fn post_reclone_handler(
State(state): State<AppState>,
headers: HeaderMap,
) -> Result<(), (StatusCode, String)> {
) -> Result<(), ApiError> {
require_perms(State(&state), headers, &[Permission::ManageUsers]).await?;
state.git.reclone().map_err(eyre_to_axum_err)?;
state.git.reclone()?;
Ok(())
}

Expand Down
Loading
Loading