From 2b7213021a3fa7c3e7417b219b570f4edca7594e Mon Sep 17 00:00:00 2001 From: c0repwn3r Date: Sun, 26 Mar 2023 18:14:55 -0400 Subject: [PATCH] userget --- src/main.rs | 1 + src/routes/v1/mod.rs | 3 +- src/routes/v1/user_add.rs | 11 ++++ src/routes/v1/user_get.rs | 129 ++++++++++++++++++++++++++++++++++++++ src/tokens.rs | 42 ++++++++----- 5 files changed, 168 insertions(+), 18 deletions(-) create mode 100644 src/routes/v1/user_get.rs diff --git a/src/main.rs b/src/main.rs index 3ed7ccf..a47a685 100644 --- a/src/main.rs +++ b/src/main.rs @@ -72,6 +72,7 @@ async fn main() -> std::io::Result<()> { })) .service(routes::v1::code_3fa::get_3fa_code) .service(routes::v1::user_add::add_user_request) + .service(routes::v1::user_get::get_user_request) }) .bind(("127.0.0.1", 8080))? .run() diff --git a/src/routes/v1/mod.rs b/src/routes/v1/mod.rs index 6dbeeae..aa19a1a 100644 --- a/src/routes/v1/mod.rs +++ b/src/routes/v1/mod.rs @@ -1,2 +1,3 @@ pub mod code_3fa; -pub mod user_add; \ No newline at end of file +pub mod user_add; +pub mod user_get; \ No newline at end of file diff --git a/src/routes/v1/user_add.rs b/src/routes/v1/user_add.rs index 6f2e579..69b2a37 100644 --- a/src/routes/v1/user_add.rs +++ b/src/routes/v1/user_add.rs @@ -41,6 +41,17 @@ pub async fn add_user_request(pool: Data, req: Json) -> }) } + if !token_has_scope(&req.token, &Scope::UserRead) { + return HttpResponse::Unauthorized().json(APIErrorResponse { + errors: vec![ + APIError { + code: "ERR_MISSING_SCOPE".to_string(), + message: "This endpoint requires the user:read scope".to_string(), + } + ], + }) + } + let req_clone = req.clone(); let pool_clone = pool.clone(); let results = match web::block(move || { diff --git a/src/routes/v1/user_get.rs b/src/routes/v1/user_get.rs new file mode 100644 index 0000000..974a614 --- /dev/null +++ b/src/routes/v1/user_get.rs @@ -0,0 +1,129 @@ +use actix_web::{HttpResponse, web}; +use actix_web::post; +use actix_web::web::{Data, Json}; +use log::error; +use serde::{Serialize, Deserialize}; + +use crate::error::{APIError, APIErrorResponse}; +use crate::models::{NewUser, User}; +use crate::PgPool; +use diesel::prelude::*; +use crate::tokens::{Scope, token_has_scope}; + +#[derive(Serialize, Deserialize, Clone)] +pub struct UserGetRequest { + pub token: String, + pub name: Option, + pub discord_id: Option, + pub id: Option +} + +#[derive(Serialize, Deserialize, Clone)] +pub struct UserResponse { + pub id: i32, + pub name: String, + pub discord_id: i64, + pub password_hash: String +} + +#[post("/v1/user/get")] +pub async fn get_user_request(pool: Data, req: Json) -> HttpResponse { + use crate::schema::users; + + if !token_has_scope(&req.token, &Scope::UserRead) { + return HttpResponse::Unauthorized().json(APIErrorResponse { + errors: vec![ + APIError { + code: "ERR_MISSING_SCOPE".to_string(), + message: "This endpoint requires the user:read scope".to_string(), + } + ], + }) + } + + let mut amt = 0; + if req.id.is_some() { amt += 1; } + if req.name.is_some() { amt += 1; } + if req.discord_id.is_some() { amt += 1; } + + if amt == 0 { + return HttpResponse::BadRequest().json(APIErrorResponse { + errors: vec![ + APIError { + code: "ERR_NOT_SPECIFIC_ENOUGH".to_string(), + message: "One filter must be provided".to_string(), + } + ], + }) + } else if amt != 1 { + return HttpResponse::BadRequest().json(APIErrorResponse { + errors: vec![ + APIError { + code: "ERR_TOO_SPECIFIC".to_string(), + message: "Only one filter may be provided".to_string(), + } + ], + }) + } + + let req_clone = req.clone(); + let pool_clone = pool.clone(); + let results = match web::block(move || { + let mut conn = pool_clone.get().expect("Unable to get db pool"); + + if let Some(id) = req_clone.id { + users::table.filter(users::id.eq(id)).load::(&mut conn) + } else if let Some(name) = req_clone.name { + users::table.filter(users::name.eq(name)).load::(&mut conn) + } else if let Some(discord_id) = req_clone.discord_id { + users::table.filter(users::discord_id.eq(discord_id)).load::(&mut conn) + } else { + unreachable!() + } + }).await { + Ok(r) => r, + Err(e) => { + error!("Database error: {}", e); + return HttpResponse::InternalServerError().json(APIErrorResponse { + errors: vec![ + APIError { + code: "ERR_BLOCKING_ERROR".to_string(), + message: "There was an error running the database request. Please try again later.".to_string() + } + ] + }) + } + }; + let user_list = match results { + Ok(r) => r, + Err(e) => { + error!("Database error: {}", e); + return HttpResponse::InternalServerError().json(APIErrorResponse { + errors: vec![ + APIError { + code: "ERR_DB_ERROR".to_string(), + message: "There was an error fetching the user. Please try again later.".to_string() + } + ] + }) + } + }; + if user_list.is_empty() { + return HttpResponse::Unauthorized().json(APIErrorResponse { + errors: vec![ + APIError { + code: "ERR_USER_DOES_NOT_EXIST".to_string(), + message: "Unable to find a user by that search query".to_string() + } + ] + }) + } + let user = &user_list[0]; + + HttpResponse::Ok().json(UserResponse { + id: user.id, + name: user.name.clone(), + discord_id: user.discord_id, + password_hash: user.password_hash.clone(), + }) +} \ No newline at end of file diff --git a/src/tokens.rs b/src/tokens.rs index 0806be8..8ecb115 100644 --- a/src/tokens.rs +++ b/src/tokens.rs @@ -1,3 +1,4 @@ +use actix_web::web::to; use crate::config::CONFIG; pub fn token_valid(token: &str) -> bool { @@ -11,41 +12,48 @@ pub fn token_has_scope(token: &str, scope: &Scope) -> bool { scopes.contains(scope) } +pub fn token_has_scopes(token: &str, req_scopes: &Vec) -> bool { + if !token_valid(token) { return false; } + let token = CONFIG.tokens.iter().find(|u| u.token == token).unwrap(); + let scopes: Vec = token.scopes.iter().map(|f| f.as_str().into()).collect(); + + for scope in req_scopes { + if !scopes.contains(scope) { + return false; + } + } + + true +} + #[derive(Eq, PartialEq, Debug)] -pub enum Scope<'a> { +pub enum Scope { Code3FAAdd, UserAdd, UserRead, UserRemove, - Unknown { scope: &'a str } + Unknown { scope: String } } -impl<'a> From<&str> for Scope<'a> { +impl From<&str> for Scope { fn from(value: &str) -> Self { match value { "3fa:add" => Self::Code3FAAdd, "user:add" => Self::UserAdd, "user:read" => Self::UserRead, "user:remove" => Self::UserRemove, - _ => Self::Unknown { scope: value } + _ => Scope::Unknown { scope: value.to_string() } } } } -impl<'a> From> for &str { - fn from(value: Scope<'a>) -> Self { +impl From for String { + fn from(value: Scope) -> Self { match value { - Scope::Code3FAAdd => "3fa:add", - Scope::UserAdd => "user:add", - Scope::UserRead => "user:read", - Scope::UserRemove => "user:remove", + Scope::Code3FAAdd => "3fa:add".to_string(), + Scope::UserAdd => "user:add".to_string(), + Scope::UserRead => "user:read".to_string(), + Scope::UserRemove => "user:remove".to_string(), Scope::Unknown { scope } => scope } } -} - -impl<'a> From> for String { - fn from(value: Scope<'a>) -> Self { - let str_val: &str = value.into(); - str_val.to_string() - } } \ No newline at end of file