Skip to content

Commit

Permalink
implement session manager within db middleware, update mocker with a …
Browse files Browse the repository at this point in the history
…new flow with session tokens
  • Loading branch information
Kirill-K-1 committed Dec 29, 2023
1 parent ca86caa commit 0bc4eb8
Show file tree
Hide file tree
Showing 25 changed files with 1,463 additions and 247 deletions.
782 changes: 604 additions & 178 deletions Cargo.lock

Large diffs are not rendered by default.

6 changes: 5 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
members = [
"user-verifier",
"gov-portal-mocker",
"gov-portal-db",
"shared"
]
resolver = "2"
Expand All @@ -11,7 +12,6 @@ resolver = "2"
tokio = { version = "1", features = ["rt-multi-thread", "time", "macros"] }

# Ethereum
web3 = "0.19.0"
ethabi = "18.0.0"
ethereum-types = "0.14.1"
jsonrpc-core = "18.0.0"
Expand All @@ -25,6 +25,7 @@ sha3 = "0"
axum = "0"
tower-http = { version = "0", features = ["cors"] }
reqwest = { version = "0.11", features = ["rustls-tls", "json", "deflate"] }
jsonwebtoken = "9"

# SerDe
serde = { version = "1.0", features = ["derive"] }
Expand All @@ -35,6 +36,9 @@ tracing = "0.1"
tracing-subscriber = { version = "0.3", features = [ "env-filter", "registry" ] }
log = "0"

# Persistence
mongodb = "2"

# Misc
backtrace = "0"
uuid = "1"
Expand Down
26 changes: 25 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@

User Verifier service provides an endpoint `/verify` which allows to verify if the response acquired from Fractal by using their user uniqueness check is valid and eligible to mint Human SBT. It responses with an encoded and signed Human SBT request to be used with Human SBT issuer smart contract call `sbtMint`.

### Gov Portal DB

Back-end service which responsive for session token generation and being a middleware between web app and database.

### Gov Portal Mocker

Mocker for Gov Portal web app which allows user to check their uniqueness with Fractal identity system and verify the response with User Verifier service.
Expand Down Expand Up @@ -37,18 +41,34 @@ Default configuration could be found in `./config/default.json` file.

Default configuration and credentials could be overriden by using `./config/custom.json` file.

### Gov Portal DB

#### Default configuration

Default configuration could be found in `./gov-portal-mocker/config/default.json` file.

- `listenAddress`: host:port to run the gov portal database middleware at. Defaults to `localhost:10001`
- `session`: session manager configuration
- `secret`: secret to generate session tokens. Should be set before app start
- `duration`: lifetime duration in seconds for which the session token will be valid to access database by using middleware. Defaults to 1 day

#### Override configuration

Default configuration and credentials could be overriden by using `./config/custom.json` file.

### Gov Portal Mocker

#### Default configuration

Default configuration could be found in `./gov-portal-mocker/config/default.json` file.

- `listenAddress`: host:port to run the gov portal web app mocker at. Defaults to `localhost:8080`
- `userDb`: connection settings to AirDAO Gov Portal DB
- `baseUrl`: base url to connect to database middleware. Defaults to `http://localhost:10001`
- `signer`: signer configuration
- `url`: base url for User Verifier service. Defaults to `http://localhost:10000`
- `redirectUri`: redirect url used by Fractal to redirect users back after uniqueness check. Could be found in Admin section at Fractal web app for developers as `Authorization callback URL`. Defaults to `http://localhost:8080/auth`
- `fractalClientId`: client id for Fractal integration. Could be found in Admin section at Fractal web app for developers. Should be set before app start
- `fakeAmbWalletAddress`: some EVM-kind Ambrosus wallet address to bind Human SBT to.
- `web`:
- `pages`: key-value table with templates for mocker web app pages, where key is an endpoint name and value is file where template content located

Expand All @@ -66,6 +86,10 @@ Supported logging levels: `info`, `debug`, `trace`. Defaults to `info` log level

While being inside repo root directory run `cargo run`. Could be run with `RUST_LOG` env variable to set logging level, e.g. `RUST_LOG=debug cargo run`.

### Gov Portal DB

While being inside repo root directory run `cargo run --bin airdao-gov-portal-db`. Could be run with `RUST_LOG` env variable to set logging level, e.g. `RUST_LOG=trace cargo run --bin airdao-gov-portal-db`.

### Gov Portal Mocker

While being inside repo root directory run `cargo run --bin airdao-gov-portal-mocker`. Could be run with `RUST_LOG` env variable to set logging level, e.g. `RUST_LOG=trace cargo run --bin airdao-gov-portal-mocker`.
Expand Down
50 changes: 50 additions & 0 deletions gov-portal-db/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
[package]
name = "airdao-gov-portal-db"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]
# Local
shared = { path = "../shared/" }

# Async
tokio = { workspace = true }

# Ethereum
ethabi = { workspace = true }
ethereum-types = { workspace = true }

# Crypto
k256 = { workspace = true }
ecdsa = { workspace = true }
sha3 = { workspace = true }

# Web
axum = { workspace = true }
tower-http = { workspace = true }
reqwest = { workspace = true }
jsonwebtoken = { workspace = true }

# SerDe
serde = { workspace = true }
serde_json = { workspace = true }

# Logging
tracing = { workspace = true }
tracing-subscriber = { workspace = true }
log = { workspace = true }

# Persistence
mongodb = { workspace = true }

# Misc
backtrace = { workspace = true }
uuid = { workspace = true }
config = { workspace = true }
chrono = { workspace = true }
thiserror = { workspace = true }
hex = { workspace = true }
base64 = { workspace = true }
anyhow = { workspace = true }
7 changes: 7 additions & 0 deletions gov-portal-db/config/default.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"listenAddress": "0.0.0.0:10001",
"session": {
"secret": null,
"duration": 86400
}
}
10 changes: 10 additions & 0 deletions gov-portal-db/src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use serde::Deserialize;

use crate::session_token::SessionConfig;

#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct AppConfig {
pub listen_address: String,
pub session: SessionConfig,
}
31 changes: 31 additions & 0 deletions gov-portal-db/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
use axum::{http::StatusCode, response::IntoResponse, Json};
use serde_json::json;

#[derive(thiserror::Error, Debug)]
pub enum AppError {
#[error("JSON parse failure: {0}")]
ParseError(#[from] serde_json::Error),
#[error("Generic error: {0}")]
Generic(String),
#[error("Server error: {0}")]
ServerError(#[from] std::io::Error),
}

impl IntoResponse for AppError {
fn into_response(self) -> axum::response::Response {
let (status, err_msg) = match self {
Self::ParseError(_) | Self::ServerError(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
"Internal server error".to_owned(),
),
Self::Generic(e) => (StatusCode::UNAUTHORIZED, format!("Request failure: {e}")),
};
(status, Json(json!({ "error": err_msg }))).into_response()
}
}

impl From<String> for AppError {
fn from(error_str: String) -> Self {
Self::Generic(error_str)
}
}
19 changes: 19 additions & 0 deletions gov-portal-db/src/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
mod config;
mod error;
mod mongo_client;
mod server;
mod session_token;

use shared::{logger, utils};

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
logger::init();
utils::set_heavy_panic();

let config = utils::load_config::<config::AppConfig>("./gov-portal-db").await?;

server::start(config).await?;

Ok(())
}
89 changes: 89 additions & 0 deletions gov-portal-db/src/mongo_client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
use mongodb::{
bson::Document,
options::{
ClientOptions, ConnectionString, FindOptions, InsertOneOptions, UpdateModifications,
UpdateOptions,
},
results::{InsertOneResult, UpdateResult},
Client, Collection, Cursor,
};
use serde::Deserialize;

#[derive(Clone)]
pub struct MongoClient {
collection: Collection<Document>,
pub req_timeout: std::time::Duration,
}

#[derive(Deserialize, Debug, Default)]
#[serde(rename_all = "camelCase")]
pub struct MongoConfig {
/// MongoDB connection url, e.g. mongodb+srv://...
#[serde(default)]
pub url: Option<ConnectionString>,
/// MongoDB database name
pub db_name: String,
/// MongoDB user profiles collection name
pub collection_name: String,
#[serde(default = "default_request_timeout")]
pub request_timeout: u64,
}

fn default_request_timeout() -> u64 {
10_000
}

impl MongoClient {
pub async fn init(mut config: MongoConfig) -> anyhow::Result<Self> {
if let Ok(conn_str) = std::env::var("MONGO_CONN_URL") {
// Parse a connection string into an options struct.
config.url = Some(ConnectionString::parse(conn_str)?);
}

let client_options = ClientOptions::parse_connection_string(
config
.url
.ok_or_else(|| anyhow::Error::msg("Mongo connection url is missed"))?,
)
.await?;

// Get a handle to the deployment.
let inner = Client::with_options(client_options)?;

// Get a handle to a database.
let db = inner.database(&config.db_name);

// Get a handle to a collection in the database.
let collection = db.collection::<Document>(&config.collection_name);

Ok(Self {
collection,
req_timeout: std::time::Duration::from_millis(config.request_timeout),
})
}

pub async fn find(
&self,
filter: impl Into<Option<Document>>,
find_options: impl Into<Option<FindOptions>>,
) -> Result<Cursor<Document>, mongodb::error::Error> {
self.collection.find(filter, find_options).await
}

pub async fn insert_one(
&self,
doc: impl std::borrow::Borrow<Document>,
options: impl Into<Option<InsertOneOptions>>,
) -> Result<InsertOneResult, mongodb::error::Error> {
self.collection.insert_one(doc, options).await
}

pub async fn update_one(
&self,
query: Document,
update: impl Into<UpdateModifications>,
options: impl Into<Option<UpdateOptions>>,
) -> Result<UpdateResult, mongodb::error::Error> {
self.collection.update_one(query, update, options).await
}
}
86 changes: 86 additions & 0 deletions gov-portal-db/src/server.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
use axum::{extract::State, routing::post, Json, Router};
use serde::Deserialize;
use shared::common::{SessionToken, User};
use tower_http::cors::CorsLayer;

use crate::{config::AppConfig, error::AppError, session_token::SessionManager};

#[derive(Clone)]
pub struct AppState {
pub config: AppConfig,
pub session_manager: SessionManager,
}

impl AppState {
pub fn new(config: AppConfig) -> Result<Self, AppError> {
Ok(Self {
session_manager: SessionManager::new(config.session.clone()),
config,
})
}
}

#[derive(Debug, Deserialize)]
#[serde(untagged)]
pub enum TokenQuery {
Message { data: String },
NoMessage {},
}

pub async fn start(config: AppConfig) -> Result<(), AppError> {
let addr = config
.listen_address
.parse::<std::net::SocketAddr>()
.expect("Can't parse socket address");

let state = AppState::new(config.clone())?;

let app = Router::new()
.route("/token", post(token_route))
.route("/user", post(user_route))
.layer(CorsLayer::permissive())
.with_state(state);

tracing::debug!("Server listening on {}", addr);

let listener = tokio::net::TcpListener::bind(addr).await?;

axum::serve(listener, app).await.map_err(AppError::from)
}

async fn token_route(
State(state): State<AppState>,
Json(req): Json<TokenQuery>,
) -> Result<Json<SessionToken>, String> {
tracing::debug!("Request {req:?}");

let res = match req {
TokenQuery::Message { data } => state
.session_manager
.acquire_token(&data)
.map_err(|e| e.to_string()),
TokenQuery::NoMessage {} => Err("Resource Not Found".to_owned()),
};

tracing::debug!("Response {res:?}");

res.map(Json)
}

async fn user_route(
State(state): State<AppState>,
Json(token): Json<SessionToken>,
) -> Result<Json<User>, String> {
tracing::debug!("Request {token:?}");

// TODO: fetch user information from MongoDB
let user = state
.session_manager
.verify_token(&token)
.map(|wallet| User { wallet })
.map_err(|e| e.to_string());

tracing::debug!("Response {user:?}");

user.map(Json)
}
Loading

0 comments on commit 0bc4eb8

Please sign in to comment.