diff --git a/api-internal/src/generated.rs b/api-internal/src/generated.rs index 2b11417..ecadd54 100644 --- a/api-internal/src/generated.rs +++ b/api-internal/src/generated.rs @@ -154,6 +154,21 @@ pub mod api { self.api = self.api.bind("/application.get", Method::POST, handler); self } + + pub fn bind_applications_list(mut self, handler: F) -> Self + where + F: Handler, + T: FromRequest + 'static, + R: Future< + Output = Result< + super::paths::applications_list::Response, + super::paths::applications_list::Error, + >, + > + 'static, + { + self.api = self.api.bind("/applications.list", Method::POST, handler); + self + } } } @@ -509,6 +524,12 @@ pub mod components { #[from] pub error: ApplicationGetError, } + + #[derive(Debug, Serialize)] + pub struct ApplicationsListSuccess { + pub installed: Vec, + pub available: Vec, + } } pub mod request_bodies { @@ -1222,4 +1243,64 @@ pub mod paths { } } } + + pub mod applications_list { + use actix_swagger::ContentType; + use actix_web::http::StatusCode; + use actix_web::{HttpRequest, HttpResponse, Responder, ResponseError}; + use serde::Serialize; + + use super::responses; + + #[derive(Debug, Serialize)] + #[serde(untagged)] + pub enum Response { + Ok(responses::ApplicationsListSuccess), + } + + #[derive(Debug, Serialize, thiserror::Error)] + #[serde(untagged)] + pub enum Error { + #[error(transparent)] + InternalServerError( + #[from] + #[serde(skip)] + eyre::Report, + ), + } + + impl Responder for Response { + fn respond_to(self, _: &HttpRequest) -> HttpResponse { + match self { + Response::Ok(r) => HttpResponse::build(StatusCode::OK).json(r), + } + } + } + + impl ResponseError for Error { + fn status_code(&self) -> StatusCode { + match self { + Error::InternalServerError(_) => StatusCode::INTERNAL_SERVER_ERROR, + } + } + + fn error_response(&self) -> HttpResponse { + let content_type = match self { + Self::InternalServerError(_) => Some(ContentType::Json), + }; + + let mut res = &mut HttpResponse::build(self.status_code()); + if let Some(content_type) = content_type { + res = res.content_type(content_type.to_string()); + + match content_type { + ContentType::Json => res.body(serde_json::to_string(self).unwrap()), + ContentType::FormData => res.body(serde_plain::to_string(self).unwrap()), + } + } else { + HttpResponse::build(self.status_code()).finish() + } + } + } + } } diff --git a/api-internal/src/main.rs b/api-internal/src/main.rs index d1f8946..ce5a1f5 100644 --- a/api-internal/src/main.rs +++ b/api-internal/src/main.rs @@ -64,7 +64,8 @@ async fn main() -> eyre::Result<()> { .bind_session_delete(routes::session::delete::route) .bind_session_get(routes::session::get::route) .bind_account_edit(account::edit::route) - .bind_application_get(routes::application::get::route), + .bind_application_get(routes::application::get::route) + .bind_applications_list(routes::application::list::route), ) }); diff --git a/api-internal/src/routes/application/list.rs b/api-internal/src/routes/application/list.rs new file mode 100644 index 0000000..1694e99 --- /dev/null +++ b/api-internal/src/routes/application/list.rs @@ -0,0 +1,45 @@ +use actix_web::web::Data; + +use accesso_core::app::application::{Application as _, ApplicationsListError}; +use accesso_core::models; + +use crate::generated::{ + components::{responses::ApplicationsListSuccess, schemas}, + paths::applications_list::{Error, Response}, +}; +use crate::session::Session; + +pub async fn route(app: Data, session: Session) -> Result { + let applications_list = app + .applications_list(session.user.id) + .await + .map_err(map_applications_list_error)?; + + Ok(Response::Ok(ApplicationsListSuccess { + available: applications_list + .available + .iter() + .map(map_application) + .collect(), + installed: applications_list + .installed + .iter() + .map(map_application) + .collect(), + })) +} + +fn map_application(application: &models::Application) -> schemas::Application { + schemas::Application { + id: application.id, + title: application.title.clone(), + allowed_registrations: application.allowed_registrations, + avatar: None, + } +} + +fn map_applications_list_error(error: ApplicationsListError) -> Error { + match error { + ApplicationsListError::Unexpected(report) => Error::InternalServerError(report.into()), + } +} diff --git a/api-internal/src/routes/application/mod.rs b/api-internal/src/routes/application/mod.rs index 125ca70..89d961b 100644 --- a/api-internal/src/routes/application/mod.rs +++ b/api-internal/src/routes/application/mod.rs @@ -1 +1,2 @@ pub mod get; +pub mod list; diff --git a/app/src/application.rs b/app/src/application.rs index 4b93dad..bdad132 100644 --- a/app/src/application.rs +++ b/app/src/application.rs @@ -1,10 +1,14 @@ -use crate::{App, Service}; -use accesso_core::app::application::{Application, ApplicationGetError}; -use accesso_core::contracts::Repository; -use accesso_core::models; use async_trait::async_trait; use uuid::Uuid; +use accesso_core::app::application::{ + Application, ApplicationGetError, ApplicationsList, ApplicationsListError, +}; +use accesso_core::contracts::Repository; +use accesso_core::models; + +use crate::{App, Service}; + #[async_trait] impl Application for App { async fn application_get( @@ -20,4 +24,26 @@ impl Application for App { None => Err(ApplicationGetError::ApplicationNotFound), } } + + async fn applications_list( + &self, + user_id: Uuid, + ) -> Result { + let db = self.get::>()?; + + let available_future = db.applications_allowed_to_register(); + let installed_future = db.applications_user_registered_in(user_id); + + let mut available = available_future.await?; + let installed = installed_future.await?; + + for application in &installed { + available.retain(|found| found.id != application.id); + } + + Ok(ApplicationsList { + available, + installed, + }) + } } diff --git a/core/src/app/application.rs b/core/src/app/application.rs index 959c364..47c40fc 100644 --- a/core/src/app/application.rs +++ b/core/src/app/application.rs @@ -6,6 +6,16 @@ use uuid::Uuid; #[async_trait] pub trait Application { async fn application_get(&self, id: Uuid) -> Result; + async fn applications_list( + &self, + user_id: Uuid, + ) -> Result; +} + +#[derive(Debug)] +pub struct ApplicationsList { + pub available: Vec, + pub installed: Vec, } #[derive(Debug, thiserror::Error)] @@ -21,3 +31,15 @@ impl From for ApplicationGetError { ApplicationGetError::Unexpected(e.into()) } } + +#[derive(Debug, thiserror::Error)] +pub enum ApplicationsListError { + #[error(transparent)] + Unexpected(#[from] eyre::Report), +} + +impl From for ApplicationsListError { + fn from(e: UnexpectedDatabaseError) -> Self { + ApplicationsListError::Unexpected(e.into()) + } +} diff --git a/core/src/contracts/repo/application.rs b/core/src/contracts/repo/application.rs index ddc28d2..b4f3473 100644 --- a/core/src/contracts/repo/application.rs +++ b/core/src/contracts/repo/application.rs @@ -46,6 +46,15 @@ pub trait ApplicationRepo { async fn application_list(&self) -> Result, UnexpectedDatabaseError>; + async fn applications_allowed_to_register( + &self, + ) -> Result, UnexpectedDatabaseError>; + + async fn applications_user_registered_in( + &self, + user_id: Uuid, + ) -> Result, UnexpectedDatabaseError>; + async fn application_create( &self, application: ApplicationForm, @@ -72,6 +81,21 @@ impl ApplicationRepo for crate::contracts::MockDb { self.application.application_list().await } + async fn applications_allowed_to_register( + &self, + ) -> Result, UnexpectedDatabaseError> { + self.application.applications_allowed_to_register().await + } + + async fn applications_user_registered_in( + &self, + user_id: Uuid, + ) -> Result, UnexpectedDatabaseError> { + self.application + .applications_user_registered_in(user_id) + .await + } + async fn application_create( &self, application: ApplicationForm, diff --git a/db/src/repos/client.rs b/db/src/repos/client.rs index 40b2662..dab5340 100644 --- a/db/src/repos/client.rs +++ b/db/src/repos/client.rs @@ -2,6 +2,8 @@ use accesso_core::contracts::{ ApplicationCreateError, ApplicationForm, ApplicationRepo, UnexpectedDatabaseError, }; use accesso_core::models; +use accesso_core::models::Application; +use uuid::Uuid; use crate::entities; use crate::Database; @@ -49,6 +51,47 @@ impl ApplicationRepo for Database { .collect()) } + async fn applications_allowed_to_register( + &self, + ) -> Result, UnexpectedDatabaseError> { + Ok(sqlx::query_as!( + entities::Client, + // language=PostgreSQL + r#" + SELECT id, is_dev, redirect_uri, secret_key, title, allowed_registrations + FROM clients + WHERE allowed_registrations = true AND is_dev = false + "# + ) + .fetch_all(&self.pool) + .await? + .into_iter() + .map(|client| client.into()) + .collect()) + } + + async fn applications_user_registered_in( + &self, + user_id: Uuid, + ) -> Result, UnexpectedDatabaseError> { + Ok(sqlx::query_as!( + entities::Client, + // language=PostgreSQL + r#" + SELECT clients.id, is_dev, redirect_uri, secret_key, title, allowed_registrations + FROM clients + LEFT JOIN user_registrations ON clients.id = user_registrations.client_id + WHERE user_registrations.user_id = $1 + "#, + user_id, + ) + .fetch_all(&self.pool) + .await? + .into_iter() + .map(|client| client.into()) + .collect()) + } + async fn application_create( &self, application: ApplicationForm,