Skip to content

Commit

Permalink
feat: customer server (#1333)
Browse files Browse the repository at this point in the history
* chore: initial customer-server boilerplate

* chore: wire customer-server via cli

* chore: render Subject in customer-server schema

* refactor: move Customer to own file

* fix: context wording
  • Loading branch information
bodymindarts authored Feb 4, 2025
1 parent 7f21975 commit b7e8bcd
Show file tree
Hide file tree
Showing 23 changed files with 444 additions and 2 deletions.
24 changes: 24 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 Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ resolver = "2"
members = [
"lana/cli",
"lana/admin-server",
"lana/customer-server",
"lana/app",
"lana/rbac-types",
"lana/events",
Expand Down
3 changes: 3 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,9 @@ sdl:
SQLX_OFFLINE=true cargo run --bin write_sdl > lana/admin-server/src/graphql/schema.graphql
cd apps/admin-panel && pnpm install && pnpm codegen

customer-sdl:
SQLX_OFFLINE=true cargo run --bin write_customer_sdl > lana/customer-server/src/graphql/schema.graphql

test-in-ci: start-deps setup-db
cargo nextest run --verbose --locked

Expand Down
2 changes: 2 additions & 0 deletions core/customer/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,8 @@ pub enum CustomerError {
AuditError(#[from] audit::error::AuditError),
#[error("CustomerError - DepositError: {0}")]
DepositError(#[from] deposit::error::CoreDepositError),
#[error("CustomerError - SubjectIsNotCustomer")]
SubjectIsNotCustomer,
}

es_entity::from_es_entity_error!(CustomerError);
12 changes: 12 additions & 0 deletions core/customer/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,18 @@ where
Ok(customer)
}

#[instrument(name = "core_custorem.find_for_subject", skip(self))]
pub async fn find_for_subject(
&self,
sub: &<<Perms as PermissionCheck>::Audit as AuditSvc>::Subject,
) -> Result<Customer, CustomerError>
where
CustomerId: for<'a> TryFrom<&'a <<Perms as PermissionCheck>::Audit as AuditSvc>::Subject>,
{
let id = CustomerId::try_from(sub).map_err(|_| CustomerError::SubjectIsNotCustomer)?;
self.repo.find_by_id(id).await
}

#[instrument(name = "customer.create_customer", skip(self), err)]
pub async fn find_by_id(
&self,
Expand Down
2 changes: 1 addition & 1 deletion lana/admin-server/src/graphql/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
macro_rules! app_and_sub_from_ctx {
($ctx:expr) => {{
let app = $ctx.data_unchecked::<lana_app::app::LanaApp>();
let AdminAuthContext { sub } = $ctx.data()?;
let $crate::primitives::AdminAuthContext { sub } = $ctx.data()?;
(app, sub)
}};
}
Expand Down
2 changes: 1 addition & 1 deletion lana/admin-server/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ pub struct AdminJwtClaims {
pub sub: String,
}

#[instrument(name = "server_admin.graphql", skip_all, fields(error, error.level, error.message))]
#[instrument(name = "admin_server.graphql", skip_all, fields(error, error.level, error.message))]
pub async fn graphql_handler(
headers: HeaderMap,
schema: Extension<Schema<graphql::Query, graphql::Mutation, EmptySubscription>>,
Expand Down
1 change: 1 addition & 0 deletions lana/cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ fail-on-warnings = []
[dependencies]
lana-app = { path = "../app" }
admin-server = { path = "../admin-server" }
customer-server = { path = "../customer-server" }

sim-bootstrap = { path = "../sim-bootstrap", optional = true }

Expand Down
3 changes: 3 additions & 0 deletions lana/cli/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ use std::path::Path;

use super::db::*;
use admin_server::AdminServerConfig;
use customer_server::CustomerServerConfig;
use lana_app::{app::AppConfig, report::ReportConfig, storage::config::StorageConfig};

#[derive(Clone, Default, Serialize, Deserialize)]
Expand All @@ -22,6 +23,8 @@ pub struct Config {
#[serde(default)]
pub admin_server: AdminServerConfig,
#[serde(default)]
pub customer_server: CustomerServerConfig,
#[serde(default)]
pub app: AppConfig,
#[serde(default)]
pub tracing: TracingConfig,
Expand Down
9 changes: 9 additions & 0 deletions lana/cli/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,7 @@ async fn run_cmd(lana_home: &str, config: Config) -> anyhow::Result<()> {
let superuser_email = config.app.user.superuser_email.clone().expect("super user");

let admin_app = lana_app::app::LanaApp::run(pool.clone(), config.app).await?;
let customer_app = admin_app.clone();

#[cfg(feature = "sim-bootstrap")]
{
Expand All @@ -91,6 +92,14 @@ async fn run_cmd(lana_home: &str, config: Config) -> anyhow::Result<()> {
.context("Admin server error"),
);
}));
let customer_send = send.clone();
handles.push(tokio::spawn(async move {
let _ = customer_send.try_send(
customer_server::run(config.customer_server, customer_app)
.await
.context("Customer server error"),
);
}));

let reason = receive.recv().await.expect("Didn't receive msg");
for handle in handles {
Expand Down
31 changes: 31 additions & 0 deletions lana/customer-server/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
[package]
name = "customer-server"
version = "0.3.32-dev"
edition = "2021"

[features]

fail-on-warnings = []

[dependencies]
lana-app = { path = "../app" }

core-customer = { path = "../../core/customer", features = ["graphql"] }

tracing-utils = { path = "../../lib/tracing-utils" }
jwks-utils = { path = "../../lib/jwks-utils" }

es-entity = { workspace = true, features = ["graphql"] }

serde = { workspace = true }
serde_json = { workspace = true }
async-graphql = { workspace = true }
async-graphql-axum = { workspace = true }
tracing = { workspace = true }
axum = { workspace = true }
axum-extra = { workspace = true }
tower-http = { workspace = true }
tokio = { workspace = true }
anyhow = { workspace = true }
uuid = { workspace = true }
chrono = { workspace = true }
3 changes: 3 additions & 0 deletions lana/customer-server/src/bin/write_customer_sdl.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fn main() {
println!("{}", customer_server::graphql::schema(None).sdl().trim());
}
33 changes: 33 additions & 0 deletions lana/customer-server/src/config.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
use serde::{Deserialize, Serialize};

#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct CustomerServerConfig {
#[serde(default = "default_port")]
pub port: u16,
#[serde(default = "default_jwks_url")]
pub jwks_url: String,
#[serde(default = "aud")]
pub aud: String,
}

impl Default for CustomerServerConfig {
fn default() -> Self {
Self {
port: default_port(),
jwks_url: default_jwks_url(),
aud: "https://admin-api/graphql".to_string(),
}
}
}

fn default_port() -> u16 {
5254
}

fn default_jwks_url() -> String {
"http://localhost:4456/.well-known/jwks.json".to_string()
}

fn aud() -> String {
"https://admin-api/graphql".to_string()
}
19 changes: 19 additions & 0 deletions lana/customer-server/src/graphql/authenticated_subject.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
use async_graphql::*;

use core_customer::Customer as DomainCustomer;

use super::customer::*;

#[derive(SimpleObject)]
#[graphql(name = "Subject")]
pub struct AuthenticatedSubject {
customer: Customer,
}

impl From<DomainCustomer> for AuthenticatedSubject {
fn from(entity: DomainCustomer) -> Self {
Self {
customer: Customer::from(entity),
}
}
}
44 changes: 44 additions & 0 deletions lana/customer-server/src/graphql/customer.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
use async_graphql::*;

use std::sync::Arc;

use core_customer::{AccountStatus, Customer as DomainCustomer, KycLevel};

use crate::primitives::*;

#[derive(SimpleObject, Clone)]
#[graphql(complex)]
pub struct Customer {
id: ID,
customer_id: UUID,
status: AccountStatus,
level: KycLevel,
created_at: Timestamp,

#[graphql(skip)]
pub(super) entity: Arc<DomainCustomer>,
}

impl From<DomainCustomer> for Customer {
fn from(customer: DomainCustomer) -> Self {
Customer {
id: customer.id.to_global_id(),
customer_id: UUID::from(customer.id),
status: customer.status,
level: customer.level,
created_at: customer.created_at().into(),
entity: Arc::new(customer),
}
}
}

#[ComplexObject]
impl Customer {
async fn email(&self) -> &str {
&self.entity.email
}

async fn telegram_id(&self) -> &str {
&self.entity.telegram_id
}
}
20 changes: 20 additions & 0 deletions lana/customer-server/src/graphql/macros.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
// Helper to extract the 'app' and 'sub' args
// instead of:
//
// async fn users(&self, ctx: &Context<'_>) -> async_graphql::Result<Vec<User>> {
// let app = ctx.data_unchecked::<LanaApp>();
// let AdminAuthContext { sub } = ctx.data()?;
//
// use
//
// async fn users(&self, ctx: &Context<'_>) -> async_graphql::Result<Vec<User>> {
// let (app, sub) = app_and_sub_from_ctx!(ctx);
//
#[macro_export]
macro_rules! app_and_sub_from_ctx {
($ctx:expr) => {{
let app = $ctx.data_unchecked::<lana_app::app::LanaApp>();
let $crate::primitives::CustomerAuthContext { sub } = $ctx.data()?;
(app, sub)
}};
}
21 changes: 21 additions & 0 deletions lana/customer-server/src/graphql/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#[macro_use]
pub mod macros;
mod authenticated_subject;
mod customer;
mod schema;

use async_graphql::*;

pub use schema::*;

use lana_app::app::LanaApp;

pub fn schema(app: Option<LanaApp>) -> Schema<Query, EmptyMutation, EmptySubscription> {
let mut schema_builder = Schema::build(Query, EmptyMutation, EmptySubscription);

if let Some(app) = app {
schema_builder = schema_builder.data(app);
}

schema_builder.finish()
}
43 changes: 43 additions & 0 deletions lana/customer-server/src/graphql/schema.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
enum AccountStatus {
INACTIVE
ACTIVE
}


type Customer {
id: ID!
customerId: UUID!
status: AccountStatus!
level: KycLevel!
createdAt: Timestamp!
email: String!
telegramId: String!
}




enum KycLevel {
NOT_KYCED
BASIC
ADVANCED
}

type Query {
me: Subject!
}


type Subject {
customer: Customer!
}

scalar Timestamp

scalar UUID

directive @include(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
directive @skip(if: Boolean!) on FIELD | FRAGMENT_SPREAD | INLINE_FRAGMENT
schema {
query: Query
}
14 changes: 14 additions & 0 deletions lana/customer-server/src/graphql/schema.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
use async_graphql::{Context, Object};

use super::authenticated_subject::*;

pub struct Query;

#[Object]
impl Query {
async fn me(&self, ctx: &Context<'_>) -> async_graphql::Result<AuthenticatedSubject> {
let (app, sub) = app_and_sub_from_ctx!(ctx);
let customer = app.customers().find_for_subject(sub).await?;
Ok(AuthenticatedSubject::from(customer))
}
}
Loading

0 comments on commit b7e8bcd

Please sign in to comment.