Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

added schema and crl apis for organisation #322

Merged
merged 1 commit into from
Dec 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

bit confused, should superposition be a part of this env ?

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))
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this has to be cleaned up later

.service(organisation::endpoints()),
)
Comment on lines +195 to +199
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

by the way, this url would fail right ?
if we dont send in tenant ?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we need to add this in the tenant exclusion list as well

/***************************** 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();
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we planned to keep the org id of fixed length 25 right ?
to allow for some buffer in future?

orgid is 5 characters, wanted to just know that how is IdInstance::next_id() making sure that the length of the string given is of 20 characters exact ?

or are we keeping the id value dynamic and not fixed length, because I was of the impression that we planned to keep the id fixed length

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),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Isn't country code supposed to be 2/3 characters, depending on the format?
Source: https://en.wikipedia.org/wiki/Country_code

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have created a ticket for country code validation to be picked up later.

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
Loading