summaryrefslogtreecommitdiff
path: root/src/api/clients.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/api/clients.rs')
-rw-r--r--src/api/clients.rs966
1 files changed, 483 insertions, 483 deletions
diff --git a/src/api/clients.rs b/src/api/clients.rs
index 3f906bb..ded8b81 100644
--- a/src/api/clients.rs
+++ b/src/api/clients.rs
@@ -1,483 +1,483 @@
-use actix_web::http::{header, StatusCode};
-use actix_web::{get, post, put, web, HttpResponse, ResponseError, Scope};
-use raise::yeet;
-use serde::{Deserialize, Serialize};
-use sqlx::MySqlPool;
-use thiserror::Error;
-use url::Url;
-use uuid::Uuid;
-
-use crate::models::client::{Client, ClientType, CreateClientError};
-use crate::services::crypto::PasswordHash;
-use crate::services::db::ClientRow;
-use crate::services::{db, id};
-
-#[derive(Debug, Clone, Serialize)]
-#[serde(rename_all = "camelCase")]
-struct ClientResponse {
- client_id: Uuid,
- alias: Box<str>,
- client_type: ClientType,
- allowed_scopes: Box<[Box<str>]>,
- default_scopes: Option<Box<[Box<str>]>>,
- is_trusted: bool,
-}
-
-impl From<ClientRow> for ClientResponse {
- fn from(value: ClientRow) -> Self {
- Self {
- client_id: value.id,
- alias: value.alias.into_boxed_str(),
- client_type: value.client_type,
- allowed_scopes: value
- .allowed_scopes
- .split_whitespace()
- .map(Box::from)
- .collect(),
- default_scopes: value
- .default_scopes
- .map(|s| s.split_whitespace().map(Box::from).collect()),
- is_trusted: value.is_trusted,
- }
- }
-}
-
-#[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: ClientResponse = client.into();
- let response = HttpResponse::Ok()
- .append_header((header::LINK, redirect_uris_link))
- .json(response);
- 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))
-}
-
-#[get("/{client_id}/allowed-scopes")]
-async fn get_client_allowed_scopes(
- client_id: web::Path<Uuid>,
- db: web::Data<MySqlPool>,
-) -> Result<HttpResponse, ClientNotFound> {
- let db = db.as_ref();
- let id = *client_id;
-
- let Some(allowed_scopes) = db::get_client_allowed_scopes(db, id).await.unwrap() else {
- yeet!(ClientNotFound::new(id))
- };
-
- let allowed_scopes = allowed_scopes.split_whitespace().collect::<Box<[&str]>>();
-
- Ok(HttpResponse::Ok().json(allowed_scopes))
-}
-
-#[get("/{client_id}/default-scopes")]
-async fn get_client_default_scopes(
- client_id: web::Path<Uuid>,
- db: web::Data<MySqlPool>,
-) -> Result<HttpResponse, ClientNotFound> {
- let db = db.as_ref();
- let id = *client_id;
-
- let Some(default_scopes) = db::get_client_default_scopes(db, id).await.unwrap() else {
- yeet!(ClientNotFound::new(id))
- };
-
- let default_scopes = default_scopes.map(|scopes| {
- scopes
- .split_whitespace()
- .map(Box::from)
- .collect::<Box<[Box<str>]>>()
- });
-
- Ok(HttpResponse::Ok().json(default_scopes))
-}
-
-#[get("/{client_id}/is-trusted")]
-async fn get_client_is_trusted(
- client_id: web::Path<Uuid>,
- db: web::Data<MySqlPool>,
-) -> Result<HttpResponse, ClientNotFound> {
- let db = db.as_ref();
- let id = *client_id;
-
- let Some(is_trusted) = db::is_client_trusted(db, id).await.unwrap() else {
- yeet!(ClientNotFound::new(id))
- };
-
- Ok(HttpResponse::Ok().json(is_trusted))
-}
-
-#[derive(Clone, Deserialize)]
-#[serde(rename_all = "camelCase")]
-struct ClientRequest {
- alias: Box<str>,
- ty: ClientType,
- redirect_uris: Box<[Url]>,
- secret: Option<Box<str>>,
- allowed_scopes: Box<[Box<str>]>,
- default_scopes: Option<Box<[Box<str>]>>,
- trusted: bool,
-}
-
-#[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.allowed_scopes.clone(),
- body.default_scopes.clone(),
- &body.redirect_uris,
- body.trusted,
- )
- .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)]
- ClientError(#[from] CreateClientError),
- #[error(transparent)]
- AliasTaken(#[from] AliasTakenError),
-}
-
-impl ResponseError for UpdateClientError {
- fn status_code(&self) -> StatusCode {
- match self {
- Self::NotFound(e) => e.status_code(),
- Self::ClientError(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.allowed_scopes.clone(),
- body.default_scopes.clone(),
- &body.redirect_uris,
- body.trusted,
- )
- .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}/allowed-scopes")]
-async fn update_client_allowed_scopes(
- id: web::Path<Uuid>,
- body: web::Json<Box<[Box<str>]>>,
- db: web::Data<MySqlPool>,
-) -> Result<HttpResponse, UpdateClientError> {
- let db = db.get_ref();
- let id = *id;
- let allowed_scopes = body.0.join(" ");
-
- if !db::client_id_exists(db, id).await.unwrap() {
- yeet!(ClientNotFound::new(id).into());
- }
-
- db::update_client_allowed_scopes(db, id, &allowed_scopes)
- .await
- .unwrap();
-
- Ok(HttpResponse::NoContent().finish())
-}
-
-#[put("/{id}/default-scopes")]
-async fn update_client_default_scopes(
- id: web::Path<Uuid>,
- body: web::Json<Option<Box<[Box<str>]>>>,
- db: web::Data<MySqlPool>,
-) -> Result<HttpResponse, UpdateClientError> {
- let db = db.get_ref();
- let id = *id;
- let default_scopes = body.0.map(|s| s.join(" "));
-
- if !db::client_id_exists(db, id).await.unwrap() {
- yeet!(ClientNotFound::new(id).into());
- }
-
- db::update_client_default_scopes(db, id, default_scopes)
- .await
- .unwrap();
-
- Ok(HttpResponse::NoContent().finish())
-}
-
-#[put("/{id}/is-trusted")]
-async fn update_client_is_trusted(
- id: web::Path<Uuid>,
- body: web::Json<bool>,
- db: web::Data<MySqlPool>,
-) -> Result<HttpResponse, UpdateClientError> {
- let db = db.get_ref();
- let id = *id;
- let is_trusted = *body;
-
- if !db::client_id_exists(db, id).await.unwrap() {
- yeet!(ClientNotFound::new(id).into());
- }
-
- db::update_client_trusted(db, id, is_trusted).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;
-
- for uri in body.0.iter() {
- if uri.scheme() != "https" {
- yeet!(CreateClientError::NonHttpsUri.into());
- }
-
- if uri.fragment().is_some() {
- yeet!(CreateClientError::UriFragment.into())
- }
- }
-
- 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!(CreateClientError::NoSecret.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_allowed_scopes)
- .service(get_client_default_scopes)
- .service(get_client_redirect_uris)
- .service(get_client_is_trusted)
- .service(create_client)
- .service(update_client)
- .service(update_client_alias)
- .service(update_client_type)
- .service(update_client_allowed_scopes)
- .service(update_client_default_scopes)
- .service(update_client_redirect_uris)
- .service(update_client_secret)
-}
+use actix_web::http::{header, StatusCode};
+use actix_web::{get, post, put, web, HttpResponse, ResponseError, Scope};
+use raise::yeet;
+use serde::{Deserialize, Serialize};
+use sqlx::MySqlPool;
+use thiserror::Error;
+use url::Url;
+use uuid::Uuid;
+
+use crate::models::client::{Client, ClientType, CreateClientError};
+use crate::services::crypto::PasswordHash;
+use crate::services::db::ClientRow;
+use crate::services::{db, id};
+
+#[derive(Debug, Clone, Serialize)]
+#[serde(rename_all = "camelCase")]
+struct ClientResponse {
+ client_id: Uuid,
+ alias: Box<str>,
+ client_type: ClientType,
+ allowed_scopes: Box<[Box<str>]>,
+ default_scopes: Option<Box<[Box<str>]>>,
+ is_trusted: bool,
+}
+
+impl From<ClientRow> for ClientResponse {
+ fn from(value: ClientRow) -> Self {
+ Self {
+ client_id: value.id,
+ alias: value.alias.into_boxed_str(),
+ client_type: value.client_type,
+ allowed_scopes: value
+ .allowed_scopes
+ .split_whitespace()
+ .map(Box::from)
+ .collect(),
+ default_scopes: value
+ .default_scopes
+ .map(|s| s.split_whitespace().map(Box::from).collect()),
+ is_trusted: value.is_trusted,
+ }
+ }
+}
+
+#[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: ClientResponse = client.into();
+ let response = HttpResponse::Ok()
+ .append_header((header::LINK, redirect_uris_link))
+ .json(response);
+ 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))
+}
+
+#[get("/{client_id}/allowed-scopes")]
+async fn get_client_allowed_scopes(
+ client_id: web::Path<Uuid>,
+ db: web::Data<MySqlPool>,
+) -> Result<HttpResponse, ClientNotFound> {
+ let db = db.as_ref();
+ let id = *client_id;
+
+ let Some(allowed_scopes) = db::get_client_allowed_scopes(db, id).await.unwrap() else {
+ yeet!(ClientNotFound::new(id))
+ };
+
+ let allowed_scopes = allowed_scopes.split_whitespace().collect::<Box<[&str]>>();
+
+ Ok(HttpResponse::Ok().json(allowed_scopes))
+}
+
+#[get("/{client_id}/default-scopes")]
+async fn get_client_default_scopes(
+ client_id: web::Path<Uuid>,
+ db: web::Data<MySqlPool>,
+) -> Result<HttpResponse, ClientNotFound> {
+ let db = db.as_ref();
+ let id = *client_id;
+
+ let Some(default_scopes) = db::get_client_default_scopes(db, id).await.unwrap() else {
+ yeet!(ClientNotFound::new(id))
+ };
+
+ let default_scopes = default_scopes.map(|scopes| {
+ scopes
+ .split_whitespace()
+ .map(Box::from)
+ .collect::<Box<[Box<str>]>>()
+ });
+
+ Ok(HttpResponse::Ok().json(default_scopes))
+}
+
+#[get("/{client_id}/is-trusted")]
+async fn get_client_is_trusted(
+ client_id: web::Path<Uuid>,
+ db: web::Data<MySqlPool>,
+) -> Result<HttpResponse, ClientNotFound> {
+ let db = db.as_ref();
+ let id = *client_id;
+
+ let Some(is_trusted) = db::is_client_trusted(db, id).await.unwrap() else {
+ yeet!(ClientNotFound::new(id))
+ };
+
+ Ok(HttpResponse::Ok().json(is_trusted))
+}
+
+#[derive(Clone, Deserialize)]
+#[serde(rename_all = "camelCase")]
+struct ClientRequest {
+ alias: Box<str>,
+ ty: ClientType,
+ redirect_uris: Box<[Url]>,
+ secret: Option<Box<str>>,
+ allowed_scopes: Box<[Box<str>]>,
+ default_scopes: Option<Box<[Box<str>]>>,
+ trusted: bool,
+}
+
+#[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.allowed_scopes.clone(),
+ body.default_scopes.clone(),
+ &body.redirect_uris,
+ body.trusted,
+ )
+ .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)]
+ ClientError(#[from] CreateClientError),
+ #[error(transparent)]
+ AliasTaken(#[from] AliasTakenError),
+}
+
+impl ResponseError for UpdateClientError {
+ fn status_code(&self) -> StatusCode {
+ match self {
+ Self::NotFound(e) => e.status_code(),
+ Self::ClientError(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.allowed_scopes.clone(),
+ body.default_scopes.clone(),
+ &body.redirect_uris,
+ body.trusted,
+ )
+ .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}/allowed-scopes")]
+async fn update_client_allowed_scopes(
+ id: web::Path<Uuid>,
+ body: web::Json<Box<[Box<str>]>>,
+ db: web::Data<MySqlPool>,
+) -> Result<HttpResponse, UpdateClientError> {
+ let db = db.get_ref();
+ let id = *id;
+ let allowed_scopes = body.0.join(" ");
+
+ if !db::client_id_exists(db, id).await.unwrap() {
+ yeet!(ClientNotFound::new(id).into());
+ }
+
+ db::update_client_allowed_scopes(db, id, &allowed_scopes)
+ .await
+ .unwrap();
+
+ Ok(HttpResponse::NoContent().finish())
+}
+
+#[put("/{id}/default-scopes")]
+async fn update_client_default_scopes(
+ id: web::Path<Uuid>,
+ body: web::Json<Option<Box<[Box<str>]>>>,
+ db: web::Data<MySqlPool>,
+) -> Result<HttpResponse, UpdateClientError> {
+ let db = db.get_ref();
+ let id = *id;
+ let default_scopes = body.0.map(|s| s.join(" "));
+
+ if !db::client_id_exists(db, id).await.unwrap() {
+ yeet!(ClientNotFound::new(id).into());
+ }
+
+ db::update_client_default_scopes(db, id, default_scopes)
+ .await
+ .unwrap();
+
+ Ok(HttpResponse::NoContent().finish())
+}
+
+#[put("/{id}/is-trusted")]
+async fn update_client_is_trusted(
+ id: web::Path<Uuid>,
+ body: web::Json<bool>,
+ db: web::Data<MySqlPool>,
+) -> Result<HttpResponse, UpdateClientError> {
+ let db = db.get_ref();
+ let id = *id;
+ let is_trusted = *body;
+
+ if !db::client_id_exists(db, id).await.unwrap() {
+ yeet!(ClientNotFound::new(id).into());
+ }
+
+ db::update_client_trusted(db, id, is_trusted).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;
+
+ for uri in body.0.iter() {
+ if uri.scheme() != "https" {
+ yeet!(CreateClientError::NonHttpsUri.into());
+ }
+
+ if uri.fragment().is_some() {
+ yeet!(CreateClientError::UriFragment.into())
+ }
+ }
+
+ 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!(CreateClientError::NoSecret.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_allowed_scopes)
+ .service(get_client_default_scopes)
+ .service(get_client_redirect_uris)
+ .service(get_client_is_trusted)
+ .service(create_client)
+ .service(update_client)
+ .service(update_client_alias)
+ .service(update_client_type)
+ .service(update_client_allowed_scopes)
+ .service(update_client_default_scopes)
+ .service(update_client_redirect_uris)
+ .service(update_client_secret)
+}