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, NoSecretError}; 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, client_type: ClientType, allowed_scopes: Box<[Box]>, default_scopes: Option]>>, } impl From 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()), } } } #[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, db: web::Data, ) -> Result { 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!("; 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, db: web::Data, ) -> Result { 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, db: web::Data, ) -> Result { 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, db: web::Data, ) -> Result { 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(Clone, Deserialize)] #[serde(rename_all = "camelCase")] struct ClientRequest { alias: Box, ty: ClientType, redirect_uris: Box<[Url]>, secret: Option>, allowed_scopes: Box<[Box]>, default_scopes: Option]>>, } #[derive(Debug, Clone, Error)] #[error("The given client alias is already taken")] struct AliasTakenError { alias: Box, } 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, db: web::Data, ) -> Result { 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, ) .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, body: web::Json, db: web::Data, ) -> Result { 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, ) .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, body: web::Json>, db: web::Data, ) -> Result { 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, body: web::Json, db: web::Data, ) -> Result { 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, body: web::Json>, db: web::Data, ) -> Result { 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, body: web::Json>>, db: web::Data, ) -> Result { 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) }