diff options
| author | mrw1593 <botahamec@outlook.com> | 2023-05-28 16:31:22 -0400 |
|---|---|---|
| committer | mrw1593 <botahamec@outlook.com> | 2023-05-29 10:51:10 -0400 |
| commit | 614c81c0f239940acb313e067dafc3213f399b10 (patch) | |
| tree | 68835a73c225a3b4fefa590b173db1cd9d7a28b2 /src/api | |
| parent | e048d7d050f87e9e5bf06f01e39fd417d6868c7e (diff) | |
Add clients to the API
Diffstat (limited to 'src/api')
| -rw-r--r-- | src/api/clients.rs | 311 | ||||
| -rw-r--r-- | src/api/mod.rs | 3 | ||||
| -rw-r--r-- | src/api/oauth.rs | 24 | ||||
| -rw-r--r-- | src/api/users.rs | 31 |
4 files changed, 352 insertions, 17 deletions
diff --git a/src/api/clients.rs b/src/api/clients.rs new file mode 100644 index 0000000..7e8ca35 --- /dev/null +++ b/src/api/clients.rs @@ -0,0 +1,311 @@ +use actix_web::http::{header, StatusCode}; +use actix_web::{get, post, put, web, HttpResponse, ResponseError, Scope}; +use raise::yeet; +use serde::Deserialize; +use sqlx::MySqlPool; +use thiserror::Error; +use url::Url; +use uuid::Uuid; + +use crate::models::client::{Client, ClientType, NoSecretError}; +use crate::services::crypto::PasswordHash; +use crate::services::{db, id}; + +#[derive(Debug, Clone, Copy, Error)] +#[error("No client with the given client ID was found")] +struct ClientNotFound { + id: Uuid, +} + +impl ResponseError for ClientNotFound { + fn status_code(&self) -> StatusCode { + StatusCode::NOT_FOUND + } +} + +impl ClientNotFound { + fn new(id: Uuid) -> Self { + Self { id } + } +} + +#[get("/{client_id}")] +async fn get_client( + client_id: web::Path<Uuid>, + db: web::Data<MySqlPool>, +) -> Result<HttpResponse, ClientNotFound> { + let db = db.as_ref(); + let id = *client_id; + + let Some(client) = db::get_client_response(db, id).await.unwrap() else { + yeet!(ClientNotFound::new(id)) + }; + + let redirect_uris_link = format!("</clients/{client_id}/redirect-uris>; rel=\"redirect-uris\""); + let response = HttpResponse::Ok() + .append_header((header::LINK, redirect_uris_link)) + .json(client); + Ok(response) +} + +#[get("/{client_id}/alias")] +async fn get_client_alias( + client_id: web::Path<Uuid>, + db: web::Data<MySqlPool>, +) -> Result<HttpResponse, ClientNotFound> { + let db = db.as_ref(); + let id = *client_id; + + let Some(alias) = db::get_client_alias(db, id).await.unwrap() else { + yeet!(ClientNotFound::new(id)) + }; + + Ok(HttpResponse::Ok().json(alias)) +} + +#[get("/{client_id}/type")] +async fn get_client_type( + client_id: web::Path<Uuid>, + db: web::Data<MySqlPool>, +) -> Result<HttpResponse, ClientNotFound> { + let db = db.as_ref(); + let id = *client_id; + + let Some(client_type) = db::get_client_type(db, id).await.unwrap() else { + yeet!(ClientNotFound::new(id)) + }; + + Ok(HttpResponse::Ok().json(client_type)) +} + +#[get("/{client_id}/redirect-uris")] +async fn get_client_redirect_uris( + client_id: web::Path<Uuid>, + db: web::Data<MySqlPool>, +) -> Result<HttpResponse, ClientNotFound> { + let db = db.as_ref(); + let id = *client_id; + + if !db::client_id_exists(db, id).await.unwrap() { + yeet!(ClientNotFound::new(id)) + }; + + let redirect_uris = db::get_client_redirect_uris(db, id).await.unwrap(); + + Ok(HttpResponse::Ok().json(redirect_uris)) +} + +#[derive(Debug, Clone, Deserialize)] +struct ClientRequest { + alias: Box<str>, + ty: ClientType, + redirect_uris: Box<[Url]>, + secret: Option<Box<str>>, +} + +#[derive(Debug, Clone, Error)] +#[error("The given client alias is already taken")] +struct AliasTakenError { + alias: Box<str>, +} + +impl ResponseError for AliasTakenError { + fn status_code(&self) -> StatusCode { + StatusCode::CONFLICT + } +} + +impl AliasTakenError { + fn new(alias: &str) -> Self { + Self { + alias: Box::from(alias), + } + } +} + +#[post("")] +async fn create_client( + body: web::Json<ClientRequest>, + db: web::Data<MySqlPool>, +) -> Result<HttpResponse, UpdateClientError> { + let db = db.get_ref(); + let alias = &body.alias; + + if db::client_alias_exists(db, &alias).await.unwrap() { + yeet!(AliasTakenError::new(&alias).into()); + } + + let id = id::new_id(db, db::client_id_exists).await.unwrap(); + let client = Client::new( + id, + &alias, + body.ty, + body.secret.as_deref(), + &body.redirect_uris, + ) + .map_err(|e| e.unwrap())?; + + let transaction = db.begin().await.unwrap(); + db::create_client(transaction, &client).await.unwrap(); + + let response = HttpResponse::Created() + .insert_header((header::LOCATION, format!("clients/{id}"))) + .finish(); + Ok(response) +} + +#[derive(Debug, Clone, Error)] +enum UpdateClientError { + #[error(transparent)] + NotFound(#[from] ClientNotFound), + #[error(transparent)] + NoSecret(#[from] NoSecretError), + #[error(transparent)] + AliasTaken(#[from] AliasTakenError), +} + +impl ResponseError for UpdateClientError { + fn status_code(&self) -> StatusCode { + match self { + Self::NotFound(e) => e.status_code(), + Self::NoSecret(e) => e.status_code(), + Self::AliasTaken(e) => e.status_code(), + } + } +} + +#[put("/{id}")] +async fn update_client( + id: web::Path<Uuid>, + body: web::Json<ClientRequest>, + db: web::Data<MySqlPool>, +) -> Result<HttpResponse, UpdateClientError> { + let db = db.get_ref(); + let id = *id; + let alias = &body.alias; + + let Some(old_alias) = db::get_client_alias(db, id).await.unwrap() else { + yeet!(ClientNotFound::new(id).into()) + }; + if old_alias != alias.clone() && db::client_alias_exists(db, &alias).await.unwrap() { + yeet!(AliasTakenError::new(&alias).into()); + } + + let client = Client::new( + id, + &alias, + body.ty, + body.secret.as_deref(), + &body.redirect_uris, + ) + .map_err(|e| e.unwrap())?; + + let transaction = db.begin().await.unwrap(); + db::update_client(transaction, &client).await.unwrap(); + + let response = HttpResponse::NoContent().finish(); + Ok(response) +} + +#[put("/{id}/alias")] +async fn update_client_alias( + id: web::Path<Uuid>, + body: web::Json<Box<str>>, + db: web::Data<MySqlPool>, +) -> Result<HttpResponse, UpdateClientError> { + let db = db.get_ref(); + let id = *id; + let alias = body.0; + + let Some(old_alias) = db::get_client_alias(db, id).await.unwrap() else { + yeet!(ClientNotFound::new(id).into()) + }; + if old_alias == alias { + return Ok(HttpResponse::NoContent().finish()); + } + if db::client_alias_exists(db, &alias).await.unwrap() { + yeet!(AliasTakenError::new(&alias).into()); + } + + db::update_client_alias(db, id, &alias).await.unwrap(); + + let response = HttpResponse::NoContent().finish(); + Ok(response) +} + +#[put("/{id}/type")] +async fn update_client_type( + id: web::Path<Uuid>, + body: web::Json<ClientType>, + db: web::Data<MySqlPool>, +) -> Result<HttpResponse, UpdateClientError> { + let db = db.get_ref(); + let id = *id; + let ty = body.0; + + if !db::client_id_exists(db, id).await.unwrap() { + yeet!(ClientNotFound::new(id).into()); + } + + db::update_client_type(db, id, ty).await.unwrap(); + + Ok(HttpResponse::NoContent().finish()) +} + +#[put("/{id}/redirect-uris")] +async fn update_client_redirect_uris( + id: web::Path<Uuid>, + body: web::Json<Box<[Url]>>, + db: web::Data<MySqlPool>, +) -> Result<HttpResponse, UpdateClientError> { + let db = db.get_ref(); + let id = *id; + + if !db::client_id_exists(db, id).await.unwrap() { + yeet!(ClientNotFound::new(id).into()); + } + + let transaction = db.begin().await.unwrap(); + db::update_client_redirect_uris(transaction, id, &body.0) + .await + .unwrap(); + + Ok(HttpResponse::NoContent().finish()) +} + +#[put("{id}/secret")] +async fn update_client_secret( + id: web::Path<Uuid>, + body: web::Json<Option<Box<str>>>, + db: web::Data<MySqlPool>, +) -> Result<HttpResponse, UpdateClientError> { + let db = db.get_ref(); + let id = *id; + + let Some(client_type) = db::get_client_type(db, id).await.unwrap() else { + yeet!(ClientNotFound::new(id).into()) + }; + + if client_type == ClientType::Confidential && body.is_none() { + yeet!(NoSecretError::new().into()) + } + + let secret = body.0.map(|s| PasswordHash::new(&s).unwrap()); + db::update_client_secret(db, id, secret).await.unwrap(); + + Ok(HttpResponse::NoContent().finish()) +} + +pub fn service() -> Scope { + web::scope("/clients") + .service(get_client) + .service(get_client_alias) + .service(get_client_type) + .service(get_client_redirect_uris) + .service(create_client) + .service(update_client) + .service(update_client_alias) + .service(update_client_type) + .service(update_client_redirect_uris) + .service(update_client_secret) +} diff --git a/src/api/mod.rs b/src/api/mod.rs index 7627a60..3d74be8 100644 --- a/src/api/mod.rs +++ b/src/api/mod.rs @@ -1,7 +1,10 @@ mod liveops; mod ops; mod users; +mod oauth; +mod clients; pub use liveops::service as liveops; pub use ops::service as ops; pub use users::service as users; +pub use clients::service as clients; diff --git a/src/api/oauth.rs b/src/api/oauth.rs new file mode 100644 index 0000000..9e0e5c6 --- /dev/null +++ b/src/api/oauth.rs @@ -0,0 +1,24 @@ +use std::collections::HashMap; + +use actix_web::{web, HttpResponse}; +use serde::Deserialize; +use url::Url; +use uuid::Uuid; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize)] +#[serde(rename_all = "snake_case")] +enum ResponseType { + Code, + Token, +} + +#[derive(Debug, Clone, Deserialize)] +struct AuthorizationParameters { + response_type: ResponseType, + client_id: Uuid, + redirect_uri: Url, + state: Box<str>, + + #[serde(flatten)] + additional_parameters: HashMap<Box<str>, Box<str>>, +} diff --git a/src/api/users.rs b/src/api/users.rs index 2b67663..2cd70c0 100644 --- a/src/api/users.rs +++ b/src/api/users.rs @@ -12,6 +12,7 @@ use crate::services::{db, id}; /// Just a username. No password hash, because that'd be tempting fate. #[derive(Debug, Clone, Serialize)] +#[serde(rename_all = "camelCase")] struct UserResponse { id: Uuid, username: Box<str>, @@ -27,6 +28,7 @@ impl From<User> for UserResponse { } #[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] struct SearchUsers { username: Option<Box<str>>, limit: Option<u32>, @@ -82,14 +84,14 @@ async fn get_user( ) -> Result<HttpResponse, UserNotFoundError> { let conn = conn.get_ref(); - let user_id = user_id.to_owned(); - let user = db::get_user(conn, user_id).await.unwrap(); + let id = user_id.to_owned(); + let username = db::get_username(conn, id).await.unwrap(); - let Some(user) = user else { - yeet!(UserNotFoundError {user_id}); + let Some(username) = username else { + yeet!(UserNotFoundError { user_id: id }); }; - let response: UserResponse = user.into(); + let response = UserResponse { id, username }; let response = HttpResponse::Ok().json(response); Ok(response) } @@ -114,6 +116,7 @@ async fn get_username( /// A request to create or update user information #[derive(Debug, Clone, Deserialize)] +#[serde(rename_all = "camelCase")] struct UserRequest { username: Box<str>, password: Box<str>, @@ -138,7 +141,7 @@ async fn create_user( ) -> Result<HttpResponse, UsernameTakenError> { let conn = conn.get_ref(); - let user_id = id::new_user_id(conn).await.unwrap(); + let user_id = id::new_id(conn, db::user_id_exists).await.unwrap(); let username = body.username.clone(); let password = PasswordHash::new(&body.password).unwrap(); @@ -152,7 +155,7 @@ async fn create_user( password, }; - db::new_user(conn, &user).await.unwrap(); + db::create_user(conn, &user).await.unwrap(); let response = HttpResponse::Created() .insert_header((header::LOCATION, format!("users/{user_id}"))) @@ -171,8 +174,8 @@ enum UpdateUserError { impl ResponseError for UpdateUserError { fn status_code(&self) -> StatusCode { match self { - Self::UsernameTaken(..) => StatusCode::CONFLICT, - Self::NotFound(..) => StatusCode::NOT_FOUND, + Self::UsernameTaken(e) => e.status_code(), + Self::NotFound(e) => e.status_code(), } } } @@ -206,10 +209,7 @@ async fn update_user( db::update_user(conn, &user).await.unwrap(); - let response = HttpResponse::NoContent() - .insert_header((header::LOCATION, format!("users/{user_id}"))) - .finish(); - + let response = HttpResponse::NoContent().finish(); Ok(response) } @@ -235,10 +235,7 @@ async fn update_username( db::update_username(conn, user_id, &body).await.unwrap(); - let response = HttpResponse::NoContent() - .insert_header((header::LOCATION, format!("users/{user_id}/username"))) - .finish(); - + let response = HttpResponse::NoContent().finish(); Ok(response) } |
