feat: login UI adapter for oauth domain

This commit is contained in:
Aravinth Manivannan 2024-05-04 22:46:58 +05:30
parent 2cf8145e31
commit 5637eb8415
Signed by: realaravinth
GPG key ID: F8F50389936984FF
9 changed files with 347 additions and 0 deletions

View file

@ -0,0 +1 @@
pub mod web;

View file

@ -0,0 +1,60 @@
use std::str::FromStr;
use actix_web::{get, http::header, post, web, HttpResponse};
use crate::forge::auth::adapter::out::forge::SupportedForges;
use crate::forge::auth::application::port::input::ui::{
errors::*, login::RequestAuthorizationInterface,
};
use crate::forge::auth::application::services::request_authorization::command::RequestAuthorizationCommand;
use super::{templates::login::LoginCtxFactory, ServiceFactory, WebCtx};
use crate::ActixCtx;
#[async_trait::async_trait]
impl RequestAuthorizationInterface for WebCtx {
#[tracing::instrument(name = "web adapter request_oauth_authorization", skip(self))]
async fn request_oauth_authorization(&self, forge_name: String) -> InUIResult<HttpResponse> {
let service = ServiceFactory::request_authorization(
SupportedForges::from_str(&forge_name).map_err(|_| InUIError::BadRequest)?,
self,
)?;
log::info!("service found");
let cmd = RequestAuthorizationCommand::new_command(forge_name)?;
let auth_page = service.request_authorization(cmd).await?;
Ok(HttpResponse::Found()
.insert_header((header::LOCATION, auth_page.as_str()))
.finish())
}
}
#[get("/login")]
async fn login_page(ctx: ActixCtx) -> InUIResult<HttpResponse> {
let template_ctx =
LoginCtxFactory::get_ctx(ctx.adapters.forges.get_supported_forges(), &ctx.routes);
let page = ctx
.templates
.login_page
.get_login_page(template_ctx)
.unwrap();
Ok(HttpResponse::Ok()
.append_header((header::CONTENT_TYPE, "text/html; charset=UTF-8"))
.body(page))
}
#[post("/oauth/{forge}/login")]
#[tracing::instrument(name = "web handler request_oauth_authorization", skip(ctx))]
async fn request_oauth_authorization(
ctx: ActixCtx,
forge_name: web::Path<String>,
) -> InUIResult<HttpResponse> {
ctx.request_oauth_authorization(forge_name.into_inner())
.await
}
pub fn services(cfg: &mut web::ServiceConfig) {
cfg.service(login_page);
cfg.service(request_oauth_authorization);
}

View file

@ -0,0 +1,81 @@
use std::sync::Arc;
use actix_web::web::{self, Data};
use url::Url;
use crate::forge::auth::adapter::out::db::DBAdapter;
use crate::forge::auth::adapter::out::forge::{ForgeRepository, SupportedForges};
use crate::forge::auth::application::port::input::ui::errors::{InUIError, InUIResult};
use crate::forge::auth::application::services::request_authorization::service::RequestAuthorizationService;
use crate::forge::auth::application::services::request_authorization::RequestAuthorizationUserCase;
use crate::settings::Settings;
pub mod login;
mod routes;
mod templates;
use routes::RoutesRepository;
pub type ArcCtx = Arc<WebCtx>;
pub type ActixCtx = Data<ArcCtx>;
#[derive(Clone)]
pub struct WebCtx {
pub routes: Arc<RoutesRepository>,
pub adapters: Adapters,
pub templates: templates::Templates,
pub settings: Settings,
}
impl WebCtx {
pub fn new_actix_ctx(forges: ForgeRepository, db: DBAdapter, settings: Settings) -> ActixCtx {
let routes = Arc::new(RoutesRepository::default());
Data::new(Arc::new(Self {
routes: routes.clone(),
adapters: Adapters::new(forges, db),
templates: templates::Templates::default(),
settings,
}))
}
}
#[derive(Clone)]
pub struct Adapters {
pub forges: ForgeRepository,
pub db: DBAdapter,
}
impl Adapters {
pub fn new(forges: ForgeRepository, db: DBAdapter) -> Self {
Self { forges, db }
}
}
pub struct ServiceFactory;
impl ServiceFactory {
pub fn request_authorization(
forge_name: SupportedForges,
ctx: &WebCtx,
) -> InUIResult<Arc<dyn RequestAuthorizationUserCase>> {
if let Some(forge) = ctx.adapters.forges.get_forge(&forge_name) {
Ok(Arc::new(RequestAuthorizationService::new(
ctx.adapters.db.save_oauth_state_adapter.clone(),
forge.get_redirect_uri_adapter.clone(),
Url::parse(&format!(
"{}://{}{}",
"http",
&ctx.settings.server.domain,
&ctx.routes.process_oauth_authorization_response(&forge_name)
))
.map_err(|_| InUIError::InternalServerError)?,
)))
} else {
Err(InUIError::BadRequest)
}
}
}
pub fn services(cfg: &mut web::ServiceConfig) {
cfg.configure(login::services);
}

View file

@ -0,0 +1,28 @@
use crate::forge::auth::adapter::out::forge::SupportedForges;
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct RoutesRepository {
process_oauth_authorization_response: String,
pub login: String,
oauth_login: String,
}
impl Default for RoutesRepository {
fn default() -> Self {
Self {
process_oauth_authorization_response: "/oauth/{forge}/authorize".to_string(),
login: "/login".to_string(),
oauth_login: "/oauth/{forge}/login".to_string(),
}
}
}
impl RoutesRepository {
pub fn oauth_login(&self, forge_name: &SupportedForges) -> String {
self.oauth_login.replace("{forge}", &forge_name.to_string())
}
pub fn process_oauth_authorization_response(&self, forge_name: &SupportedForges) -> String {
self.process_oauth_authorization_response
.replace("{forge}", &forge_name.to_string())
}
}

View file

@ -0,0 +1,21 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
{% for forge in payload.forges -%}
<form action="{{forge.path}}" method="POST">
<button type="submit">
{{forge.name}}
</button>
</form>
{%- endfor %}
</body>
</html>

View file

@ -0,0 +1,60 @@
use serde::{Deserialize, Serialize};
use crate::forge::auth::adapter::input::web::routes::RoutesRepository;
use crate::forge::auth::adapter::out::forge::SupportedForges;
use super::{tera_context, TEMPLATES};
pub trait LoginPageInterface: Send + Sync {
fn get_login_page(&self, ctx: LoginCtx) -> Result<String, Box<dyn std::error::Error>>;
}
use super::TemplateFile;
pub const LOGIN_TEMPLATE: TemplateFile = TemplateFile::new("login", "login.html");
pub fn register_templates(t: &mut tera::Tera) {
LOGIN_TEMPLATE.register(t).expect(LOGIN_TEMPLATE.name);
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct Forge {
name: String,
path: String,
}
impl Forge {
pub fn new(name: String, path: String) -> Self {
Self { name, path }
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct LoginCtx {
pub forges: Vec<Forge>,
}
pub struct LoginCtxFactory;
impl LoginCtxFactory {
pub fn get_ctx(supported_forges: Vec<SupportedForges>, routes: &RoutesRepository) -> LoginCtx {
let mut forges = Vec::with_capacity(supported_forges.len());
for s in supported_forges.iter() {
forges.push(Forge::new(s.to_string(), routes.oauth_login(s)));
}
LoginCtx { forges }
}
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
pub struct LoginPageTemplate;
impl LoginPageInterface for LoginPageTemplate {
fn get_login_page(&self, ctx: LoginCtx) -> Result<String, Box<dyn std::error::Error>> {
let ctx = tera_context(&ctx);
let page = TEMPLATES
.render(LOGIN_TEMPLATE.name, &ctx.borrow())
.unwrap();
Ok(page)
}
}

View file

@ -0,0 +1,23 @@
use std::sync::Arc;
pub mod login;
pub mod setup;
mod utils;
use login::{LoginPageInterface, LoginPageTemplate};
pub use setup::{TemplateFile, PAYLOAD_KEY, TEMPLATES};
pub use utils::tera_context;
#[derive(Clone)]
pub struct Templates {
pub login_page: Arc<dyn LoginPageInterface>,
}
impl Default for Templates {
fn default() -> Self {
Self {
login_page: Arc::new(LoginPageTemplate),
}
}
}

View file

@ -0,0 +1,62 @@
use lazy_static::lazy_static;
use rust_embed::RustEmbed;
use tera::*;
pub struct TemplateFile {
pub name: &'static str,
pub path: &'static str,
}
impl TemplateFile {
pub const fn new(name: &'static str, path: &'static str) -> Self {
Self { name, path }
}
pub fn register(&self, t: &mut Tera) -> std::result::Result<(), tera::Error> {
t.add_raw_template(self.name, &Templates::get_template(self).expect(self.name))
}
#[cfg(test)]
#[allow(dead_code)]
pub fn register_from_file(&self, t: &mut Tera) -> std::result::Result<(), tera::Error> {
use std::path::Path;
t.add_template_file(Path::new("templates/").join(self.path), Some(self.name))
}
}
pub const PAYLOAD_KEY: &str = "payload";
//pub const BASE: TemplateFile = TemplateFile::new("base", "components/base.html");
//pub const FOOTER: TemplateFile = TemplateFile::new("footer", "components/footer.html");
//pub const PUB_NAV: TemplateFile = TemplateFile::new("pub_nav", "components/nav/pub.html");
//pub const AUTH_NAV: TemplateFile = TemplateFile::new("auth_nav", "components/nav/auth.html");
lazy_static! {
pub static ref TEMPLATES: Tera = {
let mut tera = Tera::default();
// for t in [BASE, FOOTER, PUB_NAV, AUTH_NAV].iter() {
// t.register(&mut tera).unwrap();
// }
// errors::register_templates(&mut tera);
tera.autoescape_on(vec![".html", ".sql"]);
super::login::register_templates(&mut tera);
// auth::register_templates(&mut tera);
// gists::register_templates(&mut tera);
tera
};
}
#[derive(RustEmbed)]
#[folder = "src/forge/auth/adapter/input/web/templates/"]
#[include = "*.html"]
#[exclude = "*.rs"]
struct Templates;
impl Templates {
fn get_template(t: &TemplateFile) -> Option<String> {
match Self::get(t.path) {
Some(file) => Some(String::from_utf8_lossy(&file.data).into_owned()),
None => None,
}
}
}

View file

@ -0,0 +1,11 @@
use std::cell::RefCell;
use super::PAYLOAD_KEY;
use serde::Serialize;
use tera::Context;
pub fn tera_context<T: Serialize>(s: &T) -> RefCell<Context> {
let c = RefCell::new(Context::new());
c.borrow_mut().insert(PAYLOAD_KEY, s);
c
}