Compare commits
10 Commits
46f450b8d8
...
85d7d9d322
Author | SHA1 | Date |
---|---|---|
Aravinth Manivannan | 85d7d9d322 | |
Aravinth Manivannan | 27204cd53d | |
Aravinth Manivannan | 5637eb8415 | |
Aravinth Manivannan | 2cf8145e31 | |
Aravinth Manivannan | 253c26227c | |
Aravinth Manivannan | 51ac3e4977 | |
Aravinth Manivannan | 9b328a0935 | |
Aravinth Manivannan | c718ae6f4d | |
Aravinth Manivannan | 6df89a8558 | |
Aravinth Manivannan | 6500f4c122 |
|
@ -0,0 +1 @@
|
|||
export POSTGRES_DATABASE_URL="postgres://postgres:password@localhost:5432/postgres"
|
|
@ -1 +1,2 @@
|
|||
/target
|
||||
.env
|
||||
|
|
File diff suppressed because it is too large
Load Diff
26
Cargo.toml
26
Cargo.toml
|
@ -3,6 +3,32 @@ name = "forgeflux"
|
|||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[workspace]
|
||||
exclude = ["utils/db-migrations"] #, "utils/cache-bust"]
|
||||
memebers = ["."]
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
actix-web = "4.5.1"
|
||||
async-trait = "0.1.80"
|
||||
chrono = "0.4.38"
|
||||
config = "0.14.0"
|
||||
derive_more = "0.99.17"
|
||||
lazy_static = "1.4.0"
|
||||
log = "0.4.21"
|
||||
pretty_env_logger = "0.5.0"
|
||||
rand = "0.8.5"
|
||||
reqwest = { version = "0.12.4", features = ["json"] }
|
||||
rust-embed = { version = "8.3.0", features = ["include-exclude"] }
|
||||
serde = { version = "1.0.199", features = ["derive"] }
|
||||
serde_json = "1.0.116"
|
||||
sqlx = { version = "0.7.4", features = ["runtime-tokio-rustls", "postgres", "time"] }
|
||||
tera = "1.19.1"
|
||||
tracing = { version = "0.1.40", features = ["log"] }
|
||||
tracing-actix-web = "0.7.10"
|
||||
url = { version = "2.5.0", features = ["serde"] }
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt= "2.9"
|
||||
|
|
|
@ -0,0 +1,30 @@
|
|||
debug = true
|
||||
source_code = "https://git.batsense.net/ForgeFlux/ForgeFlux"
|
||||
allow_registration = true
|
||||
|
||||
[server]
|
||||
# Please set a unique value, your mCaptcha instance's security depends on this being
|
||||
# unique
|
||||
#cookie_secret = "Zae0OOxf^bOJ#zN^&k7VozgW&QAx%n02TQFXpRMG4cCU0xMzgu3dna@tQ9dvc&TlE6p*n#kXUdLZJCQsuODIV%r$@o4%770ePQB7m#dpV!optk01NpY0@615w5e2Br4d"
|
||||
# The port at which you want authentication to listen to
|
||||
# takes a number, choose from 1000-10000 if you dont know what you are doing
|
||||
port = 7000
|
||||
#IP address. Enter 0.0.0.0 to listen on all available addresses
|
||||
ip= "0.0.0.0"
|
||||
# enter your hostname, eg: example.com
|
||||
domain = "localhost:7000"
|
||||
|
||||
[database]
|
||||
# This section deals with the database location and how to access it
|
||||
# Please note that at the moment, we have support for only postgresqa.
|
||||
# Example, if you are Batman, your config would be:
|
||||
# url = "postgres://batman:password@batcave.org:5432/batcave"
|
||||
# database_type = "postgres"
|
||||
# pool = 4
|
||||
url = "postgres://example.org" # hack for tests to run successfully
|
||||
pool = 4
|
||||
|
||||
[forges.forgejo]
|
||||
client_id = "foo"
|
||||
client_secret = "bar"
|
||||
url = "http://example.org"
|
|
@ -0,0 +1,4 @@
|
|||
#[async_trait::async_trait]
|
||||
pub trait CreateDatabase: Send + Sync {
|
||||
async fn create_database(&self, url: &url::Url);
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
#[async_trait::async_trait]
|
||||
pub trait DeleteDatabase: Send + Sync {
|
||||
async fn delete_database(&self, name: &url::Url);
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
#[async_trait::async_trait]
|
||||
pub trait RunMigrations: Send + Sync {
|
||||
async fn migrate(&self);
|
||||
}
|
|
@ -0,0 +1,4 @@
|
|||
pub mod create_database;
|
||||
pub mod delete_database;
|
||||
pub mod migrate;
|
||||
pub mod sqlx_postgres;
|
|
@ -0,0 +1,43 @@
|
|||
use sqlx::migrate::MigrateDatabase;
|
||||
use sqlx::postgres::PgPool;
|
||||
|
||||
use super::migrate::RunMigrations;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Postgres {
|
||||
pool: PgPool,
|
||||
}
|
||||
|
||||
impl Postgres {
|
||||
pub fn new(pool: PgPool) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl RunMigrations for Postgres {
|
||||
async fn migrate(&self) {
|
||||
sqlx::migrate!("./migrations/")
|
||||
.run(&self.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
pub struct PostgresDatabase;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl super::create_database::CreateDatabase for PostgresDatabase {
|
||||
async fn create_database(&self, url: &url::Url) {
|
||||
sqlx::Postgres::create_database(url.as_str()).await.unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl super::delete_database::DeleteDatabase for PostgresDatabase {
|
||||
async fn delete_database(&self, url: &url::Url) {
|
||||
sqlx::Postgres::force_drop_database(url.as_str())
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
pub mod web;
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
|
@ -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>
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
pub mod input;
|
||||
pub mod out;
|
|
@ -0,0 +1,34 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use sqlx::PgPool;
|
||||
use url::Url;
|
||||
|
||||
use crate::db::migrate::RunMigrations;
|
||||
use crate::forge::auth::application::port::out::db::save_oauth_state::SaveOAuthState;
|
||||
|
||||
pub mod postgres;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DBAdapter {
|
||||
pub save_oauth_state_adapter: Arc<dyn SaveOAuthState>,
|
||||
pg_adapter: postgres::DBOutPostgresAdapter,
|
||||
}
|
||||
|
||||
impl DBAdapter {
|
||||
pub async fn init(database_url: &str) -> Self {
|
||||
let pool = PgPool::connect(database_url).await.unwrap();
|
||||
|
||||
Self::new(pool)
|
||||
}
|
||||
fn new(pool: PgPool) -> Self {
|
||||
let pg_adapter = postgres::DBOutPostgresAdapter::new(pool);
|
||||
Self {
|
||||
save_oauth_state_adapter: Arc::new(pg_adapter.clone()),
|
||||
pg_adapter,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn migratable(&self) -> Arc<dyn RunMigrations> {
|
||||
self.pg_adapter.migratable()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
use derive_more::Display;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::Error as SqlxError;
|
||||
|
||||
use crate::forge::auth::application::port::out::db::errors::OutDBPortError;
|
||||
|
||||
impl From<SqlxError> for OutDBPortError {
|
||||
fn from(value: SqlxError) -> Self {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
//impl From<ProcessAuthorizationServiceError> for OutDBPostgresError {
|
||||
// fn from(v: ProcessAuthorizationServiceError) -> Self {
|
||||
// match v {
|
||||
// ProcessAuthorizationServiceError::InteralError => Self::InternalServerError,
|
||||
// ProcessAuthorizationServiceError::BadRequest => Self::BadRequest,
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//impl From<RequestAuthorizationServiceError> for OutDBPostgresError {
|
||||
// fn from(v: RequestAuthorizationServiceError) -> Self {
|
||||
// match v {
|
||||
// RequestAuthorizationServiceError::InteralError => Self::InternalServerError,
|
||||
// }
|
||||
// }
|
||||
//}
|
|
@ -0,0 +1,24 @@
|
|||
//use crate::forge::auth::application::port::out::db::save_oauth_state::SaveOAuthState;
|
||||
use std::sync::Arc;
|
||||
|
||||
use sqlx::postgres::PgPool;
|
||||
|
||||
use crate::db::{migrate::RunMigrations, sqlx_postgres::Postgres};
|
||||
|
||||
mod errors;
|
||||
mod save_oauth_state;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct DBOutPostgresAdapter {
|
||||
pool: PgPool,
|
||||
}
|
||||
|
||||
impl DBOutPostgresAdapter {
|
||||
pub fn new(pool: PgPool) -> Self {
|
||||
Self { pool }
|
||||
}
|
||||
|
||||
pub fn migratable(&self) -> Arc<dyn RunMigrations> {
|
||||
Arc::new(Postgres::new(self.pool.clone()))
|
||||
}
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
use url::Url;
|
||||
|
||||
use super::DBOutPostgresAdapter;
|
||||
use crate::forge::auth::application::port::out::db::{
|
||||
errors::OutDBPortResult, save_oauth_state::SaveOAuthState,
|
||||
};
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl SaveOAuthState for DBOutPostgresAdapter {
|
||||
async fn save_oauth_state(
|
||||
&self,
|
||||
state: &str,
|
||||
oauth_provider: &str,
|
||||
redirect_uri: &Url,
|
||||
) -> OutDBPortResult<()> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO oauth_state (state, oauth_provider, redirect_uri) VALUES ($1, $2, $3)",
|
||||
state,
|
||||
oauth_provider,
|
||||
redirect_uri.as_str()
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_postgres_save_oauth_state() {
|
||||
let state = "statetestpostgres";
|
||||
let oauth_provider = "oauthprovitestpostgres";
|
||||
let redirect_uri = Url::parse("https://oauthprovitestpostgres").unwrap();
|
||||
|
||||
let settings = crate::settings::tests::get_settings().await;
|
||||
|
||||
let db = super::DBOutPostgresAdapter::new(
|
||||
sqlx::postgres::PgPool::connect(&settings.database.url)
|
||||
.await
|
||||
.unwrap(),
|
||||
);
|
||||
|
||||
db.save_oauth_state(state, oauth_provider, &redirect_uri)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
settings.drop_db().await;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,24 @@
|
|||
use url::Url;
|
||||
|
||||
use super::Forgejo;
|
||||
use crate::forge::auth::application::port::out::forge::{
|
||||
errors::OutForgePortResult, get_redirect_uri::GetRedirectUri,
|
||||
};
|
||||
|
||||
impl GetRedirectUri for Forgejo {
|
||||
fn get_redirect_uri(
|
||||
&self,
|
||||
state: &str,
|
||||
process_authorization_response_uri: &Url,
|
||||
) -> OutForgePortResult<Url> {
|
||||
let mut u = self.url().to_owned();
|
||||
u.set_path("/login/oauth/authorize");
|
||||
u.set_query(Some(&format!(
|
||||
"client_id={}&redirect_uri={}&response_type=code&state={state}",
|
||||
self.client_id(),
|
||||
process_authorization_response_uri.as_str()
|
||||
)));
|
||||
|
||||
Ok(u)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,32 @@
|
|||
use url::Url;
|
||||
|
||||
mod get_redirect_uri;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Forgejo {
|
||||
url: Url,
|
||||
client_id: String,
|
||||
client_secret: String,
|
||||
}
|
||||
|
||||
impl Forgejo {
|
||||
pub fn new(url: Url, client_id: String, client_secret: String) -> Self {
|
||||
Self {
|
||||
url,
|
||||
client_id,
|
||||
client_secret,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn url(&self) -> &Url {
|
||||
&self.url
|
||||
}
|
||||
|
||||
pub fn client_id(&self) -> &str {
|
||||
&self.client_id
|
||||
}
|
||||
|
||||
pub fn client_secret(&self) -> &str {
|
||||
&self.client_secret
|
||||
}
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
use std::collections::HashMap;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
|
||||
use derive_more::Display;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::forge::auth::application::port::out::forge::get_redirect_uri::GetRedirectUri;
|
||||
|
||||
use self::forgejo::Forgejo;
|
||||
|
||||
pub mod forgejo;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ForgeAdapter {
|
||||
pub get_redirect_uri_adapter: Arc<dyn GetRedirectUri>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct ForgeRepository {
|
||||
forges: HashMap<SupportedForges, ForgeAdapter>,
|
||||
}
|
||||
|
||||
impl ForgeRepository {
|
||||
fn add_forge(&mut self, name: SupportedForges, forge_factory: ForgeAdapter) {
|
||||
self.forges.insert(name, forge_factory);
|
||||
}
|
||||
|
||||
pub fn get_supported_forge_str(&self) -> Vec<String> {
|
||||
self.forges
|
||||
.clone()
|
||||
.into_keys()
|
||||
.map(|v| v.to_string())
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn get_supported_forges(&self) -> Vec<SupportedForges> {
|
||||
self.forges.clone().into_keys().collect()
|
||||
}
|
||||
|
||||
pub fn get_forge(&self, name: &SupportedForges) -> Option<&ForgeAdapter> {
|
||||
self.forges.get(name)
|
||||
}
|
||||
|
||||
pub fn new(forgejo: Forgejo) -> Self {
|
||||
let forgejo_adapter = ForgeAdapter {
|
||||
get_redirect_uri_adapter: Arc::new(forgejo),
|
||||
};
|
||||
let mut s = Self::default();
|
||||
s.add_forge(SupportedForges::Forgejo, forgejo_adapter);
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, Clone, Hash, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum SupportedForges {
|
||||
#[display(fmt = "forgejo")]
|
||||
Forgejo,
|
||||
}
|
||||
|
||||
impl FromStr for SupportedForges {
|
||||
type Err = SupportedForgesError;
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
match s.trim() {
|
||||
"forgejo" => Ok(SupportedForges::Forgejo),
|
||||
_ => Err(SupportedForgesError::UnsupportedForge),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, Clone, Hash, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum SupportedForgesError {
|
||||
UnsupportedForge,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_supported_forges() {
|
||||
assert_eq!(SupportedForges::Forgejo.to_string(), "forgejo");
|
||||
assert_eq!(
|
||||
SupportedForges::from_str("forgejo").unwrap(),
|
||||
SupportedForges::Forgejo
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,2 @@
|
|||
pub mod db;
|
||||
pub mod forge;
|
|
@ -0,0 +1,51 @@
|
|||
use actix_web::{
|
||||
http::{header, StatusCode},
|
||||
HttpResponse, HttpResponseBuilder, ResponseError,
|
||||
};
|
||||
use derive_more::Display;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::forge::auth::application::services::{
|
||||
process_authorization_response::errors::ProcessAuthorizationServiceError,
|
||||
request_authorization::errors::RequestAuthorizationServiceError,
|
||||
};
|
||||
|
||||
pub type InUIResult<V> = Result<V, InUIError>;
|
||||
|
||||
#[derive(Debug, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum InUIError {
|
||||
InternalServerError,
|
||||
BadRequest,
|
||||
}
|
||||
|
||||
impl From<ProcessAuthorizationServiceError> for InUIError {
|
||||
fn from(v: ProcessAuthorizationServiceError) -> Self {
|
||||
match v {
|
||||
ProcessAuthorizationServiceError::InteralError => Self::InternalServerError,
|
||||
ProcessAuthorizationServiceError::BadRequest => Self::BadRequest,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<RequestAuthorizationServiceError> for InUIError {
|
||||
fn from(v: RequestAuthorizationServiceError) -> Self {
|
||||
match v {
|
||||
RequestAuthorizationServiceError::InteralError => Self::InternalServerError,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ResponseError for InUIError {
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
HttpResponseBuilder::new(self.status_code())
|
||||
.append_header((header::CONTENT_TYPE, "application/sjon; charset=UTF-8"))
|
||||
.body(serde_json::to_string(self).unwrap())
|
||||
}
|
||||
|
||||
fn status_code(&self) -> actix_web::http::StatusCode {
|
||||
match self {
|
||||
Self::BadRequest => StatusCode::BAD_REQUEST,
|
||||
Self::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
use super::errors::*;
|
||||
use actix_web::HttpResponse;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait RequestAuthorizationInterface: Send + Sync {
|
||||
async fn request_oauth_authorization(&self, forge_name: String) -> InUIResult<HttpResponse>;
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
pub mod errors;
|
||||
pub mod login;
|
||||
// login
|
|
@ -0,0 +1,7 @@
|
|||
use super::errors::*;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait DeleteOAuthState: Send + Sync {
|
||||
/// Delete OAuth
|
||||
async fn delete_oauth_state(&self, state: &str, oauth_provider: &str) -> OutDBPortResult<()>;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
use derive_more::Display;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub type OutDBPortResult<V> = Result<V, OutDBPortError>;
|
||||
|
||||
#[derive(Debug, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum OutDBPortError {
|
||||
DuplicateState,
|
||||
}
|
|
@ -0,0 +1,7 @@
|
|||
use super::errors::*;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait GetOAuthState: Send + Sync {
|
||||
/// Get OAuth state code generated during authorization request
|
||||
async fn get_oauth_state(&self, state: &str) -> OutDBPortResult<()>;
|
||||
}
|
|
@ -0,0 +1,47 @@
|
|||
pub mod delete_oauth_state;
|
||||
pub mod errors;
|
||||
pub mod oauth_state_exists;
|
||||
pub mod save_oauth_state;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use std::sync::{Arc, RwLock};
|
||||
|
||||
use url::Url;
|
||||
|
||||
use super::*;
|
||||
|
||||
use errors::*;
|
||||
use save_oauth_state::SaveOAuthState;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct MockDB {
|
||||
pub calls: Arc<RwLock<Vec<Call>>>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct Call {
|
||||
pub state: String,
|
||||
pub oauth_provider: String,
|
||||
pub redirect_uri: String,
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl SaveOAuthState for MockDB {
|
||||
async fn save_oauth_state(
|
||||
&self,
|
||||
state: &str,
|
||||
oauth_provider: &str,
|
||||
redirect_uri: &Url,
|
||||
) -> OutDBPortResult<()> {
|
||||
let mut calls = self.calls.write().unwrap();
|
||||
calls.push(Call {
|
||||
state: state.to_string(),
|
||||
oauth_provider: oauth_provider.to_string(),
|
||||
redirect_uri: redirect_uri.to_string(),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
use url::Url;
|
||||
|
||||
use super::errors::*;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait OAuthStateExists: Send + Sync {
|
||||
/// Save OAuth state code generated during authorization request, which will later be used to
|
||||
/// validate authorization response from OAuth server
|
||||
async fn oauth_state_exists(
|
||||
&self,
|
||||
state: &str,
|
||||
oauth_provider: &str,
|
||||
redirect_uri: &Url,
|
||||
) -> OutDBPortResult<bool>;
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
use url::Url;
|
||||
|
||||
use super::errors::*;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait SaveOAuthState: Send + Sync {
|
||||
/// Save OAuth state code generated during authorization request, which will later be used to
|
||||
/// validate authorization response from OAuth server
|
||||
async fn save_oauth_state(
|
||||
&self,
|
||||
state: &str,
|
||||
oauth_provider: &str,
|
||||
redirect_uri: &Url,
|
||||
) -> OutDBPortResult<()>;
|
||||
}
|
|
@ -0,0 +1,9 @@
|
|||
use derive_more::Display;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub type OutForgePortResult<V> = Result<V, OutForgePortError>;
|
||||
|
||||
#[derive(Debug, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum OutForgePortError {
|
||||
InteralError,
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
use super::errors::*;
|
||||
use url::Url;
|
||||
|
||||
pub trait GetRedirectUri: Send + Sync {
|
||||
fn get_redirect_uri(
|
||||
&self,
|
||||
state: &str,
|
||||
process_authorization_response_uri: &Url,
|
||||
) -> OutForgePortResult<Url>;
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
pub mod errors;
|
||||
pub mod get_redirect_uri;
|
||||
pub mod refresh_access_token;
|
||||
pub mod request_access_token;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use url::Url;
|
||||
|
||||
use super::*;
|
||||
|
||||
use errors::*;
|
||||
use get_redirect_uri::GetRedirectUri;
|
||||
|
||||
#[derive(Clone, Default)]
|
||||
pub struct MockForge; // {
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl GetRedirectUri for MockForge {
|
||||
fn get_redirect_uri(
|
||||
&self,
|
||||
state: &str,
|
||||
process_authorization_response_uri: &Url,
|
||||
) -> OutForgePortResult<Url> {
|
||||
let mut u = process_authorization_response_uri.clone();
|
||||
u.set_query(Some(&format!("state={state}")));
|
||||
Ok(u)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,19 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::errors::*;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct RequestAuthorizationCommand {
|
||||
oauth_provider: String,
|
||||
}
|
||||
|
||||
impl RequestAuthorizationCommand {
|
||||
pub fn oauth_provider(&self) -> &str {
|
||||
&self.oauth_provider
|
||||
}
|
||||
|
||||
pub fn new_command(oauth_provider: String) -> RequestAuthorizationServiceResult<Self> {
|
||||
let oauth_provider = oauth_provider.trim().to_string();
|
||||
Ok(Self { oauth_provider })
|
||||
}
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
use derive_more::Display;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::forge::auth::application::port::out::db::errors::OutDBPortError;
|
||||
use crate::forge::auth::application::port::out::forge::errors::OutForgePortError;
|
||||
|
||||
pub type RequestAuthorizationServiceResult<V> = Result<V, RequestAuthorizationServiceError>;
|
||||
|
||||
#[derive(Debug, Display, Clone, Serialize, Deserialize, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum RequestAuthorizationServiceError {
|
||||
InteralError,
|
||||
}
|
||||
|
||||
impl From<OutDBPortError> for RequestAuthorizationServiceError {
|
||||
fn from(v: OutDBPortError) -> Self {
|
||||
match v {
|
||||
OutDBPortError::DuplicateState => Self::InteralError,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<OutForgePortError> for RequestAuthorizationServiceError {
|
||||
fn from(v: OutForgePortError) -> Self {
|
||||
match v {
|
||||
OutForgePortError::InteralError => Self::InteralError,
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
use url::Url;
|
||||
|
||||
pub mod command;
|
||||
pub mod errors;
|
||||
pub mod service;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
pub trait RequestAuthorizationUserCase: Send + Sync {
|
||||
async fn request_authorization(
|
||||
&self,
|
||||
cmd: command::RequestAuthorizationCommand,
|
||||
) -> errors::RequestAuthorizationServiceResult<Url>;
|
||||
}
|
|
@ -0,0 +1,104 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use url::Url;
|
||||
|
||||
use crate::forge::auth::application::port::out::db::errors::OutDBPortError;
|
||||
use crate::forge::auth::application::port::out::db::save_oauth_state::SaveOAuthState;
|
||||
use crate::forge::auth::application::port::out::forge::get_redirect_uri::GetRedirectUri;
|
||||
use crate::utils;
|
||||
|
||||
use super::{errors::*, RequestAuthorizationUserCase};
|
||||
|
||||
const STATE_LEN: usize = 8;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct RequestAuthorizationService {
|
||||
save_oauth_state_adapter: Arc<dyn SaveOAuthState>,
|
||||
get_redirect_uri_adapter: Arc<dyn GetRedirectUri>,
|
||||
process_authorization_response_redirect_uri: Url,
|
||||
}
|
||||
|
||||
impl RequestAuthorizationService {
|
||||
pub fn new(
|
||||
save_oauth_state_adapter: Arc<dyn SaveOAuthState>,
|
||||
get_redirect_uri_adapter: Arc<dyn GetRedirectUri>,
|
||||
process_authorization_response_redirect_uri: Url,
|
||||
) -> Self {
|
||||
Self {
|
||||
save_oauth_state_adapter,
|
||||
get_redirect_uri_adapter,
|
||||
process_authorization_response_redirect_uri,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl RequestAuthorizationUserCase for RequestAuthorizationService {
|
||||
async fn request_authorization(
|
||||
&self,
|
||||
cmd: super::command::RequestAuthorizationCommand,
|
||||
) -> RequestAuthorizationServiceResult<Url> {
|
||||
let mut state = utils::get_random(STATE_LEN);
|
||||
loop {
|
||||
match self
|
||||
.save_oauth_state_adapter
|
||||
.save_oauth_state(
|
||||
&state,
|
||||
cmd.oauth_provider(),
|
||||
&self.process_authorization_response_redirect_uri,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Err(OutDBPortError::DuplicateState) => {
|
||||
state = utils::get_random(STATE_LEN);
|
||||
continue;
|
||||
}
|
||||
Ok(_) => break,
|
||||
Err(e) => return Err(e)?,
|
||||
}
|
||||
}
|
||||
let redirect = self
|
||||
.get_redirect_uri_adapter
|
||||
.get_redirect_uri(&state, &self.process_authorization_response_redirect_uri)?;
|
||||
Ok(redirect)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::forge::auth::application::{
|
||||
port::out::{db::tests::MockDB, forge::tests::MockForge},
|
||||
services::request_authorization::command::RequestAuthorizationCommand,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_service() {
|
||||
let save_oauth_state = MockDB::default();
|
||||
let get_redirect_uri = MockForge::default();
|
||||
let url = Url::parse("http://test_service_request_auth").unwrap();
|
||||
let oauth_provider = "test_service_request_auth_oauth_provider";
|
||||
|
||||
let s = RequestAuthorizationService::new(
|
||||
Arc::new(save_oauth_state.clone()),
|
||||
Arc::new(get_redirect_uri),
|
||||
url.clone(),
|
||||
);
|
||||
let cmd = RequestAuthorizationCommand::new_command(oauth_provider.to_owned()).unwrap();
|
||||
|
||||
let res = s.request_authorization(cmd).await.unwrap().to_string();
|
||||
{
|
||||
let save_oauth_state_ctx = save_oauth_state.calls.read().unwrap();
|
||||
let call = save_oauth_state_ctx.get(0).unwrap();
|
||||
assert_eq!(
|
||||
res,
|
||||
format!("http://test_service_request_auth/?state={}", &call.state)
|
||||
);
|
||||
assert_eq!(call.oauth_provider, oauth_provider);
|
||||
assert_eq!(call.redirect_uri, url.to_string());
|
||||
}
|
||||
}
|
||||
}
|
57
src/main.rs
57
src/main.rs
|
@ -1,3 +1,56 @@
|
|||
fn main() {
|
||||
println!("Hello, world!");
|
||||
use std::env;
|
||||
|
||||
use actix_web::{middleware, App, HttpServer};
|
||||
|
||||
mod db;
|
||||
mod forge;
|
||||
mod settings;
|
||||
mod utils;
|
||||
|
||||
pub use crate::forge::auth::adapter::input::web::{services, ActixCtx, WebCtx};
|
||||
pub use crate::forge::auth::adapter::out::{
|
||||
db::DBAdapter,
|
||||
forge::{forgejo::Forgejo, ForgeRepository},
|
||||
};
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() {
|
||||
let settings = settings::Settings::new().unwrap();
|
||||
if env::var("RUST_LOG").is_err() {
|
||||
env::set_var("RUST_LOG", &settings.log);
|
||||
}
|
||||
{
|
||||
// Settings::new() outputs logs, but since we are only setting up logger _after_ Settings is
|
||||
// initialized, this dummy reinitialization will output logs.
|
||||
settings::Settings::new().unwrap();
|
||||
}
|
||||
|
||||
pretty_env_logger::init();
|
||||
|
||||
let forgejo = Forgejo::new(
|
||||
settings.forges.forgejo.url.clone(),
|
||||
settings.forges.forgejo.client_id.clone(),
|
||||
settings.forges.forgejo.client_secret.clone(),
|
||||
);
|
||||
let forges = ForgeRepository::new(forgejo);
|
||||
let db = DBAdapter::init(&settings.database.url).await;
|
||||
db.migratable().migrate().await;
|
||||
let ctx = WebCtx::new_actix_ctx(forges, db, settings.clone());
|
||||
|
||||
let socket_addr = settings.server.get_ip();
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.wrap(tracing_actix_web::TracingLogger::default())
|
||||
.wrap(middleware::Compress::default())
|
||||
.app_data(ctx.clone())
|
||||
.wrap(
|
||||
middleware::DefaultHeaders::new().add(("Permissions-Policy", "interest-cohort=()")),
|
||||
)
|
||||
.configure(services)
|
||||
})
|
||||
.bind(&socket_addr)
|
||||
.unwrap()
|
||||
.run()
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
use std::env;
|
||||
|
||||
use config::{builder::DefaultState, ConfigBuilder, ConfigError};
|
||||
use derive_more::Display;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
#[derive(Deserialize, Serialize, Display, Eq, PartialEq, Clone, Debug)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum DBType {
|
||||
#[display(fmt = "postgres")]
|
||||
Postgres,
|
||||
}
|
||||
|
||||
impl DBType {
|
||||
fn from_url(url: &Url) -> Result<Self, ConfigError> {
|
||||
match url.scheme() {
|
||||
// "mysql" => Ok(Self::Maria),
|
||||
"postgres" => Ok(Self::Postgres),
|
||||
_ => Err(ConfigError::Message("Unknown database type".into())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Eq, PartialEq)]
|
||||
pub struct Database {
|
||||
pub url: String,
|
||||
pub pool: u32,
|
||||
pub database_type: DBType,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
pub fn env_override(mut s: ConfigBuilder<DefaultState>) -> ConfigBuilder<DefaultState> {
|
||||
for (parameter, env_var_name) in [
|
||||
("database.url", "DATABASE_URL"),
|
||||
("database.pool", "FORGEFLUX_database_POOL"),
|
||||
]
|
||||
.iter()
|
||||
{
|
||||
if let Ok(val) = env::var(env_var_name) {
|
||||
log::debug!("Overriding [{parameter}] with environment variable {env_var_name}");
|
||||
s = s.set_override(parameter, val).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
s
|
||||
}
|
||||
|
||||
pub fn set_database_type(&mut self) {
|
||||
let url =
|
||||
Url::parse(&self.url).expect("couldn't parse Database URL and detect database type");
|
||||
self.database_type = DBType::from_url(&url).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::env_helper;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_db_env_override() {
|
||||
let init_settings = crate::settings::Settings::new().unwrap();
|
||||
env_helper!(
|
||||
init_settings,
|
||||
"DATABASE_URL",
|
||||
"postgres://test_db_env_override",
|
||||
database.url
|
||||
);
|
||||
env_helper!(init_settings, "FORGEFLUX_database_POOL", 99, database.pool);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,67 @@
|
|||
use std::env;
|
||||
|
||||
use config::{builder::DefaultState, ConfigBuilder};
|
||||
use serde::Deserialize;
|
||||
use url::Url;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Eq, PartialEq)]
|
||||
pub struct Forgejo {
|
||||
pub url: Url,
|
||||
pub client_id: String,
|
||||
pub client_secret: String,
|
||||
}
|
||||
|
||||
impl Forgejo {
|
||||
pub fn env_override(mut s: ConfigBuilder<DefaultState>) -> ConfigBuilder<DefaultState> {
|
||||
for (parameter, env_var_name) in [
|
||||
("forges.forgejo.url", "FORGEFLUX_forges_FORGEJO_url"),
|
||||
(
|
||||
"forges.forgejo.client_id",
|
||||
"FORGEFLUX_forges_FORGEJO_client_id",
|
||||
),
|
||||
(
|
||||
"forges.forgejo.client_secret",
|
||||
"FORGEFLUX_forges_FORGEJO_client_secret",
|
||||
),
|
||||
]
|
||||
.iter()
|
||||
{
|
||||
if let Ok(val) = env::var(env_var_name) {
|
||||
log::debug!("Overriding [{parameter}] with environment variable {env_var_name}");
|
||||
s = s.set_override(parameter, val).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::env_helper;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_forges_forgejo_env_override() {
|
||||
let init_settings = crate::settings::Settings::new().unwrap();
|
||||
env_helper!(
|
||||
init_settings,
|
||||
"FORGEFLUX_forges_FORGEJO_url",
|
||||
Url::parse("postgres://test_forges_forgejo_env_override").unwrap(),
|
||||
forges.forgejo.url
|
||||
);
|
||||
env_helper!(
|
||||
init_settings,
|
||||
"FORGEFLUX_forges_FORGEJO_client_id",
|
||||
"test_forges_forgejo_env_override_client_id",
|
||||
forges.forgejo.client_id
|
||||
);
|
||||
env_helper!(
|
||||
init_settings,
|
||||
"FORGEFLUX_forges_FORGEJO_client_secret",
|
||||
"test_forges_forgejo_env_override_client_secret",
|
||||
forges.forgejo.client_secret
|
||||
);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
use config::{builder::DefaultState, ConfigBuilder};
|
||||
use serde::Deserialize;
|
||||
use std::env;
|
||||
|
||||
pub mod forgejo;
|
||||
|
||||
use forgejo::Forgejo;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Eq, PartialEq)]
|
||||
pub struct Forges {
|
||||
pub forgejo: Forgejo,
|
||||
}
|
||||
|
||||
impl Forges {
|
||||
pub fn env_override(mut s: ConfigBuilder<DefaultState>) -> ConfigBuilder<DefaultState> {
|
||||
Forgejo::env_override(s)
|
||||
}
|
||||
}
|
|
@ -0,0 +1,162 @@
|
|||
use std::path::Path;
|
||||
use std::{env, fs};
|
||||
|
||||
use config::builder::DefaultState;
|
||||
use config::{Config, ConfigBuilder, ConfigError, File};
|
||||
|
||||
use serde::Deserialize;
|
||||
use url::Url;
|
||||
|
||||
pub mod database;
|
||||
pub mod forges;
|
||||
pub mod server;
|
||||
|
||||
use database::{DBType, Database};
|
||||
use forges::Forges;
|
||||
use server::Server;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Eq, PartialEq)]
|
||||
pub struct Settings {
|
||||
pub debug: bool,
|
||||
pub log: String,
|
||||
pub source_code: String,
|
||||
pub allow_registration: bool,
|
||||
pub database: Database,
|
||||
pub forges: Forges,
|
||||
pub server: Server,
|
||||
// pub smtp: Option<Smtp>,
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
fn env_override(mut s: ConfigBuilder<DefaultState>) -> ConfigBuilder<DefaultState> {
|
||||
for (parameter, env_var_name) in [
|
||||
("debug", "FORGEFLUX_debug"),
|
||||
("source_code", "FORGEFLUX_source_code"),
|
||||
("allow_registration", "FORGEFLUX_allow_registration"),
|
||||
]
|
||||
.iter()
|
||||
{
|
||||
if let Ok(val) = env::var(env_var_name) {
|
||||
log::debug!("Overriding [{parameter}] with environment variable {env_var_name}");
|
||||
s = s.set_override(parameter, val).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
s = Database::env_override(s);
|
||||
s = Forges::env_override(s);
|
||||
Server::env_override(s)
|
||||
}
|
||||
|
||||
pub fn new() -> Result<Self, ConfigError> {
|
||||
let mut s = Config::builder();
|
||||
|
||||
const CURRENT_DIR: &str = "./config/default.toml";
|
||||
const ETC: &str = "/etc/forgeflux/config.toml";
|
||||
|
||||
// Will be overridden after config is parsed and loaded into Settings by
|
||||
// Settings::set_database_type.
|
||||
// This parameter is not ergonomic for users, but it is required and can be programatically
|
||||
// inferred. But we need a default value for config lib to parse successfully, since it is
|
||||
// DBType and not Option<DBType>
|
||||
s = s
|
||||
.set_default("database.database_type", DBType::Postgres.to_string())
|
||||
.expect("unable to set database.database_type default config");
|
||||
|
||||
s = s
|
||||
.set_default("log", "INFO")
|
||||
.expect("unable to set log default config");
|
||||
|
||||
if let Ok(path) = env::var("FORGEFLUX_CONFIG") {
|
||||
let absolute_path = Path::new(&path).canonicalize().unwrap();
|
||||
log::info!(
|
||||
"Loading config file from {}",
|
||||
absolute_path.to_str().unwrap()
|
||||
);
|
||||
s = s.add_source(File::with_name(absolute_path.to_str().unwrap()));
|
||||
} else if Path::new(CURRENT_DIR).exists() {
|
||||
let absolute_path = fs::canonicalize(CURRENT_DIR).unwrap();
|
||||
log::info!(
|
||||
"Loading config file from {}",
|
||||
absolute_path.to_str().unwrap()
|
||||
);
|
||||
// merging default config from file
|
||||
s = s.add_source(File::with_name(absolute_path.to_str().unwrap()));
|
||||
} else if Path::new(ETC).exists() {
|
||||
log::info!("{}", format!("Loading config file from {}", ETC));
|
||||
s = s.add_source(File::with_name(ETC));
|
||||
} else {
|
||||
log::warn!("Configuration file not found");
|
||||
}
|
||||
|
||||
s = Self::env_override(s);
|
||||
|
||||
let mut settings = s.build()?.try_deserialize::<Settings>()?;
|
||||
settings.check_url();
|
||||
|
||||
settings.database.set_database_type();
|
||||
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
fn check_url(&self) {
|
||||
Url::parse(&self.source_code).expect("Please enter a URL for source_code in settings");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use url::Url;
|
||||
|
||||
use super::Settings;
|
||||
use crate::db::create_database::CreateDatabase;
|
||||
use crate::db::delete_database::DeleteDatabase;
|
||||
use crate::db::migrate::RunMigrations;
|
||||
use crate::db::sqlx_postgres::Postgres;
|
||||
use crate::utils::get_random;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! env_helper {
|
||||
($init_settings:ident, $env:expr, $val:expr, $val_typed:expr, $($param:ident).+) => {
|
||||
println!("Setting env var {} to {} for test", $env, $val);
|
||||
env::set_var($env, $val);
|
||||
{
|
||||
let new_settings = crate::settings::Settings::new().unwrap();
|
||||
assert_eq!(new_settings.$($param).+, $val_typed, "should match");
|
||||
assert_ne!(new_settings.$($param).+, $init_settings.$($param).+);
|
||||
}
|
||||
env::remove_var($env);
|
||||
};
|
||||
|
||||
|
||||
($init_settings:ident, $env:expr, $val:expr, $($param:ident).+) => {
|
||||
env_helper!($init_settings, $env, $val.to_string(), $val, $($param).+);
|
||||
};
|
||||
}
|
||||
|
||||
pub async fn get_settings() -> Settings {
|
||||
let mut settings = Settings::new().unwrap();
|
||||
let mut db_url = Url::parse(&settings.database.url).unwrap();
|
||||
db_url.set_path(&get_random(12));
|
||||
settings.database.url = db_url.to_string();
|
||||
|
||||
crate::db::sqlx_postgres::PostgresDatabase
|
||||
.create_database(&db_url)
|
||||
.await;
|
||||
let db = Postgres::new(
|
||||
sqlx::postgres::PgPool::connect(&settings.database.url)
|
||||
.await
|
||||
.unwrap(),
|
||||
);
|
||||
db.migrate().await;
|
||||
|
||||
settings
|
||||
}
|
||||
|
||||
impl Settings {
|
||||
pub async fn drop_db(&self) {
|
||||
crate::db::sqlx_postgres::PostgresDatabase
|
||||
.delete_database(&Url::parse(&self.database.url).unwrap())
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
use config::{builder::DefaultState, ConfigBuilder};
|
||||
use serde::Deserialize;
|
||||
use std::env;
|
||||
|
||||
#[derive(Debug, Clone, Deserialize, Eq, PartialEq)]
|
||||
pub struct Server {
|
||||
pub port: u32,
|
||||
pub domain: String,
|
||||
pub ip: String,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
pub fn get_ip(&self) -> String {
|
||||
format!("{}:{}", self.ip, self.port)
|
||||
}
|
||||
|
||||
pub fn env_override(mut s: ConfigBuilder<DefaultState>) -> ConfigBuilder<DefaultState> {
|
||||
for (parameter, env_var_name) in [
|
||||
("server.port", "PORT"),
|
||||
("server.domain", "FORGEFLUX_server_DOMAIN"),
|
||||
("server.ip", "FORGEFLUX_server_IP"),
|
||||
]
|
||||
.iter()
|
||||
{
|
||||
if let Ok(val) = env::var(env_var_name) {
|
||||
log::debug!("Overriding [{parameter}] with environment variable {env_var_name}");
|
||||
s = s.set_override(parameter, val).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
s
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::env_helper;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_server_env_override() {
|
||||
let init_settings = crate::settings::Settings::new().unwrap();
|
||||
env_helper!(init_settings, "PORT", 22, server.port);
|
||||
env_helper!(
|
||||
init_settings,
|
||||
"FORGEFLUX_server_DOMAIN",
|
||||
"test_server_env_override.org",
|
||||
server.domain
|
||||
);
|
||||
env_helper!(init_settings, "FORGEFLUX_server_IP", "1.1.1.1", server.ip);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,13 @@
|
|||
pub fn get_random(len: usize) -> String {
|
||||
use std::iter;
|
||||
|
||||
use rand::{distributions::Alphanumeric, rngs::ThreadRng, thread_rng, Rng};
|
||||
|
||||
let mut rng: ThreadRng = thread_rng();
|
||||
|
||||
iter::repeat(())
|
||||
.map(|()| rng.sample(Alphanumeric))
|
||||
.map(char::from)
|
||||
.take(len)
|
||||
.collect::<String>()
|
||||
}
|
|
@ -0,0 +1 @@
|
|||
/target
|
File diff suppressed because it is too large
Load Diff
|
@ -0,0 +1,10 @@
|
|||
[package]
|
||||
name = "db-migrations"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
actix-rt = "2.9.0"
|
||||
sqlx = { version = "0.7.4", features = ["runtime-tokio-rustls", "postgres", "time"] }
|
|
@ -0,0 +1,24 @@
|
|||
use std::env;
|
||||
|
||||
use sqlx::postgres::PgPoolOptions;
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
#[actix_rt::main]
|
||||
async fn main() {
|
||||
//TODO featuregate sqlite and postgres
|
||||
postgres_migrate().await;
|
||||
}
|
||||
|
||||
async fn postgres_migrate() {
|
||||
let db_url = env::var("DATABASE_URL").expect("set DATABASE_URL env var");
|
||||
let db = PgPoolOptions::new()
|
||||
.max_connections(2)
|
||||
.connect(&db_url)
|
||||
.await
|
||||
.expect("Unable to form database pool");
|
||||
|
||||
sqlx::migrate!("../../migrations/")
|
||||
.run(&db)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
Loading…
Reference in New Issue