From f559c415698411451ecb5ade58a67bf298b7eb6a Mon Sep 17 00:00:00 2001 From: Jordan Doyle Date: Thu, 21 Oct 2021 02:14:31 +0100 Subject: [PATCH] Keep request handlers top of each file --- chartered-web/src/main.rs | 5 +++++ chartered-web/src/middleware/auth.rs | 13 ++++++++++++- chartered-web/src/middleware/logging.rs | 2 ++ chartered-web/src/endpoints/cargo_api/download.rs | 96 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++------------------------ chartered-web/src/endpoints/cargo_api/mod.rs | 9 +++++++++ chartered-web/src/endpoints/cargo_api/owners.rs | 64 +++++++++++++++++++++++++++++++++++++--------------------------- chartered-web/src/endpoints/cargo_api/publish.rs | 141 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---- chartered-web/src/endpoints/cargo_api/yank.rs | 47 +++++++++++++++++++++++++++-------------------- chartered-web/src/endpoints/web_api/ssh_key.rs | 41 ++++++++++++++++++++++------------------- chartered-web/src/endpoints/web_api/auth/logout.rs | 3 +++ chartered-web/src/endpoints/web_api/auth/mod.rs | 2 ++ chartered-web/src/endpoints/web_api/auth/openid.rs | 75 +++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------- chartered-web/src/endpoints/web_api/auth/password.rs | 44 ++++++++++++++++++++++++-------------------- chartered-web/src/endpoints/web_api/crates/info.rs | 50 ++++++++++++++++++++++++++++++-------------------- chartered-web/src/endpoints/web_api/crates/members.rs | 60 +++++++++++++++++++++++++++++++++++++----------------------- chartered-web/src/endpoints/web_api/crates/most_downloaded.rs | 34 ++++++++++++++++++---------------- chartered-web/src/endpoints/web_api/crates/recently_created.rs | 35 +++++++++++++++++++---------------- chartered-web/src/endpoints/web_api/crates/recently_updated.rs | 34 ++++++++++++++++++---------------- chartered-web/src/endpoints/web_api/crates/search.rs | 46 ++++++++++++++++++++++++++-------------------- chartered-web/src/endpoints/web_api/organisations/crud.rs | 37 ++++++++++++++++++++----------------- chartered-web/src/endpoints/web_api/organisations/info.rs | 40 ++++++++++++++++++++++++---------------- chartered-web/src/endpoints/web_api/organisations/list.rs | 35 +++++++++++++++++++---------------- chartered-web/src/endpoints/web_api/organisations/members.rs | 28 +++++++++++++++++----------- chartered-web/src/endpoints/web_api/users/info.rs | 23 ++++++++++++++--------- chartered-web/src/endpoints/web_api/users/search.rs | 40 +++++++++++++++++++++++----------------- 25 files changed, 585 insertions(+), 419 deletions(-) diff --git a/chartered-web/src/main.rs b/chartered-web/src/main.rs index 7ab6c49..9b35ccf 100644 --- a/chartered-web/src/main.rs +++ a/chartered-web/src/main.rs @@ -58,8 +58,11 @@ #[tokio::main] #[allow(clippy::semicolon_if_nothing_returned)] // lint breaks with tokio::main async fn main() -> Result<(), InitError> { + // parse CLI arguments let opts: Opts = Opts::parse(); + // overrides the RUST_LOG variable to our own value based on the + // amount of `-v`s that were passed when calling the service std::env::set_var( "RUST_LOG", match opts.verbose { @@ -71,11 +74,13 @@ let config: config::Config = toml::from_slice(&std::fs::read(&opts.config)?)?; + // initialise logging/tracing tracing_subscriber::fmt::init(); let bind_address = config.bind_address; let pool = chartered_db::init(&config.database_uri)?; + // the base stack of middleware that is applied to _all_ routes let middleware_stack = ServiceBuilder::new() .layer_fn(middleware::logging::LoggingMiddleware) .into_inner(); diff --git a/chartered-web/src/middleware/auth.rs b/chartered-web/src/middleware/auth.rs index 4c66dbf..5357247 100644 --- a/chartered-web/src/middleware/auth.rs +++ a/chartered-web/src/middleware/auth.rs @@ -1,3 +1,6 @@ +//! Check the API key embedded in the path is valid otherwise returns a 401 for authenticated +//! endpoints. + use axum::{ body::{box_body, Body, BoxBody}, extract::{self, FromRequest, RequestParts}, @@ -41,12 +44,15 @@ Box::pin(async move { let mut req = RequestParts::new(req); + // extracts all parameters from the path so we can get the API key which should + // always be named key let params = extract::Path::>::from_request(&mut req) .await .unwrap(); - let key = params.get("key").map(String::as_str).unwrap_or_default(); + // grab the ConnectionPool from the extensions created when we initialised the + // server let db = req .extensions() .unwrap() @@ -54,6 +60,8 @@ .unwrap() .clone(); + // grab the UserSession that's currently being used for this request and the User that + // owns the key, otherwise return a 401 if the key doesn't exist let (session, user) = match User::find_by_session_key(db, String::from(key)) .await .unwrap() @@ -72,9 +80,12 @@ } }; + // insert both the user and the session into extensions so handlers can + // get their hands on them req.extensions_mut().unwrap().insert(user); req.extensions_mut().unwrap().insert(session); + // calls handlers/other middleware and drives the request to response let response: Response = inner.call(req.try_into_request().unwrap()).await?; Ok(response) diff --git a/chartered-web/src/middleware/logging.rs b/chartered-web/src/middleware/logging.rs index c92d36d..7ada30d 100644 --- a/chartered-web/src/middleware/logging.rs +++ a/chartered-web/src/middleware/logging.rs @@ -1,3 +1,5 @@ +//! Logs each and every request out in a format similar to that of Apache's logs. + use axum::{ extract::{self, FromRequest, RequestParts}, http::{Request, Response}, diff --git a/chartered-web/src/endpoints/cargo_api/download.rs b/chartered-web/src/endpoints/cargo_api/download.rs index 858fdf2..1282394 100644 --- a/chartered-web/src/endpoints/cargo_api/download.rs +++ a/chartered-web/src/endpoints/cargo_api/download.rs @@ -1,3 +1,7 @@ +//! Called by cargo to download a crate, depending on how we're configured we'll either serve +//! the crate directly from the disk - or we'll redirect cargo elsewhere to download the +//! crate. It all really depends on the `FileSystem` in use in `chartered-fs`. + use axum::{ body::{Full, HttpBody}, extract, @@ -6,50 +10,9 @@ }; use bytes::Bytes; use chartered_db::{crates::Crate, users::User, ConnectionPool}; -use chartered_fs::{FilePointer, FileSystem}; +use chartered_fs::{FilePointer, FileReference, FileSystem}; use std::{str::FromStr, sync::Arc}; use thiserror::Error; - -#[derive(Error, Debug)] -pub enum Error { - #[error("{0}")] - Database(#[from] chartered_db::Error), - #[error("Failed to fetch crate file: {0}")] - File(#[from] Box), - #[error("The requested version does not exist for the crate")] - NoVersion, -} - -impl Error { - pub fn status_code(&self) -> axum::http::StatusCode { - use axum::http::StatusCode; - - match self { - Self::Database(e) => e.status_code(), - Self::File(_) => StatusCode::INTERNAL_SERVER_ERROR, - Self::NoVersion => StatusCode::NOT_FOUND, - } - } -} - -define_error_response!(Error); - -pub enum ResponseOrRedirect { - Response(Vec), - Redirect(Redirect), -} - -impl IntoResponse for ResponseOrRedirect { - type Body = Full; - type BodyError = ::Error; - - fn into_response(self) -> Response { - match self { - Self::Response(v) => v.into_response(), - Self::Redirect(v) => v.into_response().map(|_| Full::from(Bytes::new())), - } - } -} pub async fn handle( extract::Path((_session_key, organisation, name, version)): extract::Path<( @@ -73,17 +36,62 @@ .increment_download_count(db.clone()), ); + // grab the requested version of the crate let version = crate_with_permissions .version(db, version) .await? .ok_or(Error::NoVersion)?; - let file_ref = chartered_fs::FileReference::from_str(&version.filesystem_object).unwrap(); - + // parses the filesystem_object returned by the database to a `FileReference` that + // we can use to get a `FilePointer` that is either on the disk and already available + // to us or is stored elsewhere but we have a link to that we can redirect to. + let file_ref = FileReference::from_str(&version.filesystem_object).map_err(Box::new)?; let res = fs.read(file_ref).await.map_err(Box::new)?; match res { FilePointer::Redirect(uri) => Ok(ResponseOrRedirect::Redirect(Redirect::to(uri))), FilePointer::Content(content) => Ok(ResponseOrRedirect::Response(content)), + } +} + +/// Returns either bytes directly to the client or redirects them elsewhere. +pub enum ResponseOrRedirect { + Response(Vec), + Redirect(Redirect), +} + +impl IntoResponse for ResponseOrRedirect { + type Body = Full; + type BodyError = ::Error; + + fn into_response(self) -> Response { + match self { + Self::Response(v) => v.into_response(), + Self::Redirect(v) => v.into_response().map(|_| Full::from(Bytes::new())), + } + } +} + +#[derive(Error, Debug)] +pub enum Error { + #[error("{0}")] + Database(#[from] chartered_db::Error), + #[error("Failed to fetch crate file: {0}")] + File(#[from] Box), + #[error("The requested version does not exist for the crate")] + NoVersion, +} + +impl Error { + pub fn status_code(&self) -> axum::http::StatusCode { + use axum::http::StatusCode; + + match self { + Self::Database(e) => e.status_code(), + Self::File(_) => StatusCode::INTERNAL_SERVER_ERROR, + Self::NoVersion => StatusCode::NOT_FOUND, + } } } + +define_error_response!(Error); diff --git a/chartered-web/src/endpoints/cargo_api/mod.rs b/chartered-web/src/endpoints/cargo_api/mod.rs index 1740db0..b6b7efb 100644 --- a/chartered-web/src/endpoints/cargo_api/mod.rs +++ a/chartered-web/src/endpoints/cargo_api/mod.rs @@ -1,3 +1,11 @@ +//! This module is used for anything called directly by cargo. The base URL for all the routes +//! listed in this module is `/a/:key/o/:organisation/api/v1`. +//! +//! Generally only endpoints listed in the [Web API section of the Cargo book][cargo-book] should +//! be implemented here. +//! +//! [cargo-book]: https://doc.rust-lang.org/cargo/reference/registries.html#web-api + mod download; mod owners; mod publish; @@ -12,6 +20,7 @@ use futures::future::Future; use std::convert::Infallible; +// requests are already authenticated before this router pub fn routes() -> Router< impl tower::Service< Request, diff --git a/chartered-web/src/endpoints/cargo_api/owners.rs b/chartered-web/src/endpoints/cargo_api/owners.rs index 35a51e4..b94a34c 100644 --- a/chartered-web/src/endpoints/cargo_api/owners.rs +++ a/chartered-web/src/endpoints/cargo_api/owners.rs @@ -1,37 +1,13 @@ +//! Called by `cargo owners` to get a list of owners of a particular crate. In our case, however, +//! an 'owner' is quite ambiguous as a _person_ isn't directly responsible for a crate, an +//! _organisation_ is. But for the sake of returning some sort of valuable data we'll just return +//! anyone with the `MANAGE_USERS` permission. + use axum::{extract, Json}; use chartered_db::{crates::Crate, users::User, ConnectionPool}; use serde::Serialize; use std::sync::Arc; use thiserror::Error; - -#[derive(Error, Debug)] -pub enum Error { - #[error("{0}")] - Database(#[from] chartered_db::Error), -} - -impl Error { - pub fn status_code(&self) -> axum::http::StatusCode { - match self { - Self::Database(e) => e.status_code(), - } - } -} - -define_error_response!(Error); - -#[derive(Serialize)] -pub struct GetResponse { - users: Vec, -} - -#[derive(Serialize)] -pub struct GetResponseUser { - // cargo spec says this should be an unsigned 32-bit integer - // uuid: chartered_db::uuid::Uuid, - login: String, - name: Option, -} pub async fn handle_get( extract::Path((_session_key, organisation, name)): extract::Path<(String, String, String)>, @@ -41,6 +17,7 @@ let crate_with_permissions = Arc::new(Crate::find_by_name(db.clone(), user.id, organisation, name).await?); + // grab all users with the `MANAGE_USERS` permission for the crate let users = crate_with_permissions .owners(db) .await? @@ -53,4 +30,33 @@ .collect(); Ok(Json(GetResponse { users })) +} + +#[derive(Serialize)] +pub struct GetResponse { + users: Vec, +} + +#[derive(Serialize)] +pub struct GetResponseUser { + // cargo spec says this should be an unsigned 32-bit integer + // uuid: chartered_db::uuid::Uuid, + login: String, + name: Option, +} + +#[derive(Error, Debug)] +pub enum Error { + #[error("{0}")] + Database(#[from] chartered_db::Error), +} + +impl Error { + pub fn status_code(&self) -> axum::http::StatusCode { + match self { + Self::Database(e) => e.status_code(), + } + } } + +define_error_response!(Error); diff --git a/chartered-web/src/endpoints/cargo_api/publish.rs b/chartered-web/src/endpoints/cargo_api/publish.rs index ef56d46..685c9ad 100644 --- a/chartered-web/src/endpoints/cargo_api/publish.rs +++ a/chartered-web/src/endpoints/cargo_api/publish.rs @@ -1,3 +1,8 @@ +//! Publishes a crate version +//! +//! This can also potentially create the crate under the given organisation if the crate doesn't +//! already exist and the user has the `CREATE_CRATE` permissions. + use axum::extract; use bytes::Bytes; use chartered_db::{crates::Crate, users::User, ConnectionPool}; @@ -8,64 +13,6 @@ use sha2::{Digest, Sha256}; use std::{borrow::Cow, convert::TryInto, sync::Arc}; use thiserror::Error; - -#[derive(Error, Debug)] -pub enum Error { - #[error("{0}")] - Database(#[from] chartered_db::Error), - #[error("Invalid JSON from client: {0}")] - JsonParse(#[from] serde_json::Error), - #[error("Invalid body")] - MetadataParse, - #[error("expected a valid crate name to start with a letter, contain only letters, numbers, hyphens, or underscores and have at most 64 characters ")] - InvalidCrateName, - #[error("Failed to push crate file to storage: {0}")] - File(#[from] Box), -} - -impl Error { - pub fn status_code(&self) -> axum::http::StatusCode { - use axum::http::StatusCode; - - match self { - Self::Database(e) => e.status_code(), - Self::JsonParse(_) | Self::MetadataParse | Self::InvalidCrateName => { - StatusCode::BAD_REQUEST - } - Self::File(_) => StatusCode::INTERNAL_SERVER_ERROR, - } - } -} - -define_error_response!(Error); - -#[derive(Serialize, Debug, Default)] -pub struct PublishCrateResponse { - warnings: PublishCrateResponseWarnings, -} - -#[derive(Serialize, Debug, Default)] -pub struct PublishCrateResponseWarnings { - invalid_categories: Vec, - invalid_badges: Vec, - other: Vec, -} - -fn validate_crate_name(name: &str) -> bool { - const MAX_NAME_LENGTH: usize = 64; - - let starts_with_alphabetic = name - .chars() - .next() - .map(|c| c.is_ascii_alphabetic()) - .unwrap_or_default(); - let is_alphanumeric = name - .chars() - .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_'); - let is_under_max_length = name.len() < MAX_NAME_LENGTH; - - starts_with_alphabetic && is_alphanumeric && is_under_max_length -} pub async fn handle( extract::Path((_session_key, organisation)): extract::Path<(String, String)>, @@ -74,13 +21,18 @@ extract::Extension(fs): extract::Extension>, body: Bytes, ) -> Result, Error> { + // cargo sends the crate metadata and the crate itself packed together, we'll parse these + // two separate bits of data out let (_, (metadata_bytes, crate_bytes)) = parse(body).map_err(|_| Error::MetadataParse)?; let metadata: Metadata<'_> = serde_json::from_slice(&metadata_bytes)?; + // validates the crate has a valid name, crates.io imposes some sane restrictions + // so we'll just use those if !validate_crate_name(&metadata.inner.name) { return Err(Error::InvalidCrateName); } + // looks up the crate, though we won't error on it just yet let crate_with_permissions = Crate::find_by_name( db.clone(), user.id, @@ -89,6 +41,8 @@ ) .await; + // if we failed to lookup the crate because it was missing, we'll create a new one fresh + // if we have the permissions, that is. `Crate::create` will check those for us let crate_with_permissions = match crate_with_permissions { Ok(v) => Arc::new(v), Err(chartered_db::Error::MissingCrate) => { @@ -104,10 +58,14 @@ Err(e) => return Err(e.into()), }; + // take a checksum of the crate to write to the database to ensure integrity let checksum = hex::encode(Sha256::digest(&crate_bytes)); + // writes the file to the filesystem and takes a `FileReference` we can store in the + // db to.. reference this file when it's needed (ie. on download) let file_ref = fs.write(crate_bytes).await.map_err(Box::new)?; + // and finally, publish the version! crate_with_permissions .publish_version( db, @@ -123,6 +81,9 @@ Ok(axum::response::Json(PublishCrateResponse::default())) } +/// Cargo sends the metadata and crate packed together and prepended with a single `u32` +/// representing how many bytes are in the next section, we'll parse these two byte chunks +/// out and return them `(Metadata, Crate)`. fn parse(body: impl Into) -> nom::IResult { use nom::{bytes::complete::take, combinator::map_res}; use std::array::TryFromSliceError; @@ -139,8 +100,29 @@ let (rest, crate_bytes) = take(crate_length)(rest)?; Ok((rest, (metadata_bytes.into(), crate_bytes.into()))) +} + +/// Most of these limitations are copied from crates.io as they're rather sane defaults, +/// though not all of them are implemented yet, a non-comprehensive list is available [here][list] +/// +/// [list]: https://doc.rust-lang.org/cargo/reference/manifest.html#the-name-field +fn validate_crate_name(name: &str) -> bool { + const MAX_NAME_LENGTH: usize = 64; + + let starts_with_alphabetic = name + .chars() + .next() + .map(|c| c.is_ascii_alphabetic()) + .unwrap_or_default(); + let is_alphanumeric = name + .chars() + .all(|c| c.is_ascii_alphanumeric() || c == '-' || c == '_'); + let is_under_max_length = name.len() < MAX_NAME_LENGTH; + + starts_with_alphabetic && is_alphanumeric && is_under_max_length } +/// Some metadata about the crate, sent to us by the user's `cargo` CLI #[allow(dead_code)] // a lot of these need checking/validating #[derive(Deserialize, Debug)] pub struct Metadata<'a> { @@ -162,6 +144,7 @@ inner: MetadataCrateVersion<'a>, } +/// Some specific metadata about the crate version currently being pushed to us #[derive(Deserialize, Debug)] pub struct MetadataCrateVersion<'a> { #[serde(borrow)] @@ -227,6 +210,48 @@ kind: Cow::Owned(us.kind.into_owned()), registry: us.registry.map(|v| Cow::Owned(v.into_owned())), package: package.map(Cow::Owned), + } + } +} + +#[derive(Serialize, Debug, Default)] +pub struct PublishCrateResponse { + warnings: PublishCrateResponseWarnings, +} + +#[derive(Serialize, Debug, Default)] +pub struct PublishCrateResponseWarnings { + invalid_categories: Vec, + invalid_badges: Vec, + other: Vec, +} + +#[derive(Error, Debug)] +pub enum Error { + #[error("{0}")] + Database(#[from] chartered_db::Error), + #[error("Invalid JSON from client: {0}")] + JsonParse(#[from] serde_json::Error), + #[error("Invalid body")] + MetadataParse, + #[error("expected a valid crate name to start with a letter, contain only letters, numbers, hyphens, or underscores and have at most 64 characters ")] + InvalidCrateName, + #[error("Failed to push crate file to storage: {0}")] + File(#[from] Box), +} + +impl Error { + pub fn status_code(&self) -> axum::http::StatusCode { + use axum::http::StatusCode; + + match self { + Self::Database(e) => e.status_code(), + Self::JsonParse(_) | Self::MetadataParse | Self::InvalidCrateName => { + StatusCode::BAD_REQUEST + } + Self::File(_) => StatusCode::INTERNAL_SERVER_ERROR, } } } + +define_error_response!(Error); diff --git a/chartered-web/src/endpoints/cargo_api/yank.rs b/chartered-web/src/endpoints/cargo_api/yank.rs index 869dd08..ba141a2 100644 --- a/chartered-web/src/endpoints/cargo_api/yank.rs +++ a/chartered-web/src/endpoints/cargo_api/yank.rs @@ -1,29 +1,13 @@ +//! Handles yanking and unyanking a crate version for those with the `YANK_CRATE` and `UNYANK_CRATE` +//! permissions. +//! +//! If a crate is yanked, cargo will refuse to download it. + use axum::{extract, Json}; use chartered_db::{crates::Crate, users::User, ConnectionPool}; use serde::Serialize; use std::sync::Arc; use thiserror::Error; - -#[derive(Error, Debug)] -pub enum Error { - #[error("{0}")] - Database(#[from] chartered_db::Error), -} - -impl Error { - pub fn status_code(&self) -> axum::http::StatusCode { - match self { - Self::Database(e) => e.status_code(), - } - } -} - -define_error_response!(Error); - -#[derive(Serialize)] -pub struct Response { - ok: bool, -} pub async fn handle_yank( extract::Path((_session_key, organisation, name, version)): extract::Path<( @@ -63,4 +47,25 @@ .await?; Ok(Json(Response { ok: true })) +} + +#[derive(Serialize)] +pub struct Response { + ok: bool, +} + +#[derive(Error, Debug)] +pub enum Error { + #[error("{0}")] + Database(#[from] chartered_db::Error), +} + +impl Error { + pub fn status_code(&self) -> axum::http::StatusCode { + match self { + Self::Database(e) => e.status_code(), + } + } } + +define_error_response!(Error); diff --git a/chartered-web/src/endpoints/web_api/ssh_key.rs b/chartered-web/src/endpoints/web_api/ssh_key.rs index 5eb3855..e67b9b9 100644 --- a/chartered-web/src/endpoints/web_api/ssh_key.rs +++ a/chartered-web/src/endpoints/web_api/ssh_key.rs @@ -1,3 +1,6 @@ +//! Handles CRD of SSH keys for the requesting user, these are not updatable as SSH keys are +//! immutable. + use chartered_db::{users::User, ConnectionPool}; use axum::{extract, Json}; @@ -9,20 +12,6 @@ use tracing::warn; use crate::endpoints::ErrorResponse; - -#[derive(Serialize)] -pub struct GetResponse { - keys: Vec, -} - -#[derive(Serialize)] -pub struct GetResponseKey { - uuid: Uuid, - name: String, - fingerprint: String, - created_at: DateTime, - last_used_at: Option>, -} pub async fn handle_get( extract::Extension(db): extract::Extension, @@ -47,11 +36,6 @@ .collect(); Ok(Json(GetResponse { keys })) -} - -#[derive(Deserialize)] -pub struct PutRequest { - key: String, } pub async fn handle_put( @@ -78,6 +62,25 @@ } else { Err(Error::NonExistentKey) } +} + +#[derive(Serialize)] +pub struct GetResponse { + keys: Vec, +} + +#[derive(Serialize)] +pub struct GetResponseKey { + uuid: Uuid, + name: String, + fingerprint: String, + created_at: DateTime, + last_used_at: Option>, +} + +#[derive(Deserialize)] +pub struct PutRequest { + key: String, } #[derive(Error, Debug)] diff --git a/chartered-web/src/endpoints/web_api/auth/logout.rs b/chartered-web/src/endpoints/web_api/auth/logout.rs index 85d0526..5185fe1 100644 --- a/chartered-web/src/endpoints/web_api/auth/logout.rs +++ a/chartered-web/src/endpoints/web_api/auth/logout.rs @@ -1,3 +1,6 @@ +//! Logs a user out of the session used to send this request and deletes it from the +//! database. + use axum::{extract, Json}; use chartered_db::users::UserSession; use chartered_db::ConnectionPool; diff --git a/chartered-web/src/endpoints/web_api/auth/mod.rs b/chartered-web/src/endpoints/web_api/auth/mod.rs index 094ecbb..0e6063e 100644 --- a/chartered-web/src/endpoints/web_api/auth/mod.rs +++ a/chartered-web/src/endpoints/web_api/auth/mod.rs @@ -54,6 +54,8 @@ picture_url: Option, } +/// Takes the given `User` and generates a session for it and returns a response containing an API +/// key to the frontend that it can save for further request pub async fn login( db: ConnectionPool, user: User, diff --git a/chartered-web/src/endpoints/web_api/auth/openid.rs b/chartered-web/src/endpoints/web_api/auth/openid.rs index 47cba46..6d012d1 100644 --- a/chartered-web/src/endpoints/web_api/auth/openid.rs +++ a/chartered-web/src/endpoints/web_api/auth/openid.rs @@ -1,3 +1,7 @@ +//! Methods for `OpenID` Connect authentication, we allow the frontend to list all the available and +//! enabled providers so they can show them to the frontend and provide methods for actually doing +//! the authentication. + use crate::config::{Config, OidcClients}; use axum::{extract, Json}; use chacha20poly1305::{ @@ -11,12 +15,8 @@ use thiserror::Error; pub type Nonce = [u8; 16]; - -#[derive(Serialize)] -pub struct ListProvidersResponse { - providers: Vec, -} +/// Lists all the available and enabled providers that the user can authenticate with. #[allow(clippy::unused_async)] pub async fn list_providers( extract::Extension(oidc_clients): extract::Extension>, @@ -28,19 +28,11 @@ .map(std::string::ToString::to_string) .collect(), }) -} - -#[derive(Serialize, Deserialize, Debug)] -pub struct State { - provider: String, - nonce: Nonce, } -#[derive(Serialize)] -pub struct BeginResponse { - redirect_url: String, -} - +/// Starts the authentication process, generating an encrypted state so we can validate the +/// request came from us and returning back the URL the frontend should redirect the user for +/// authenticating with the provider. #[allow(clippy::unused_async)] pub async fn begin_oidc( extract::Path(provider): extract::Path, @@ -64,17 +56,10 @@ Ok(Json(BeginResponse { redirect_url: auth_url.to_string(), })) -} - -#[allow(dead_code)] -#[derive(Deserialize)] -pub struct CompleteOidcParams { - state: String, - code: String, - scope: Option, - prompt: Option, } +/// Handles the response back from the OIDC provider, checking the state came from us, validating +/// the token with the provider themselves and then finally logging the user in. pub async fn complete_oidc( extract::Query(params): extract::Query, extract::Extension(config): extract::Extension>, @@ -83,8 +68,11 @@ user_agent: Option>, addr: extract::ConnectInfo, ) -> Result, Error> { + // decrypt the state that we created in `begin_oidc` and parse it as json let state: State = serde_json::from_slice(&decrypt_url_safe(¶ms.state, &config)?)?; + // check the state for the provider so we can get the right OIDC client to + // verify the code and grab an id_token let client = oidc_clients .get(&state.provider) .ok_or(Error::UnknownOauthProvider)?; @@ -92,18 +80,26 @@ let mut token: Token = client.request_token(¶ms.code).await?.into(); if let Some(id_token) = token.id_token.as_mut() { + // ensure the id_token is valid, checking `exp`, etc. client.decode_token(id_token)?; + // ensure the nonce in the returned id_token is the same as the one we sent out encrypted + // with the original request let nonce = base64::encode_config(state.nonce, base64::URL_SAFE_NO_PAD); client.validate_token(id_token, Some(nonce.as_str()), None)?; } else { + // the provider didn't send us back a id_token return Err(Error::MissingToken); } + // get some basic info from the provider using the claims we requested in `begin_oidc` let userinfo = client.request_userinfo(&token).await?; let user = User::find_or_create( db.clone(), + // we're using `provider:uid` as the format for OIDC logins, this is fine to create + // without a password because (1) password auth rejects blank passwords and (2) password + // auth also rejects any usernames with a `:` in. format!("{}:{}", state.provider, userinfo.sub.unwrap()), userinfo.name, userinfo.nickname, @@ -113,11 +109,14 @@ ) .await?; + // request looks good, log the user in! Ok(Json(super::login(db, user, user_agent, addr).await?)) } const NONCE_LEN: usize = 12; +// Encrypts the given string using ChaCha20Poly1305 and returns a url safe base64 encoded +// version of it fn encrypt_url_safe(input: &[u8], config: &Config) -> Result { let cipher = ChaCha20Poly1305::new(&config.encryption_key); @@ -130,6 +129,7 @@ Ok(base64::encode_config(&ciphertext, base64::URL_SAFE_NO_PAD)) } +// Decrypts the given string assuming it's a url safe base64 encoded ChaCha20Poly1305 cipher. fn decrypt_url_safe(input: &str, config: &Config) -> Result, Error> { let cipher = ChaCha20Poly1305::new(&config.encryption_key); @@ -140,6 +140,31 @@ cipher .decrypt(ciphertext_nonce, ciphertext.as_ref()) .map_err(Error::from) +} + +#[derive(Serialize)] +pub struct ListProvidersResponse { + providers: Vec, +} + +#[derive(Serialize)] +pub struct BeginResponse { + redirect_url: String, +} + +#[allow(dead_code)] +#[derive(Deserialize)] +pub struct CompleteOidcParams { + state: String, + code: String, + scope: Option, + prompt: Option, +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct State { + provider: String, + nonce: Nonce, } #[derive(Error, Debug)] diff --git a/chartered-web/src/endpoints/web_api/auth/password.rs b/chartered-web/src/endpoints/web_api/auth/password.rs index 1a57907..bb7dcf2 100644 --- a/chartered-web/src/endpoints/web_api/auth/password.rs +++ a/chartered-web/src/endpoints/web_api/auth/password.rs @@ -1,28 +1,9 @@ +//! Password-based authentication, including registration and login. + use axum::{extract, Json}; use chartered_db::{users::User, ConnectionPool}; use serde::Deserialize; use thiserror::Error; - -#[derive(Error, Debug)] -pub enum Error { - #[error("Failed to query database")] - Database(#[from] chartered_db::Error), - #[error("Invalid username/password")] - UnknownUser, -} - -impl Error { - pub fn status_code(&self) -> axum::http::StatusCode { - use axum::http::StatusCode; - - match self { - Self::Database(_) => StatusCode::INTERNAL_SERVER_ERROR, - Self::UnknownUser => StatusCode::FORBIDDEN, - } - } -} - -define_error_response!(Error); pub async fn handle( extract::Extension(db): extract::Extension, @@ -48,4 +29,25 @@ pub struct Request { username: String, password: String, +} + +#[derive(Error, Debug)] +pub enum Error { + #[error("Failed to query database")] + Database(#[from] chartered_db::Error), + #[error("Invalid username/password")] + UnknownUser, +} + +impl Error { + pub fn status_code(&self) -> axum::http::StatusCode { + use axum::http::StatusCode; + + match self { + Self::Database(_) => StatusCode::INTERNAL_SERVER_ERROR, + Self::UnknownUser => StatusCode::FORBIDDEN, + } + } } + +define_error_response!(Error); diff --git a/chartered-web/src/endpoints/web_api/crates/info.rs b/chartered-web/src/endpoints/web_api/crates/info.rs index 8addec3..acbe79a 100644 --- a/chartered-web/src/endpoints/web_api/crates/info.rs +++ a/chartered-web/src/endpoints/web_api/crates/info.rs @@ -1,3 +1,10 @@ +//! Grabs some info about the crate that can be shown in the web UI, like READMEs, descriptions, +//! versions, etc. We group them all together into a single response as we can fetch them in +//! a single query and the users don't have to make multiple calls out. +//! +//! Unlike crates.io, we're only keeping the _latest_ README pushed to the crate, so there's no +//! need to have version-specific info responses - we'll just send an overview of each one. + use axum::{body::Full, extract, response::IntoResponse, Json}; use bytes::Bytes; use chartered_db::{crates::Crate, users::User, ConnectionPool}; @@ -6,23 +13,7 @@ use serde::Serialize; use std::sync::Arc; use thiserror::Error; - -#[derive(Error, Debug)] -pub enum Error { - #[error("{0}")] - Database(#[from] chartered_db::Error), -} - -impl Error { - pub fn status_code(&self) -> axum::http::StatusCode { - match self { - Self::Database(e) => e.status_code(), - } - } -} -define_error_response!(Error); - pub async fn handle( extract::Path((_session_key, organisation, name)): extract::Path<(String, String, String)>, extract::Extension(db): extract::Extension, @@ -31,16 +22,12 @@ let crate_with_permissions = Arc::new(Crate::find_by_name(db.clone(), user.id, organisation, name).await?); + // grab all versions of this crate and the person who uploaded them let versions = crate_with_permissions .clone() .versions_with_uploader(db) .await?; - // returning a Response instead of Json here so we don't have to close - // every Crate/CrateVersion etc, would be easier if we just had an owned - // version of each but we're using `spawn_blocking` in chartered-db for - // diesel which requires `'static' which basically forces us to use Arc - // if we want to keep a reference to anything ourselves. Ok(Json(Response { info: (&crate_with_permissions.crate_).into(), versions: versions @@ -57,6 +44,11 @@ }) .collect(), }) + // returning a Response instead of Json here so we don't have to clone + // every Crate/CrateVersion etc, would be easier if we just had an owned + // version of each but we're using `spawn_blocking` in chartered-db for + // diesel which requires `'static' which basically forces us to use Arc + // if we want to keep a reference to anything ourselves. .into_response()) } @@ -102,6 +94,22 @@ repository: crate_.repository.as_deref(), homepage: crate_.homepage.as_deref(), documentation: crate_.documentation.as_deref(), + } + } +} + +#[derive(Error, Debug)] +pub enum Error { + #[error("{0}")] + Database(#[from] chartered_db::Error), +} + +impl Error { + pub fn status_code(&self) -> axum::http::StatusCode { + match self { + Self::Database(e) => e.status_code(), } } } + +define_error_response!(Error); diff --git a/chartered-web/src/endpoints/web_api/crates/members.rs b/chartered-web/src/endpoints/web_api/crates/members.rs index 65a31ea..3f3dc09 100644 --- a/chartered-web/src/endpoints/web_api/crates/members.rs +++ a/chartered-web/src/endpoints/web_api/crates/members.rs @@ -1,3 +1,7 @@ +//! Handles crate-level member overrides that can add permissions on top of organisations. +//! +//! This is essentially a CRUD controller, nice and easy one. + use axum::{extract, Json}; use chartered_db::{ crates::Crate, permissions::UserPermission, users::User, uuid::Uuid, ConnectionPool, @@ -7,21 +11,10 @@ use thiserror::Error; use crate::endpoints::ErrorResponse; - -#[derive(Serialize)] -pub struct GetResponse { - allowed_permissions: &'static [&'static str], - members: Vec, -} - -#[derive(Deserialize, Serialize)] -pub struct GetResponseMember { - uuid: Uuid, - display_name: String, - picture_url: Option, - permissions: UserPermission, -} +/// Lists all crate-level members and the permissions they have. +/// +/// These members could be specific to the crate or they could be overrides ontop of the org. pub async fn handle_get( extract::Path((_session_key, organisation, name)): extract::Path<(String, String, String)>, extract::Extension(db): extract::Extension, @@ -46,14 +39,9 @@ allowed_permissions: UserPermission::names(), members, })) -} - -#[derive(Deserialize)] -pub struct PutOrPatchRequest { - user_uuid: chartered_db::uuid::Uuid, - permissions: UserPermission, } +/// Updates a crate member's permissions pub async fn handle_patch( extract::Path((_session_key, organisation, name)): extract::Path<(String, String, String)>, extract::Extension(db): extract::Extension, @@ -77,6 +65,7 @@ Ok(Json(ErrorResponse { error: None })) } +/// Inserts an permissions override for this crate for a specific user pub async fn handle_put( extract::Path((_session_key, organisation, name)): extract::Path<(String, String, String)>, extract::Extension(db): extract::Extension, @@ -95,13 +84,9 @@ .await?; Ok(Json(ErrorResponse { error: None })) -} - -#[derive(Deserialize)] -pub struct DeleteRequest { - user_uuid: chartered_db::uuid::Uuid, } +/// Deletes a member override from this crate pub async fn handle_delete( extract::Path((_session_key, organisation, name)): extract::Path<(String, String, String)>, extract::Extension(db): extract::Extension, @@ -120,6 +105,31 @@ .await?; Ok(Json(ErrorResponse { error: None })) +} + +#[derive(Serialize)] +pub struct GetResponse { + allowed_permissions: &'static [&'static str], + members: Vec, +} + +#[derive(Deserialize, Serialize)] +pub struct GetResponseMember { + uuid: Uuid, + display_name: String, + picture_url: Option, + permissions: UserPermission, +} + +#[derive(Deserialize)] +pub struct PutOrPatchRequest { + user_uuid: chartered_db::uuid::Uuid, + permissions: UserPermission, +} + +#[derive(Deserialize)] +pub struct DeleteRequest { + user_uuid: chartered_db::uuid::Uuid, } #[derive(Error, Debug)] diff --git a/chartered-web/src/endpoints/web_api/crates/most_downloaded.rs b/chartered-web/src/endpoints/web_api/crates/most_downloaded.rs index 623a11d..f19df1f 100644 --- a/chartered-web/src/endpoints/web_api/crates/most_downloaded.rs +++ a/chartered-web/src/endpoints/web_api/crates/most_downloaded.rs @@ -1,24 +1,10 @@ +//! Returns a list of the top 10 most downloaded crates that the user has access to. + use axum::{extract, Json}; use chartered_db::{crates::Crate, users::User, ConnectionPool}; use serde::Serialize; use std::sync::Arc; use thiserror::Error; - -#[derive(Error, Debug)] -pub enum Error { - #[error("{0}")] - Database(#[from] chartered_db::Error), -} - -impl Error { - pub fn status_code(&self) -> axum::http::StatusCode { - match self { - Self::Database(e) => e.status_code(), - } - } -} - -define_error_response!(Error); pub async fn handle( extract::Extension(db): extract::Extension, @@ -48,4 +34,20 @@ name: String, downloads: i32, organisation: String, +} + +#[derive(Error, Debug)] +pub enum Error { + #[error("{0}")] + Database(#[from] chartered_db::Error), +} + +impl Error { + pub fn status_code(&self) -> axum::http::StatusCode { + match self { + Self::Database(e) => e.status_code(), + } + } } + +define_error_response!(Error); diff --git a/chartered-web/src/endpoints/web_api/crates/recently_created.rs b/chartered-web/src/endpoints/web_api/crates/recently_created.rs index 45942e9..c2d8520 100644 --- a/chartered-web/src/endpoints/web_api/crates/recently_created.rs +++ a/chartered-web/src/endpoints/web_api/crates/recently_created.rs @@ -1,25 +1,12 @@ +//! Returns a list to the 10 most recently created crates that the user has access to for their +//! viewing pleasure + use axum::{extract, Json}; use chartered_db::{crates::Crate, users::User, ConnectionPool}; use chrono::{DateTime, TimeZone, Utc}; use serde::Serialize; use std::sync::Arc; use thiserror::Error; - -#[derive(Error, Debug)] -pub enum Error { - #[error("{0}")] - Database(#[from] chartered_db::Error), -} - -impl Error { - pub fn status_code(&self) -> axum::http::StatusCode { - match self { - Self::Database(e) => e.status_code(), - } - } -} - -define_error_response!(Error); pub async fn handle( extract::Extension(db): extract::Extension, @@ -49,4 +36,20 @@ name: String, created_at: DateTime, organisation: String, +} + +#[derive(Error, Debug)] +pub enum Error { + #[error("{0}")] + Database(#[from] chartered_db::Error), +} + +impl Error { + pub fn status_code(&self) -> axum::http::StatusCode { + match self { + Self::Database(e) => e.status_code(), + } + } } + +define_error_response!(Error); diff --git a/chartered-web/src/endpoints/web_api/crates/recently_updated.rs b/chartered-web/src/endpoints/web_api/crates/recently_updated.rs index c2cec7c..beba1c2 100644 --- a/chartered-web/src/endpoints/web_api/crates/recently_updated.rs +++ a/chartered-web/src/endpoints/web_api/crates/recently_updated.rs @@ -1,24 +1,10 @@ +//! Lists the 10 most recently updated crates that the user has access to. + use axum::{extract, Json}; use chartered_db::{crates::Crate, users::User, ConnectionPool}; use serde::Serialize; use std::sync::Arc; use thiserror::Error; - -#[derive(Error, Debug)] -pub enum Error { - #[error("{0}")] - Database(#[from] chartered_db::Error), -} - -impl Error { - pub fn status_code(&self) -> axum::http::StatusCode { - match self { - Self::Database(e) => e.status_code(), - } - } -} - -define_error_response!(Error); pub async fn handle( extract::Extension(db): extract::Extension, @@ -48,4 +34,20 @@ name: String, version: String, organisation: String, +} + +#[derive(Error, Debug)] +pub enum Error { + #[error("{0}")] + Database(#[from] chartered_db::Error), +} + +impl Error { + pub fn status_code(&self) -> axum::http::StatusCode { + match self { + Self::Database(e) => e.status_code(), + } + } } + +define_error_response!(Error); diff --git a/chartered-web/src/endpoints/web_api/crates/search.rs b/chartered-web/src/endpoints/web_api/crates/search.rs index 9abb3d1..6bbb5e5 100644 --- a/chartered-web/src/endpoints/web_api/crates/search.rs +++ a/chartered-web/src/endpoints/web_api/crates/search.rs @@ -1,29 +1,12 @@ +//! Does a simple search over the crates table for a search term, the organisation and crate name +//! are concatenated using a `/` so any substring of `org/crate` will return results. The latest +//! version for each is also fetched so we can show them in the search results. + use axum::{extract, Json}; use chartered_db::{crates::Crate, permissions::UserPermission, users::User, ConnectionPool}; use serde::{Deserialize, Serialize}; use std::sync::Arc; use thiserror::Error; - -#[derive(Deserialize)] -pub struct RequestParams { - q: String, -} - -#[derive(Serialize)] -pub struct Response { - crates: Vec, -} - -#[derive(Serialize)] -pub struct ResponseCrate { - organisation: String, - name: String, - description: Option, - version: String, - homepage: Option, - repository: Option, - permissions: UserPermission, -} pub async fn handle( extract::Extension(db): extract::Extension, @@ -63,6 +46,27 @@ .await?; Ok(Json(Response { crates })) +} + +#[derive(Deserialize)] +pub struct RequestParams { + q: String, +} + +#[derive(Serialize)] +pub struct Response { + crates: Vec, +} + +#[derive(Serialize)] +pub struct ResponseCrate { + organisation: String, + name: String, + description: Option, + version: String, + homepage: Option, + repository: Option, + permissions: UserPermission, } #[derive(Error, Debug)] diff --git a/chartered-web/src/endpoints/web_api/organisations/crud.rs b/chartered-web/src/endpoints/web_api/organisations/crud.rs index 7f91de2..e05febe 100644 --- a/chartered-web/src/endpoints/web_api/organisations/crud.rs +++ a/chartered-web/src/endpoints/web_api/organisations/crud.rs @@ -1,3 +1,6 @@ +//! Allows users to create whole organisations. This endpoint currently isn't limited to any +//! specific users so all users can create an organisation and add people to it. + use axum::{extract, Json}; use chartered_db::{organisations::Organisation, users::User, ConnectionPool}; use serde::Deserialize; @@ -5,6 +8,23 @@ use thiserror::Error; use crate::endpoints::ErrorResponse; + +pub async fn handle_put( + extract::Extension(db): extract::Extension, + extract::Extension(user): extract::Extension>, + extract::Json(req): extract::Json, +) -> Result, Error> { + Organisation::create(db, req.name, req.description, req.public, user.id).await?; + + Ok(Json(ErrorResponse { error: None })) +} + +#[derive(Deserialize)] +pub struct PutRequest { + name: String, + description: String, + public: bool, +} #[derive(Error, Debug)] pub enum Error { @@ -21,20 +41,3 @@ } define_error_response!(Error); - -#[derive(Deserialize)] -pub struct PutRequest { - name: String, - description: String, - public: bool, -} - -pub async fn handle_put( - extract::Extension(db): extract::Extension, - extract::Extension(user): extract::Extension>, - extract::Json(req): extract::Json, -) -> Result, Error> { - Organisation::create(db, req.name, req.description, req.public, user.id).await?; - - Ok(Json(ErrorResponse { error: None })) -} diff --git a/chartered-web/src/endpoints/web_api/organisations/info.rs b/chartered-web/src/endpoints/web_api/organisations/info.rs index d1015a8..8cc54a6 100644 --- a/chartered-web/src/endpoints/web_api/organisations/info.rs +++ a/chartered-web/src/endpoints/web_api/organisations/info.rs @@ -1,3 +1,7 @@ +//! Grabs info about a specific organisation including name, description, and a list of members +//! and a list of permissions that are allowed to be applied to others by the requesting user and +//! also all the crates that belong to the organisation. + use axum::{extract, Json}; use chartered_db::{ organisations::Organisation, permissions::UserPermission, users::User, ConnectionPool, @@ -5,22 +9,6 @@ use serde::Serialize; use std::sync::Arc; use thiserror::Error; - -#[derive(Error, Debug)] -pub enum Error { - #[error("{0}")] - Database(#[from] chartered_db::Error), -} - -impl Error { - pub fn status_code(&self) -> axum::http::StatusCode { - match self { - Self::Database(e) => e.status_code(), - } - } -} - -define_error_response!(Error); pub async fn handle_get( extract::Path((_session_key, organisation)): extract::Path<(String, String)>, @@ -30,10 +18,13 @@ let organisation = Arc::new(Organisation::find_by_name(db.clone(), user.id, organisation).await?); + // checks if the requesting user has the `MANGE_USERS` permission for this + // organisation let can_manage_users = organisation .permissions() .contains(UserPermission::MANAGE_USERS); + // fetch both crates and members for the organisation at the same time let (crates, users) = tokio::try_join!( organisation.clone().crates(db.clone()), organisation.clone().members(db), @@ -41,6 +32,7 @@ Ok(Json(Response { description: organisation.organisation().description.to_string(), + // all the permissions the requesting user can give out for this organisation possible_permissions: can_manage_users.then(UserPermission::all), crates: crates .into_iter() @@ -81,4 +73,20 @@ display_name: String, picture_url: Option, permissions: Option, +} + +#[derive(Error, Debug)] +pub enum Error { + #[error("{0}")] + Database(#[from] chartered_db::Error), +} + +impl Error { + pub fn status_code(&self) -> axum::http::StatusCode { + match self { + Self::Database(e) => e.status_code(), + } + } } + +define_error_response!(Error); diff --git a/chartered-web/src/endpoints/web_api/organisations/list.rs b/chartered-web/src/endpoints/web_api/organisations/list.rs index 480f122..ddc0922 100644 --- a/chartered-web/src/endpoints/web_api/organisations/list.rs +++ a/chartered-web/src/endpoints/web_api/organisations/list.rs @@ -1,24 +1,11 @@ +//! Lists all the organisations that the requesting user is apart of returning their names and +//! descriptions. + use axum::{extract, Json}; use chartered_db::{organisations::Organisation, users::User, ConnectionPool}; use serde::Serialize; use std::sync::Arc; use thiserror::Error; - -#[derive(Error, Debug)] -pub enum Error { - #[error("{0}")] - Database(#[from] chartered_db::Error), -} - -impl Error { - pub fn status_code(&self) -> axum::http::StatusCode { - match self { - Self::Database(e) => e.status_code(), - } - } -} - -define_error_response!(Error); pub async fn handle_get( extract::Extension(db): extract::Extension, @@ -46,4 +33,20 @@ pub struct ResponseOrganisation { name: String, description: String, +} + +#[derive(Error, Debug)] +pub enum Error { + #[error("{0}")] + Database(#[from] chartered_db::Error), +} + +impl Error { + pub fn status_code(&self) -> axum::http::StatusCode { + match self { + Self::Database(e) => e.status_code(), + } + } } + +define_error_response!(Error); diff --git a/chartered-web/src/endpoints/web_api/organisations/members.rs b/chartered-web/src/endpoints/web_api/organisations/members.rs index 2262a17..60c4b78 100644 --- a/chartered-web/src/endpoints/web_api/organisations/members.rs +++ a/chartered-web/src/endpoints/web_api/organisations/members.rs @@ -1,3 +1,6 @@ +//! CRUD methods to manage members of an organisation, given the requesting user has the +//! `MANAGE_USERS` permission at the organisation level. + use axum::{extract, Json}; use chartered_db::{ organisations::Organisation, permissions::UserPermission, users::User, ConnectionPool, @@ -7,13 +10,8 @@ use thiserror::Error; use crate::endpoints::ErrorResponse; - -#[derive(Deserialize)] -pub struct PutOrPatchRequest { - user_uuid: chartered_db::uuid::Uuid, - permissions: UserPermission, -} +/// Updates an organisation member's permissions pub async fn handle_patch( extract::Path((_session_key, organisation)): extract::Path<(String, String)>, extract::Extension(db): extract::Extension, @@ -37,6 +35,7 @@ Ok(Json(ErrorResponse { error: None })) } +/// Adds a new member to the organisation with a given set of permissions. pub async fn handle_put( extract::Path((_session_key, organisation)): extract::Path<(String, String)>, extract::Extension(db): extract::Extension, @@ -55,13 +54,9 @@ .await?; Ok(Json(ErrorResponse { error: None })) -} - -#[derive(Deserialize)] -pub struct DeleteRequest { - user_uuid: chartered_db::uuid::Uuid, } +/// Deletes a member from the organisation entirely pub async fn handle_delete( extract::Path((_session_key, organisation)): extract::Path<(String, String)>, extract::Extension(db): extract::Extension, @@ -78,6 +73,17 @@ organisation.delete_member(db, action_user.id).await?; Ok(Json(ErrorResponse { error: None })) +} + +#[derive(Deserialize)] +pub struct PutOrPatchRequest { + user_uuid: chartered_db::uuid::Uuid, + permissions: UserPermission, +} + +#[derive(Deserialize)] +pub struct DeleteRequest { + user_uuid: chartered_db::uuid::Uuid, } #[derive(Error, Debug)] diff --git a/chartered-web/src/endpoints/web_api/users/info.rs b/chartered-web/src/endpoints/web_api/users/info.rs index 3349b5f..9fd6658 100644 --- a/chartered-web/src/endpoints/web_api/users/info.rs +++ a/chartered-web/src/endpoints/web_api/users/info.rs @@ -1,7 +1,21 @@ +//! Returns a user profile, with some customisable information and some fixed information (such +//! as `alias`) which can be used to uniquely identify a user by another user. +//! +//! Users don't need to be in a common organisation to be able to see each other's profiles. + use axum::{extract, Json}; use chartered_db::{users::User, ConnectionPool}; use serde::Serialize; use thiserror::Error; + +pub async fn handle( + extract::Path((_session_key, uuid)): extract::Path<(String, chartered_db::uuid::Uuid)>, + extract::Extension(db): extract::Extension, +) -> Result, Error> { + let user = User::find_by_uuid(db, uuid).await?.ok_or(Error::NotFound)?; + + Ok(Json(user.into())) +} #[derive(Serialize)] pub struct Response { @@ -26,15 +40,6 @@ picture_url: user.picture_url, } } -} - -pub async fn handle( - extract::Path((_session_key, uuid)): extract::Path<(String, chartered_db::uuid::Uuid)>, - extract::Extension(db): extract::Extension, -) -> Result, Error> { - let user = User::find_by_uuid(db, uuid).await?.ok_or(Error::NotFound)?; - - Ok(Json(user.into())) } #[derive(Error, Debug)] diff --git a/chartered-web/src/endpoints/web_api/users/search.rs b/chartered-web/src/endpoints/web_api/users/search.rs index a940f38..bfef7f6 100644 --- a/chartered-web/src/endpoints/web_api/users/search.rs +++ a/chartered-web/src/endpoints/web_api/users/search.rs @@ -1,24 +1,13 @@ +//! Searches through all users for the given search terms, matching on either full name, username +//! or nickname. This is used on the overall search form and also when adding members to either +//! an organisation or a crate. +//! +//! Users are not restricted in what other users they can see. + use axum::{extract, Json}; use chartered_db::{users::User, ConnectionPool}; use serde::{Deserialize, Serialize}; use thiserror::Error; - -#[derive(Deserialize)] -pub struct RequestParams { - q: String, -} - -#[derive(Serialize)] -pub struct Response { - users: Vec, -} - -#[derive(Serialize)] -pub struct ResponseUser { - user_uuid: chartered_db::uuid::Uuid, - display_name: String, - picture_url: Option, -} pub async fn handle( extract::Extension(db): extract::Extension, @@ -35,6 +24,23 @@ .collect(); Ok(Json(Response { users })) +} + +#[derive(Deserialize)] +pub struct RequestParams { + q: String, +} + +#[derive(Serialize)] +pub struct Response { + users: Vec, +} + +#[derive(Serialize)] +pub struct ResponseUser { + user_uuid: chartered_db::uuid::Uuid, + display_name: String, + picture_url: Option, } #[derive(Error, Debug)] -- rgit 0.1.3