Skip to content

Commit

Permalink
feat: added schema and crl apis for organisation (#322)
Browse files Browse the repository at this point in the history
  • Loading branch information
sauraww authored and Datron committed Jan 2, 2025
1 parent 474037e commit 9270000
Show file tree
Hide file tree
Showing 15 changed files with 326 additions and 5 deletions.
5 changes: 3 additions & 2 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ HOSTNAME="<application_name>-<deployment_id>-<replicaset>-<pod>"
ACTIX_KEEP_ALIVE=120
MAX_DB_CONNECTION_POOL_SIZE=3
ENABLE_TENANT_AND_SCOPE=true
TENANTS=dev,test
TENANTS=dev,test,superposition
TENANT_MIDDLEWARE_EXCLUSION_LIST="/health,/assets/favicon.ico,/pkg/frontend.js,/pkg,/pkg/frontend_bg.wasm,/pkg/tailwind.css,/pkg/style.css,/assets,/admin,/oidc/login,/admin/organisations,/organisations,/organisations/switch/{organisation_id},/"
SERVICE_PREFIX=""
SERVICE_NAME="CAC"
Expand All @@ -28,4 +28,5 @@ AUTH_PROVIDER=DISABLED
OIDC_CLIENT_ID=superposition
OIDC_CLIENT_SECRET=superposition_secret
OIDC_TOKEN_ENDPOINT_FORMAT="http://localhost:8081/realms/<organisation>/protocol/openid-connect/token"
OIDC_ISSUER_ENDPOINT_FORMAT="http://http://localhost:8081/realms/<organisation>"
OIDC_ISSUER_ENDPOINT_FORMAT="http://http://localhost:8081/realms/<organisation>"
WORKER_ID=1
16 changes: 16 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions crates/service_utils/src/service/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ impl FromStr for AppEnv {
pub enum AppScope {
CAC,
EXPERIMENTATION,
SUPERPOSITION,
}
impl FromRequest for AppScope {
type Error = Error;
Expand Down
4 changes: 4 additions & 0 deletions crates/superposition/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,16 +8,20 @@ edition = "2021"
[dependencies]
actix-files = { version = "0.6" }
actix-web = { workspace = true }
anyhow = { workspace = true }
aws-sdk-kms = { workspace = true }
cac_toml = { path = "../cac_toml" }
cfg-if = { workspace = true }
chrono = { workspace = true }
context_aware_config = { path = "../context_aware_config" }
diesel = { workspace = true }
dotenv = "0.15.0"
env_logger = "0.8"
experimentation_platform = { path = "../experimentation_platform" }
fred = { workspace = true, optional = true }
frontend = { path = "../frontend" }
futures-util = { workspace = true }
idgenerator = "2.0.0"
leptos = { workspace = true }
leptos_actix = { version = "0.6.11" }
log = { workspace = true }
Expand Down
16 changes: 16 additions & 0 deletions crates/superposition/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#![deny(unused_crate_dependencies)]
mod app_state;
mod auth;
mod organisation;

use idgenerator::{IdGeneratorOptions, IdInstance};
use std::{collections::HashSet, io::Result, time::Duration};

use actix_files::Files;
Expand Down Expand Up @@ -44,6 +46,15 @@ async fn main() -> Result<()> {
let service_prefix: String =
get_from_env_unsafe("SERVICE_PREFIX").expect("SERVICE_PREFIX is not set");

let worker_id: u32 = get_from_env_unsafe("WORKER_ID").expect("WORKER_ID is not set");

let options = IdGeneratorOptions::new()
.worker_id(worker_id)
.worker_id_bit_len(8)
.seq_bit_len(12);

IdInstance::init(options).expect("Failed to initialize ID generator");

/*
Reading from a env returns a String at best we cannot obtain a &'static str from it,
which seems logical as it not known at compiletime, and there is no straightforward way to do this.
Expand Down Expand Up @@ -181,6 +192,11 @@ async fn main() -> Result<()> {
AppExecutionScopeMiddlewareFactory::new(AppScope::EXPERIMENTATION),
),
)
.service(
scope("/organisation")
.wrap(AppExecutionScopeMiddlewareFactory::new(AppScope::SUPERPOSITION))
.service(organisation::endpoints()),
)
/***************************** UI Routes ******************************/
.route("/fxn/{tail:.*}", leptos_actix::handle_server_fns())
// serve JS/WASM/CSS from `pkg`
Expand Down
3 changes: 3 additions & 0 deletions crates/superposition/src/organisation.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
mod handlers;
pub mod types;
pub use handlers::endpoints;
146 changes: 146 additions & 0 deletions crates/superposition/src/organisation/handlers.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,146 @@
use actix_web::{
get, post,
web::{self, Json, Query},
HttpResponse, Scope,
};
use chrono::Utc;
use diesel::prelude::*;
use idgenerator::IdInstance;
use service_utils::service::types::DbConnection;
use superposition_types::database::{
models::organisation::{OrgStatus, Organisation},
schema::organisations::dsl::organisations,
};
use superposition_types::{
custom_query::PaginationParams, result as superposition, PaginatedResponse, User,
};

use super::types::{CreateOrganisationRequest, CreateOrganisationResponse};

pub fn endpoints() -> Scope {
Scope::new("")
.service(create_organisation)
.service(list_organisations)
.service(get_organisation)
}

#[post("")]
pub async fn create_organisation(
req: web::Json<CreateOrganisationRequest>,
db_conn: DbConnection,
user: User,
) -> superposition::Result<HttpResponse> {
let DbConnection(mut conn) = db_conn;

// Generating a numeric ID from IdInstance and prefixing it with `orgid`
let numeric_id = IdInstance::next_id();
let org_id = format!("orgid{}", numeric_id);
let now = Utc::now().naive_utc();

let new_org = Organisation {
id: org_id.clone(),
name: req.name.clone(),
country_code: req.country_code.clone(),
contact_email: req.contact_email.clone(),
contact_phone: req.contact_phone.clone(),
created_by: user.get_username(),
admin_email: req.admin_email.clone(),
status: OrgStatus::PendingKyb,
sector: req.sector.clone(),
created_at: now,
updated_at: now,
updated_by: user.get_username(),
};

diesel::insert_into(organisations)
.values(&new_org)
.execute(&mut conn)
.map_err(|e| {
log::error!("Failed to insert new organisation: {:?}", e);
superposition::AppError::UnexpectedError(anyhow::anyhow!(
"Failed to create organisation"
))
})?;

let mut http_resp = HttpResponse::Created();
Ok(http_resp.json(CreateOrganisationResponse { org_id }))
}

#[get("/{org_id}")]
pub async fn get_organisation(
org_id: web::Path<String>,
db_conn: DbConnection,
) -> superposition::Result<HttpResponse> {
let DbConnection(mut conn) = db_conn;

let org = organisations
.find(org_id.as_str())
.first::<Organisation>(&mut conn)
.map_err(|e| {
log::error!("Failed to fetch organisation {}: {:?}", org_id, e);
match e {
diesel::result::Error::NotFound => superposition::AppError::NotFound(
format!("Organisation {} not found", org_id),
),
_ => superposition::AppError::UnexpectedError(anyhow::anyhow!(
"Failed to fetch organisation"
)),
}
})?;

Ok(HttpResponse::Ok().json(org))
}

#[get("/list")]
pub async fn list_organisations(
db_conn: DbConnection,
filters: Query<PaginationParams>,
) -> superposition::Result<Json<PaginatedResponse<Organisation>>> {
use superposition_types::database::schema::organisations::dsl::*;
let DbConnection(mut conn) = db_conn;
log::info!("list_organisations");
let result =
conn.transaction::<_, superposition::AppError, _>(|transaction_conn| {
// If all parameter is true, return all organisations
if let Some(true) = filters.all {
let result: Vec<Organisation> = organisations
.order(created_at.desc())
.get_results(transaction_conn)?;
log::info!("organisations: {organisations:?}");
return Ok(PaginatedResponse {
total_pages: 1,
total_items: result.len() as i64,
data: result,
});
}

// Get total count of organisations
let total_items: i64 = organisations.count().get_result(transaction_conn)?;

// Set up pagination
let limit = filters.count.unwrap_or(10);
let mut builder = organisations
.into_boxed()
.order(created_at.desc())
.limit(limit);

// Apply offset if page is specified
if let Some(page) = filters.page {
let offset = (page - 1) * limit;
builder = builder.offset(offset);
}

// Get paginated results
let data: Vec<Organisation> = builder.load(transaction_conn)?;

let total_pages = (total_items as f64 / limit as f64).ceil() as i64;

Ok(PaginatedResponse {
total_pages,
total_items,
data: data,
})
})?;

Ok(Json(result))
}
18 changes: 18 additions & 0 deletions crates/superposition/src/organisation/types.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
use serde::{Deserialize, Serialize};

// Request payload for creating an organisation
#[derive(Deserialize)]
pub struct CreateOrganisationRequest {
pub country_code: Option<String>,
pub contact_email: Option<String>,
pub contact_phone: Option<String>,
pub admin_email: String,
pub name: String,
pub sector: Option<String>,
}

// Response type to include `org_id`
#[derive(Serialize)]
pub struct CreateOrganisationResponse {
pub org_id: String,
}
4 changes: 2 additions & 2 deletions crates/superposition_types/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ log = { workspace = true }
regex = { workspace = true }
serde = { workspace = true }
serde_json = { workspace = true }
strum_macros = { workspace = true, optional = true }
strum_macros = { workspace = true }
superposition_derives = { path = "../superposition_derives", optional = true }
thiserror = { version = "1.0.57", optional = true }
uuid = { workspace = true }
Expand All @@ -36,7 +36,7 @@ diesel_derives = [
disable_db_data_validation = []
result = ["dep:diesel", "dep:anyhow", "dep:thiserror", "dep:actix-web"]
server = ["dep:actix-web"]
experimentation = ["dep:strum_macros"]
experimentation = []

[lints]
workspace = true
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
-- down.sql
DROP INDEX IF EXISTS superposition.idx_organisation_admin_email;
DROP INDEX IF EXISTS superposition.idx_organisation_created_at;
DROP INDEX IF EXISTS superposition.idx_organisation_status;
DROP INDEX IF EXISTS superposition.idx_organisation_contact_email;
DROP TABLE IF EXISTS superposition.organisation;
DROP TYPE IF EXISTS superposition.org_status;
DROP SCHEMA IF EXISTS superposition;
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
-- up.sql
CREATE SCHEMA IF NOT EXISTS superposition;

CREATE TYPE superposition.org_status AS ENUM ('ACTIVE', 'INACTIVE', 'PENDING_KYB');

CREATE TABLE IF NOT EXISTS superposition.organisations (
id VARCHAR(30) PRIMARY KEY NOT NULL,
name TEXT NOT NULL,
country_code VARCHAR(10),
contact_email VARCHAR(255),
contact_phone VARCHAR(15),
created_by VARCHAR(255) NOT NULL,
admin_email VARCHAR(255) NOT NULL,
status superposition.org_status NOT NULL DEFAULT 'ACTIVE',
sector VARCHAR(100),
created_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL,
updated_at TIMESTAMP WITHOUT TIME ZONE DEFAULT NOW() NOT NULL,
updated_by VARCHAR(255) NOT NULL
);

-- Indexes for optimizing queries
CREATE INDEX IF NOT EXISTS idx_organisation_contact_email ON superposition.organisations (contact_email);
CREATE INDEX IF NOT EXISTS idx_organisation_status ON superposition.organisations (status);
CREATE INDEX IF NOT EXISTS idx_organisation_created_at ON superposition.organisations (created_at);
CREATE INDEX IF NOT EXISTS idx_organisation_admin_email ON superposition.organisations (admin_email);

1 change: 1 addition & 0 deletions crates/superposition_types/src/database/models.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
pub mod cac;
#[cfg(feature = "experimentation")]
pub mod experimentation;
pub mod organisation;
Loading

0 comments on commit 9270000

Please sign in to comment.