From 3c4d17cf2840c643b8cd111ef775750cc5ae83b3 Mon Sep 17 00:00:00 2001 From: rtkay123 Date: Sun, 27 Jul 2025 18:16:41 +0200 Subject: refactor: profile -> users --- Cargo.lock | 2 +- crates/auth-service/Cargo.toml | 2 +- .../auth-service/src/server/routes/authorised.rs | 4 +- crates/auth-service/src/state.rs | 8 +- crates/profile-service/Cargo.toml | 32 ------ .../migrations/20250726161947_profile.sql | 32 ------ crates/profile-service/profile.toml | 37 ------- crates/profile-service/src/cnfg.rs | 8 -- crates/profile-service/src/main.rs | 110 --------------------- crates/profile-service/src/server.rs | 2 - crates/profile-service/src/server/interceptor.rs | 11 --- crates/profile-service/src/server/manager.rs | 45 --------- crates/profile-service/src/state.rs | 42 -------- crates/users-service/Cargo.toml | 32 ++++++ .../migrations/20250726161947_profile.sql | 32 ++++++ crates/users-service/src/cnfg.rs | 8 ++ crates/users-service/src/main.rs | 109 ++++++++++++++++++++ crates/users-service/src/server.rs | 2 + crates/users-service/src/server/interceptor.rs | 11 +++ crates/users-service/src/server/manager.rs | 85 ++++++++++++++++ crates/users-service/src/state.rs | 65 ++++++++++++ crates/users-service/users.toml | 37 +++++++ lib/sellershut-core/Cargo.toml | 2 +- lib/sellershut-core/build.rs | 22 ++--- lib/sellershut-core/proto/profile/profile.proto | 57 ----------- lib/sellershut-core/proto/users/users.proto | 79 +++++++++++++++ lib/sellershut-core/src/lib.rs | 8 +- lib/sellershut-core/src/profile.rs | 4 - lib/sellershut-core/src/users.rs | 36 +++++++ 29 files changed, 520 insertions(+), 404 deletions(-) delete mode 100644 crates/profile-service/Cargo.toml delete mode 100644 crates/profile-service/migrations/20250726161947_profile.sql delete mode 100644 crates/profile-service/profile.toml delete mode 100644 crates/profile-service/src/cnfg.rs delete mode 100644 crates/profile-service/src/main.rs delete mode 100644 crates/profile-service/src/server.rs delete mode 100644 crates/profile-service/src/server/interceptor.rs delete mode 100644 crates/profile-service/src/server/manager.rs delete mode 100644 crates/profile-service/src/state.rs create mode 100644 crates/users-service/Cargo.toml create mode 100644 crates/users-service/migrations/20250726161947_profile.sql create mode 100644 crates/users-service/src/cnfg.rs create mode 100644 crates/users-service/src/main.rs create mode 100644 crates/users-service/src/server.rs create mode 100644 crates/users-service/src/server/interceptor.rs create mode 100644 crates/users-service/src/server/manager.rs create mode 100644 crates/users-service/src/state.rs create mode 100644 crates/users-service/users.toml delete mode 100644 lib/sellershut-core/proto/profile/profile.proto create mode 100644 lib/sellershut-core/proto/users/users.proto delete mode 100644 lib/sellershut-core/src/profile.rs create mode 100644 lib/sellershut-core/src/users.rs diff --git a/Cargo.lock b/Cargo.lock index 23a8702..ec78576 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2649,7 +2649,7 @@ dependencies = [ ] [[package]] -name = "sellershut-profiles" +name = "sellershut-users" version = "0.1.0" dependencies = [ "anyhow", diff --git a/crates/auth-service/Cargo.toml b/crates/auth-service/Cargo.toml index bbbb10d..837fc8b 100644 --- a/crates/auth-service/Cargo.toml +++ b/crates/auth-service/Cargo.toml @@ -19,7 +19,7 @@ jsonwebtoken = "9.3.1" nanoid.workspace = true oauth2 = "5.0.0" reqwest = { workspace = true, features = ["json", "rustls-tls"] } -sellershut-core = { workspace = true, features = ["auth", "serde"] } +sellershut-core = { workspace = true, features = ["auth", "serde", "users"] } serde = { workspace = true, features = ["derive"] } serde_json.workspace = true sqlx = { workspace = true, features = ["macros", "migrate", "runtime-tokio", "time", "tls-rustls", "uuid"] } diff --git a/crates/auth-service/src/server/routes/authorised.rs b/crates/auth-service/src/server/routes/authorised.rs index 4d48299..2538cdc 100644 --- a/crates/auth-service/src/server/routes/authorised.rs +++ b/crates/auth-service/src/server/routes/authorised.rs @@ -9,7 +9,7 @@ use axum::{ use axum_extra::{TypedHeader, headers}; use oauth2::{AuthorizationCode, TokenResponse}; use reqwest::{StatusCode, header::SET_COOKIE}; -use sellershut_core::profile::CreateUserRequest; +use sellershut_core::users::CreateUserRequest; use serde::{Deserialize, Serialize}; use sqlx::types::uuid; use time::OffsetDateTime; @@ -215,7 +215,7 @@ pub async fn login_authorised( let cookie = format!("{SESSION_COOKIE}={session_id}; SameSite=Lax; HttpOnly; Secure; Path=/"); - let mut profile_client = state.profile_client.clone(); + let mut profile_client = state.users_client.clone(); let resp = profile_client.create_user(user_request).await?.into_inner(); let user_id = resp.temp_id; diff --git a/crates/auth-service/src/state.rs b/crates/auth-service/src/state.rs index 5905948..07bfda9 100644 --- a/crates/auth-service/src/state.rs +++ b/crates/auth-service/src/state.rs @@ -1,6 +1,6 @@ use std::{ops::Deref, sync::Arc}; -use sellershut_core::profile::profile_client::ProfileClient; +use sellershut_core::users::users_service_client::UsersServiceClient; use sqlx::PgPool; use stack_up::Configuration; use tokio::task::JoinHandle; @@ -39,7 +39,7 @@ pub struct AppState { pub discord_client: OauthClient, pub http_client: reqwest::Client, pub session_store: CachingSessionStore, - pub profile_client: ProfileClient, + pub users_client: UsersServiceClient, } impl AppState { @@ -68,7 +68,7 @@ impl AppState { .await .inspect_err(|e| error!("could not connect to profile service: {e}"))?; - let profile_client = ProfileClient::with_interceptor(channel, MyInterceptor); + let users_client = UsersServiceClient::with_interceptor(channel, MyInterceptor); Ok(( AppHandle(Arc::new(Self { @@ -77,7 +77,7 @@ impl AppState { discord_client, http_client: reqwest::Client::new(), session_store: store, - profile_client, + users_client, })), deletion_task, )) diff --git a/crates/profile-service/Cargo.toml b/crates/profile-service/Cargo.toml deleted file mode 100644 index e56db3a..0000000 --- a/crates/profile-service/Cargo.toml +++ /dev/null @@ -1,32 +0,0 @@ -[package] -name = "sellershut-profiles" -version = "0.1.0" -edition = "2024" -license.workspace = true -homepage.workspace = true -documentation.workspace = true -description.workspace = true - -[dependencies] -anyhow.workspace = true -base64.workspace = true -clap = { workspace = true, features = ["derive"] } -config = { workspace = true, features = ["convert-case", "toml"] } -futures-util.workspace = true -nanoid.workspace = true -prost.workspace = true -sellershut-core = { workspace = true, features = ["profile", "serde"] } -serde = { workspace = true, features = ["derive"] } -serde_json.workspace = true -sqlx = { workspace = true, features = ["macros", "migrate", "runtime-tokio", "time", "tls-rustls", "uuid"] } -time = { workspace = true, features = ["parsing", "serde"] } -tokio = { workspace = true, features = ["macros", "rt-multi-thread", "signal"] } -tonic.workspace = true -tonic-reflection = "0.13.0" -tracing.workspace = true -url.workspace = true -uuid = { workspace = true, features = ["serde", "v7"] } - -[dependencies.stack-up] -workspace = true -features = ["api", "cache", "postgres", "tracing"] diff --git a/crates/profile-service/migrations/20250726161947_profile.sql b/crates/profile-service/migrations/20250726161947_profile.sql deleted file mode 100644 index 15822c8..0000000 --- a/crates/profile-service/migrations/20250726161947_profile.sql +++ /dev/null @@ -1,32 +0,0 @@ -create table profile ( - id text primary key, - username varchar(30) not null, - inbox text not null, - outbox text, - local boolean not null, - avatar_url text, - description text, - user_type text not null check ( - user_type IN ('PERSON', 'APPLICATION', 'GROUP', 'ORGANIZATION', 'SERVICE') - ), - created_at timestamptz not null default now(), - updated_at timestamptz not null default now(), - public_key text not null -); - -create unique index unique_username_local - on profile (username) - where local = true; - -create or replace function set_updated_at() -returns trigger as $$ -begin - new.updated_at := now(); - return new; -end; -$$ language plpgsql; - -create trigger trigger_set_updated_at -before update on profile -for each row -execute function set_updated_at(); diff --git a/crates/profile-service/profile.toml b/crates/profile-service/profile.toml deleted file mode 100644 index 13a5f0a..0000000 --- a/crates/profile-service/profile.toml +++ /dev/null @@ -1,37 +0,0 @@ -[application] -env = "development" -port = 1610 - -[monitoring] -log-level = "sellershut_profiles=trace,info" - -[misc] -temp-ttl = 1000 -cache-ttl = 300 - -[database] -pool_size = 100 -port = 5432 -name = "profiles" -host = "localhost" -password = "password" -user = "postgres" - -[nats] -hosts = ["nats://localhost:4222"] - -[cache] -dsn = "redis://localhost:6379" -pooled = true -type = "non-clustered" # clustered, non-clustered or sentinel -max-connections = 100 - -[cache.sentinel] -master-name = "mymaster" -nodes = [ - { host = "127.0.0.1", port = 26379 }, - { host = "127.0.0.2", port = 26379 }, - { host = "127.0.0.3", port = 26379 }, -] - -# vim:ft=toml diff --git a/crates/profile-service/src/cnfg.rs b/crates/profile-service/src/cnfg.rs deleted file mode 100644 index fec4cf7..0000000 --- a/crates/profile-service/src/cnfg.rs +++ /dev/null @@ -1,8 +0,0 @@ -use serde::Deserialize; - -#[derive(Deserialize, Clone)] -#[serde(rename_all = "kebab-case")] -pub struct LocalConfig { - pub temp_ttl: u64, - pub cache_ttl: u64, -} diff --git a/crates/profile-service/src/main.rs b/crates/profile-service/src/main.rs deleted file mode 100644 index 5fe1331..0000000 --- a/crates/profile-service/src/main.rs +++ /dev/null @@ -1,110 +0,0 @@ -mod cnfg; -mod server; -mod state; -use std::net::{Ipv6Addr, SocketAddr}; - -use clap::Parser; -use sellershut_core::profile::profile_server::ProfileServer; -use stack_up::{Configuration, Services, tracing::Tracing}; -use tokio::signal; -use tonic::transport::{Server, server::TcpIncoming}; -use tracing::{error, info}; - -use crate::{ - server::interceptor::MyInterceptor, - state::{AppHandle, AppState}, -}; - -/// sellershut-profiles -#[derive(Parser, Debug)] -#[command(version, about, long_about = None)] -struct Args { - /// Path to config file - #[arg(short, long)] - config_file: Option, -} - -#[tokio::main] -async fn main() -> anyhow::Result<()> { - let args = Args::parse(); - let config = include_str!("../profile.toml"); - - let mut config = config::Config::builder() - .add_source(config::File::from_str(config, config::FileFormat::Toml)); - - if let Some(cf) = args.config_file.as_ref().and_then(|v| v.to_str()) { - config = config.add_source(config::File::new(cf, config::FileFormat::Toml)); - }; - - let mut config: Configuration = config.build()?.try_deserialize()?; - config.application.name = env!("CARGO_CRATE_NAME").into(); - config.application.version = env!("CARGO_PKG_VERSION").into(); - - let tracing = Tracing::builder().build(&config.monitoring); - - let mut services = Services::builder() - .postgres(&config.database) - .await - .inspect_err(|e| error!("database: {e}"))? - .cache(&config.cache) - .await - .inspect_err(|e| error!("cache: {e}"))? - .build(); - - let postgres = services - .postgres - .take() - .ok_or_else(|| anyhow::anyhow!("database is not ready"))?; - - let cache = services - .cache - .take() - .ok_or_else(|| anyhow::anyhow!("cache is not ready"))?; - - let services = crate::state::Services { postgres, cache }; - - let state = AppState::create(services, &config).await?; - - let addr = SocketAddr::from((Ipv6Addr::UNSPECIFIED, config.application.port)); - - let listener = tokio::net::TcpListener::bind(addr).await?; - - info!(addr = ?addr, "starting server"); - - Server::builder() - .trace_fn(|_| tracing::info_span!(env!("CARGO_PKG_NAME"))) - // .add_service(QueryUsersServer::new(state.clone())) - .add_service(ProfileServer::with_interceptor( - state.clone(), - MyInterceptor, - )) - .serve_with_incoming_shutdown(TcpIncoming::from(listener), shutdown_signal(state)) - .await?; - - Ok(()) -} -async fn shutdown_signal(state: AppHandle) { - let ctrl_c = async { - signal::ctrl_c() - .await - .expect("failed to install Ctrl+C handler"); - }; - - #[cfg(unix)] - let terminate = async { - signal::unix::signal(signal::unix::SignalKind::terminate()) - .expect("failed to install signal handler") - .recv() - .await; - }; - - #[cfg(not(unix))] - let terminate = std::future::pending::<()>(); - - tokio::select! { - _ = ctrl_c => { - }, - _ = terminate => { - }, - } -} diff --git a/crates/profile-service/src/server.rs b/crates/profile-service/src/server.rs deleted file mode 100644 index b2e04f9..0000000 --- a/crates/profile-service/src/server.rs +++ /dev/null @@ -1,2 +0,0 @@ -pub mod interceptor; -pub mod manager; diff --git a/crates/profile-service/src/server/interceptor.rs b/crates/profile-service/src/server/interceptor.rs deleted file mode 100644 index 6fbe7fa..0000000 --- a/crates/profile-service/src/server/interceptor.rs +++ /dev/null @@ -1,11 +0,0 @@ -use tonic::{Status, service::Interceptor}; -use tracing::Span; - -#[derive(Clone, Copy)] -pub struct MyInterceptor; - -impl Interceptor for MyInterceptor { - fn call(&mut self, request: tonic::Request<()>) -> Result, Status> { - Ok(request) - } -} diff --git a/crates/profile-service/src/server/manager.rs b/crates/profile-service/src/server/manager.rs deleted file mode 100644 index bd7e149..0000000 --- a/crates/profile-service/src/server/manager.rs +++ /dev/null @@ -1,45 +0,0 @@ -use prost::Message; -use sellershut_core::profile::{ - CompleteUserRequest, CreateUserRequest, CreateUserResponse, User, profile_server::Profile, -}; -use stack_up::redis::AsyncCommands; -use tonic::{Request, Response, Status, async_trait}; -use tracing::trace; -use uuid::Uuid; - -use crate::state::AppHandle; - -#[async_trait] -impl Profile for AppHandle { - #[doc = " Create a new user profile"] - async fn create_user( - &self, - request: Request, - ) -> Result, Status> { - trace!("creating user"); - let data = request.into_inner(); - let id = Uuid::now_v7().to_string(); - - let bytes = data.encode_to_vec(); - let mut cache = self - .services - .cache - .get() - .await - .map_err(|e| Status::internal("storage not ready"))?; - cache - .set_ex::<_, _, ()>(&id, &bytes, self.local_config.temp_ttl) - .await - .map_err(|e| Status::internal("storage not ready"))?; - - Ok(Response::new(CreateUserResponse { temp_id: id })) - } - - #[doc = " Complete Profile"] - async fn complete_profile( - &self, - request: Request, - ) -> Result, Status> { - todo!() - } -} diff --git a/crates/profile-service/src/state.rs b/crates/profile-service/src/state.rs deleted file mode 100644 index 1ccfbfd..0000000 --- a/crates/profile-service/src/state.rs +++ /dev/null @@ -1,42 +0,0 @@ -use std::sync::Arc; - -use sqlx::PgPool; -use stack_up::{Configuration, cache::RedisManager}; - -use crate::cnfg::LocalConfig; - -#[derive(Clone)] -pub struct AppHandle(Arc); - -impl std::ops::Deref for AppHandle { - type Target = Arc; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -#[derive(Clone)] -pub struct Services { - pub postgres: PgPool, - pub cache: RedisManager, -} - -pub struct AppState { - pub services: Services, - pub local_config: LocalConfig, -} - -impl AppState { - pub async fn create( - services: Services, - configuration: &Configuration, - ) -> Result { - let local_config: LocalConfig = serde_json::from_value(configuration.misc.clone())?; - - Ok(AppHandle(Arc::new(Self { - services, - local_config, - }))) - } -} diff --git a/crates/users-service/Cargo.toml b/crates/users-service/Cargo.toml new file mode 100644 index 0000000..2bbfe28 --- /dev/null +++ b/crates/users-service/Cargo.toml @@ -0,0 +1,32 @@ +[package] +name = "sellershut-users" +version = "0.1.0" +edition = "2024" +license.workspace = true +homepage.workspace = true +documentation.workspace = true +description.workspace = true + +[dependencies] +anyhow.workspace = true +base64.workspace = true +clap = { workspace = true, features = ["derive"] } +config = { workspace = true, features = ["convert-case", "toml"] } +futures-util.workspace = true +nanoid.workspace = true +prost.workspace = true +sellershut-core = { workspace = true, features = ["users", "serde"] } +serde = { workspace = true, features = ["derive"] } +serde_json.workspace = true +sqlx = { workspace = true, features = ["macros", "migrate", "runtime-tokio", "time", "tls-rustls", "uuid"] } +time = { workspace = true, features = ["parsing", "serde"] } +tokio = { workspace = true, features = ["macros", "rt-multi-thread", "signal"] } +tonic.workspace = true +tonic-reflection = "0.13.0" +tracing.workspace = true +url.workspace = true +uuid = { workspace = true, features = ["serde", "v7"] } + +[dependencies.stack-up] +workspace = true +features = ["api", "cache", "postgres", "tracing"] diff --git a/crates/users-service/migrations/20250726161947_profile.sql b/crates/users-service/migrations/20250726161947_profile.sql new file mode 100644 index 0000000..15822c8 --- /dev/null +++ b/crates/users-service/migrations/20250726161947_profile.sql @@ -0,0 +1,32 @@ +create table profile ( + id text primary key, + username varchar(30) not null, + inbox text not null, + outbox text, + local boolean not null, + avatar_url text, + description text, + user_type text not null check ( + user_type IN ('PERSON', 'APPLICATION', 'GROUP', 'ORGANIZATION', 'SERVICE') + ), + created_at timestamptz not null default now(), + updated_at timestamptz not null default now(), + public_key text not null +); + +create unique index unique_username_local + on profile (username) + where local = true; + +create or replace function set_updated_at() +returns trigger as $$ +begin + new.updated_at := now(); + return new; +end; +$$ language plpgsql; + +create trigger trigger_set_updated_at +before update on profile +for each row +execute function set_updated_at(); diff --git a/crates/users-service/src/cnfg.rs b/crates/users-service/src/cnfg.rs new file mode 100644 index 0000000..fec4cf7 --- /dev/null +++ b/crates/users-service/src/cnfg.rs @@ -0,0 +1,8 @@ +use serde::Deserialize; + +#[derive(Deserialize, Clone)] +#[serde(rename_all = "kebab-case")] +pub struct LocalConfig { + pub temp_ttl: u64, + pub cache_ttl: u64, +} diff --git a/crates/users-service/src/main.rs b/crates/users-service/src/main.rs new file mode 100644 index 0000000..218c74a --- /dev/null +++ b/crates/users-service/src/main.rs @@ -0,0 +1,109 @@ +mod cnfg; +mod server; +mod state; +use std::net::{Ipv6Addr, SocketAddr}; + +use clap::Parser; +use sellershut_core::users::users_service_server::UsersServiceServer; +use stack_up::{Configuration, Services, tracing::Tracing}; +use tokio::signal; +use tonic::transport::{Server, server::TcpIncoming}; +use tracing::{error, info}; + +use crate::{ + server::interceptor::MyInterceptor, + state::{AppHandle, AppState}, +}; + +/// sellershut-profiles +#[derive(Parser, Debug)] +#[command(version, about, long_about = None)] +struct Args { + /// Path to config file + #[arg(short, long)] + config_file: Option, +} + +#[tokio::main] +async fn main() -> anyhow::Result<()> { + let args = Args::parse(); + let config = include_str!("../users.toml"); + + let mut config = config::Config::builder() + .add_source(config::File::from_str(config, config::FileFormat::Toml)); + + if let Some(cf) = args.config_file.as_ref().and_then(|v| v.to_str()) { + config = config.add_source(config::File::new(cf, config::FileFormat::Toml)); + }; + + let mut config: Configuration = config.build()?.try_deserialize()?; + config.application.name = env!("CARGO_CRATE_NAME").into(); + config.application.version = env!("CARGO_PKG_VERSION").into(); + + let tracing = Tracing::builder().build(&config.monitoring); + + let mut services = Services::builder() + .postgres(&config.database) + .await + .inspect_err(|e| error!("database: {e}"))? + .cache(&config.cache) + .await + .inspect_err(|e| error!("cache: {e}"))? + .build(); + + let postgres = services + .postgres + .take() + .ok_or_else(|| anyhow::anyhow!("database is not ready"))?; + + let cache = services + .cache + .take() + .ok_or_else(|| anyhow::anyhow!("cache is not ready"))?; + + let services = crate::state::Services::new(postgres, cache); + + let state = AppState::create(services, &config).await?; + + let addr = SocketAddr::from((Ipv6Addr::UNSPECIFIED, config.application.port)); + + let listener = tokio::net::TcpListener::bind(addr).await?; + + info!(addr = ?addr, "starting server"); + + Server::builder() + .trace_fn(|_| tracing::info_span!(env!("CARGO_PKG_NAME"))) + .add_service(UsersServiceServer::with_interceptor( + state.clone(), + MyInterceptor, + )) + .serve_with_incoming_shutdown(TcpIncoming::from(listener), shutdown_signal(state)) + .await?; + + Ok(()) +} +async fn shutdown_signal(state: AppHandle) { + let ctrl_c = async { + signal::ctrl_c() + .await + .expect("failed to install Ctrl+C handler"); + }; + + #[cfg(unix)] + let terminate = async { + signal::unix::signal(signal::unix::SignalKind::terminate()) + .expect("failed to install signal handler") + .recv() + .await; + }; + + #[cfg(not(unix))] + let terminate = std::future::pending::<()>(); + + tokio::select! { + _ = ctrl_c => { + }, + _ = terminate => { + }, + } +} diff --git a/crates/users-service/src/server.rs b/crates/users-service/src/server.rs new file mode 100644 index 0000000..b2e04f9 --- /dev/null +++ b/crates/users-service/src/server.rs @@ -0,0 +1,2 @@ +pub mod interceptor; +pub mod manager; diff --git a/crates/users-service/src/server/interceptor.rs b/crates/users-service/src/server/interceptor.rs new file mode 100644 index 0000000..6fbe7fa --- /dev/null +++ b/crates/users-service/src/server/interceptor.rs @@ -0,0 +1,11 @@ +use tonic::{Status, service::Interceptor}; +use tracing::Span; + +#[derive(Clone, Copy)] +pub struct MyInterceptor; + +impl Interceptor for MyInterceptor { + fn call(&mut self, request: tonic::Request<()>) -> Result, Status> { + Ok(request) + } +} diff --git a/crates/users-service/src/server/manager.rs b/crates/users-service/src/server/manager.rs new file mode 100644 index 0000000..6affb4a --- /dev/null +++ b/crates/users-service/src/server/manager.rs @@ -0,0 +1,85 @@ +use prost::Message; +use sellershut_core::users::{ + CompleteUserRequest, CreateUserRequest, CreateUserResponse, User, + users_service_server::UsersService, +}; +use stack_up::redis::AsyncCommands; +use tonic::{Request, Response, Status, async_trait}; +use tracing::{error, trace}; +use uuid::Uuid; + +use crate::state::AppHandle; + +#[async_trait] +impl UsersService for AppHandle { + #[doc = " Create a new user profile"] + async fn create_user( + &self, + request: Request, + ) -> Result, Status> { + trace!("creating user"); + let data = request.into_inner(); + let id = Uuid::now_v7().to_string(); + + let bytes = data.encode_to_vec(); + let mut cache = self.cache().await?; + cache + .set_ex::<_, _, ()>(&id, &bytes, self.local_config.temp_ttl) + .await + .inspect_err(|e| error!("{e}")) + .map_err(|_e| Status::internal("storage not ready"))?; + + Ok(Response::new(CreateUserResponse { temp_id: id })) + } + + #[doc = " Complete Profile"] + async fn complete_user( + &self, + request: Request, + ) -> Result, Status> { + let request = request.into_inner(); + + let mut cache = self.cache().await?; + + let resp = cache + .get_del::<_, Vec>(&request.id) + .await + .inspect_err(|e| error!("{e}")) + .map_err(|_e| Status::internal("storage not ready"))?; + + if resp.is_empty() { + return Err(Status::data_loss("user unavailable")); + } + + let create_user = CreateUserRequest::decode(resp.as_ref()) + .map_err(|_e| Status::data_loss("internal data corrupted"))?; + + let user = sqlx::query!( + "insert + into + profile ( + id, + username, + inbox, + outbox, + local, + avatar_url, + description, + user_type, + public_key + ) + values ($1, $2, $3, $4, $5, $6, $7, $8, $9) + ", + request.id, + request.username, + request.inbox, + request.outbox, + request.local, + request.avatar.or(create_user.avatar), + request.description, + request.user_type.to_string(), + request.public_key, + ); + todo!() + } +} diff --git a/crates/users-service/src/state.rs b/crates/users-service/src/state.rs new file mode 100644 index 0000000..3f5ac7b --- /dev/null +++ b/crates/users-service/src/state.rs @@ -0,0 +1,65 @@ +use std::sync::Arc; + +use sqlx::PgPool; +use stack_up::{ + Configuration, + cache::{RedisConnection, RedisManager}, +}; +use tonic::Status; +use tracing::error; + +use crate::cnfg::LocalConfig; + +#[derive(Clone)] +pub struct AppHandle(Arc); + +impl std::ops::Deref for AppHandle { + type Target = Arc; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +#[derive(Clone)] +pub struct Services { + pub postgres: PgPool, + pub cache: RedisManager, +} + +impl Services { + pub fn new(postgres: PgPool, cache: RedisManager) -> Self { + Self { postgres, cache } + } +} + +pub struct AppState { + pub services: Services, + pub local_config: LocalConfig, +} + +impl AppState { + pub async fn create( + services: Services, + configuration: &Configuration, + ) -> Result { + let local_config: LocalConfig = serde_json::from_value(configuration.misc.clone())?; + + Ok(AppHandle(Arc::new(Self { + services, + local_config, + }))) + } + + pub async fn cache(&self) -> Result { + let cache = self + .services + .cache + .get() + .await + .inspect_err(|e| error!("{e}")) + .map_err(|_e| Status::internal("storage not ready"))?; + + Ok(cache) + } +} diff --git a/crates/users-service/users.toml b/crates/users-service/users.toml new file mode 100644 index 0000000..9706fcf --- /dev/null +++ b/crates/users-service/users.toml @@ -0,0 +1,37 @@ +[application] +env = "development" +port = 1610 + +[monitoring] +log-level = "sellershut_users=trace,info" + +[misc] +temp-ttl = 1000 +cache-ttl = 300 + +[database] +pool_size = 100 +port = 5432 +name = "profiles" +host = "localhost" +password = "password" +user = "postgres" + +[nats] +hosts = ["nats://localhost:4222"] + +[cache] +dsn = "redis://localhost:6379" +pooled = true +type = "non-clustered" # clustered, non-clustered or sentinel +max-connections = 100 + +[cache.sentinel] +master-name = "mymaster" +nodes = [ + { host = "127.0.0.1", port = 26379 }, + { host = "127.0.0.2", port = 26379 }, + { host = "127.0.0.3", port = 26379 }, +] + +# vim:ft=toml diff --git a/lib/sellershut-core/Cargo.toml b/lib/sellershut-core/Cargo.toml index 1b7d5f7..0be2ce7 100644 --- a/lib/sellershut-core/Cargo.toml +++ b/lib/sellershut-core/Cargo.toml @@ -18,7 +18,7 @@ tonic-types = "0.13.0" [features] default = [] auth = [] -profile = [] +users = [] serde = ["dep:serde", "serde/derive", "serde_json"] time = [ "dep:time", diff --git a/lib/sellershut-core/build.rs b/lib/sellershut-core/build.rs index 13e3d06..ff57fec 100644 --- a/lib/sellershut-core/build.rs +++ b/lib/sellershut-core/build.rs @@ -1,12 +1,12 @@ -#[cfg(any(feature = "auth", feature = "profile"))] +#[cfg(any(feature = "auth", feature = "users"))] enum Entity { #[cfg(feature = "auth")] Auth, - #[cfg(feature = "profile")] - Profile, + #[cfg(feature = "users")] + User, } -#[cfg(any(feature = "auth", feature = "profile"))] +#[cfg(any(feature = "auth", feature = "users"))] impl Entity { fn protos(&self) -> Vec<&'static str> { let mut res: Vec<&'static str> = vec![]; @@ -16,9 +16,9 @@ impl Entity { Entity::Auth => { res.extend(vec!["proto/auth/auth.proto"]); } - #[cfg(feature = "profile")] - Entity::Profile => { - res.extend(vec!["proto/profile/profile.proto"]); + #[cfg(feature = "users")] + Entity::User => { + res.extend(vec!["proto/users/users.proto"]); } } res @@ -31,13 +31,13 @@ fn main() -> Result<(), Box> { #[cfg(feature = "auth")] build_proto("auth", Entity::Auth); - #[cfg(feature = "profile")] - build_proto("profile", Entity::Profile); + #[cfg(feature = "users")] + build_proto("users", Entity::User); Ok(()) } -#[cfg(any(feature = "auth", feature = "profile"))] +#[cfg(any(feature = "auth", feature = "users"))] fn build_proto(package: &str, entity: Entity) { let out_dir = std::path::PathBuf::from(std::env::var("OUT_DIR").unwrap()); @@ -81,7 +81,7 @@ fn build_proto(package: &str, entity: Entity) { .compile_protos(&entity.protos(), include_paths).unwrap(); } -#[cfg(all(feature = "serde", any(feature = "auth", feature = "profile")))] +#[cfg(all(feature = "serde", any(feature = "auth", feature = "users")))] fn add_serde(config: tonic_build::Builder) -> tonic_build::Builder { config.type_attribute( ".", diff --git a/lib/sellershut-core/proto/profile/profile.proto b/lib/sellershut-core/proto/profile/profile.proto deleted file mode 100644 index 61181b3..0000000 --- a/lib/sellershut-core/proto/profile/profile.proto +++ /dev/null @@ -1,57 +0,0 @@ -syntax = "proto3"; - -package profile; - -import "google/protobuf/timestamp.proto"; - -// A message representing a user profile -message User { - // Unique identifier for the user - string id = 1; - // Email address of the user - string email = 2; - // Unique username chosen by the user - string username = 3; - // URL to the user's avatar image - optional string avatar = 4; - // Timestamp when the user was created - google.protobuf.Timestamp created_at = 5; - // Timestamp when the user was last updated - google.protobuf.Timestamp updated_at = 6; - // User-provided description or bio - optional string description = 7; -} - -// Request message for creating a new user profile -message CreateUserRequest { - // Email address of the new user - string email = 1; - // Avatar for the new user - optional string avatar = 2; -} - -// Response message for CreateUser RPC -message CreateUserResponse { - // Temporary assigned id - string temp_id = 1; -} - -// Message to finalise profile creation -message CompleteUserRequest { - // ID of the user to finalise - string id = 1; - // Required: username to finalise the profile - string username = 2; - // Optional: user-provided description - optional string description = 3; - // Optional: update avatar - optional string avatar = 4; -} - -// Profile gRPC service -service Profile { - // Create a new user profile - rpc CreateUser (CreateUserRequest) returns (CreateUserResponse); - // Complete Profile - rpc CompleteProfile (CompleteUserRequest) returns (User); -} diff --git a/lib/sellershut-core/proto/users/users.proto b/lib/sellershut-core/proto/users/users.proto new file mode 100644 index 0000000..d1cf692 --- /dev/null +++ b/lib/sellershut-core/proto/users/users.proto @@ -0,0 +1,79 @@ +syntax = "proto3"; + +package users; + +import "google/protobuf/timestamp.proto"; + +enum UserType { + PERSON = 0; + APPLICATION = 1; + GROUP = 2; + ORGANIZATION = 3; + SERVICE = 4; +} + +// A message representing a user user +message User { + // Unique identifier for the user + string id = 1; + // Email address of the user + string email = 2; + // Unique username chosen by the user + string username = 3; + // URL to the user's avatar image + optional string avatar = 4; + // Timestamp when the user was created + google.protobuf.Timestamp created_at = 5; + // Timestamp when the user was last updated + google.protobuf.Timestamp updated_at = 6; + // User-provided description or bio + optional string description = 7; + // User type + UserType user_type = 8; + // Public key + string public_key = 9; +} + +// Request message for creating a new user +message CreateUserRequest { + // Email address of the new user + string email = 1; + // Avatar for the new user + optional string avatar = 2; +} + +// Response message for CreateUser RPC +message CreateUserResponse { + // Temporary assigned id + string temp_id = 1; +} + +// Message to finalise user creation +message CompleteUserRequest { + // ID of the user to finalise + string id = 1; + // Required: username to finalise the user + string username = 2; + // Optional: user-provided description + optional string description = 3; + // Optional: update avatar + optional string avatar = 4; + // Inbox URL for this user + string inbox = 5; + // Outbox URL for this user + string outbox = 6; + // Is this user local or remote + bool local = 7; + // Public key for this user + string public_key = 8; + // User type + UserType user_type = 9; +} + +// Users gRPC service +service UsersService { + // Create a new user + rpc CreateUser (CreateUserRequest) returns (CreateUserResponse); + // Complete user + rpc CompleteUser (CompleteUserRequest) returns (User); +} diff --git a/lib/sellershut-core/src/lib.rs b/lib/sellershut-core/src/lib.rs index 70544cf..afbd20f 100644 --- a/lib/sellershut-core/src/lib.rs +++ b/lib/sellershut-core/src/lib.rs @@ -7,13 +7,13 @@ )] /// Protobuf types -#[cfg(any(feature = "auth", feature = "profile"))] +#[cfg(any(feature = "auth", feature = "users"))] pub mod google; /// Interactions with Auth server #[cfg(feature = "auth")] pub mod auth; -/// Interactions with Profile server -#[cfg(feature = "profile")] -pub mod profile; +/// Interactions with user server +#[cfg(feature = "users")] +pub mod users; diff --git a/lib/sellershut-core/src/profile.rs b/lib/sellershut-core/src/profile.rs deleted file mode 100644 index 06484d9..0000000 --- a/lib/sellershut-core/src/profile.rs +++ /dev/null @@ -1,4 +0,0 @@ -tonic::include_proto!("profile"); -/// Profile file descriptor -pub const PROFILE_FILE_DESCRIPTOR_SET: &[u8] = - tonic::include_file_descriptor_set!("profile_descriptor"); diff --git a/lib/sellershut-core/src/users.rs b/lib/sellershut-core/src/users.rs new file mode 100644 index 0000000..5721d53 --- /dev/null +++ b/lib/sellershut-core/src/users.rs @@ -0,0 +1,36 @@ +tonic::include_proto!("users"); +/// Users file descriptor +pub const USERS_FILE_DESCRIPTOR_SET: &[u8] = + tonic::include_file_descriptor_set!("users_descriptor"); + +impl std::fmt::Display for UserType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "{}", + match self { + UserType::Person => "person", + UserType::Application => "application", + UserType::Group => "group", + UserType::Organization => "organization", + UserType::Service => "service", + } + .to_uppercase() + ) + } +} + +impl std::str::FromStr for UserType { + type Err = String; + + fn from_str(value: &str) -> Result { + match value.to_lowercase().as_str() { + "person" => Ok(Self::Person), + "application" => Ok(Self::Application), + "group" => Ok(Self::Group), + "organization" => Ok(Self::Organization), + "service" => Ok(Self::Service), + _ => Err(format!("invalid user type: {value}")), + } + } +} -- cgit v1.2.3