From 733704b628ef66a1a2a5db5ace31422c0b882562 Mon Sep 17 00:00:00 2001 From: rtkay123 Date: Sat, 2 Aug 2025 14:12:43 +0200 Subject: feat(users): get user --- crates/auth-service/src/server/grpc/auth.rs | 22 ++++- .../auth-service/src/server/routes/authorised.rs | 12 ++- crates/sellershut/Cargo.toml | 2 +- crates/sellershut/src/cnfg.rs | 1 + crates/sellershut/src/entity/user.rs | 91 +++++++-------------- crates/sellershut/src/state.rs | 93 ++++++++++++++++------ crates/users-service/src/server/manager.rs | 30 ++++++- lib/sellershut-core/proto/auth/auth.proto | 13 +++ lib/sellershut-core/proto/users/users.proto | 19 +++++ 9 files changed, 190 insertions(+), 93 deletions(-) diff --git a/crates/auth-service/src/server/grpc/auth.rs b/crates/auth-service/src/server/grpc/auth.rs index 87113a5..18e7546 100644 --- a/crates/auth-service/src/server/grpc/auth.rs +++ b/crates/auth-service/src/server/grpc/auth.rs @@ -3,8 +3,8 @@ use std::str::FromStr; use jsonwebtoken::DecodingKey; use sellershut_core::{ auth::{ - RegisterUserRequest, RegisterUserResponse, ValidationRequest, ValidationResponse, - auth_server::Auth, + GetPrivateKeyRequest, GetPrivateKeyResponse, RegisterUserRequest, RegisterUserResponse, + ValidationRequest, ValidationResponse, auth_server::Auth, }, users::CreateUserRequest, }; @@ -136,4 +136,22 @@ impl Auth for AppHandle { auth_id: user.id.to_string(), })) } + + async fn get_private_key( + &self, + request: Request, + ) -> Result, Status> { + let email = request.into_inner().email; + + let private_key = sqlx::query_scalar!( + "select private_key from auth_user where email = $1 + ", + uuid, + ) + .fetch_one(&self.services.postgres) + .await + .unwrap(); + + Ok(Response::new(GetPrivateKeyResponse { private_key })) + } } diff --git a/crates/auth-service/src/server/routes/authorised.rs b/crates/auth-service/src/server/routes/authorised.rs index 552f6c1..63c5a49 100644 --- a/crates/auth-service/src/server/routes/authorised.rs +++ b/crates/auth-service/src/server/routes/authorised.rs @@ -9,7 +9,9 @@ use axum::{ use axum_extra::{TypedHeader, headers}; use oauth2::{AuthorizationCode, TokenResponse}; use reqwest::{StatusCode, header::SET_COOKIE}; -use sellershut_core::auth::{RegisterUserRequest, auth_server::Auth, register_user_request::AccountDetails}; +use sellershut_core::auth::{ + RegisterUserRequest, auth_server::Auth, register_user_request::AccountDetails, +}; use serde::{Deserialize, Serialize}; use sqlx::types::uuid; use time::OffsetDateTime; @@ -123,10 +125,14 @@ pub async fn login_authorised( account: Some(AccountDetails { provider_id: provider, provider_user_id: data.id, - }) + }), }; - let resp = state.register_user(request.into_request()).await.unwrap().into_inner(); + let resp = state + .register_user(request.into_request()) + .await + .unwrap() + .into_inner(); Uuid::parse_str(&resp.auth_id).unwrap() }; diff --git a/crates/sellershut/Cargo.toml b/crates/sellershut/Cargo.toml index d742086..bc58b8d 100644 --- a/crates/sellershut/Cargo.toml +++ b/crates/sellershut/Cargo.toml @@ -19,7 +19,7 @@ enum_delegate = "0.2.0" futures-util.workspace = true nanoid.workspace = true openssl = "0.10.73" -sellershut-core = { workspace = true, features = ["auth"] } +sellershut-core = { workspace = true, features = ["auth", "users"] } serde = { workspace = true, features = ["derive"] } serde_json.workspace = true sha2 = "0.10.9" diff --git a/crates/sellershut/src/cnfg.rs b/crates/sellershut/src/cnfg.rs index 82cd34b..6fe9c39 100644 --- a/crates/sellershut/src/cnfg.rs +++ b/crates/sellershut/src/cnfg.rs @@ -6,4 +6,5 @@ pub struct LocalConfig { pub hostname: String, pub instance_name: String, pub auth_endpoint: String, + pub users_endpoint: String, } diff --git a/crates/sellershut/src/entity/user.rs b/crates/sellershut/src/entity/user.rs index 6fb12ae..5c9ac4f 100644 --- a/crates/sellershut/src/entity/user.rs +++ b/crates/sellershut/src/entity/user.rs @@ -7,27 +7,20 @@ use activitypub_federation::{ activity_sending::SendActivityTask, config::Data, fetch::object_id::ObjectId, - http_signatures::generate_actor_keypair, kinds::actor::{ApplicationType, GroupType, OrganizationType, PersonType, ServiceType}, protocol::{context::WithContext, public_key::PublicKey}, traits::{Activity, Actor, Object}, }; use async_trait::async_trait; use serde::{Deserialize, Serialize}; -use stack_up::Environment; use time::OffsetDateTime; use tracing::trace; use url::Url; -use uuid::Uuid; -use crate::{ - error::AppError, - state::{AppHandle, Services}, -}; +use crate::{error::AppError, state::AppHandle}; #[derive(PartialEq, Clone, Debug)] pub struct User { - pub id: String, pub username: String, pub ap_id: ObjectId, pub private_key: Option, @@ -67,6 +60,22 @@ pub enum UserType { Service(ServiceType), } +impl From for UserType { + fn from(value: sellershut_core::users::UserType) -> Self { + match value { + sellershut_core::users::UserType::Person => Self::Person(PersonType::default()), + sellershut_core::users::UserType::Application => { + Self::Application(ApplicationType::default()) + } + sellershut_core::users::UserType::Group => Self::Group(GroupType::default()), + sellershut_core::users::UserType::Organization => { + Self::Organization(OrganizationType::default()) + } + sellershut_core::users::UserType::Service => Self::Service(ServiceType::default()), + } + } +} + impl Display for UserType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( @@ -101,7 +110,6 @@ impl TryFrom for User { type Error = AppError; fn try_from(value: DbUser) -> Result { Ok(Self { - id: value.id, username: value.username, ap_id: Url::parse(&value.ap_id)?.into(), private_key: value.private_key, @@ -119,58 +127,18 @@ impl TryFrom for User { } impl User { - pub async fn new( - username: &str, - hostname: &str, - services: &Services, - environment: Environment, - ) -> Result { - trace!(username = ?username, "checking for system user"); - - let user = sqlx::query_as!( - DbUser, - "select * from account where username = $1 and local = $2", - username, - true - ) - .fetch_optional(&services.postgres) - .await?; - - if let Some(user) = user { - trace!(username = ?username, "system user exists"); - return Self::try_from(user); - } else { - trace!(username = ?username, "system user does not exist. creating"); - } - - trace!("creating keypair for new user"); - let keys = generate_actor_keypair()?; - let stub = &format!( - "{}://{hostname}/users/{username}", - match environment { - Environment::Development => "http", - Environment::Production => "https", - } - ); - let id = Uuid::now_v7(); - - let kind = UserType::Service(ServiceType::Service); - - trace!(id = ?id, "creating a new user"); - let user = sqlx::query_as!( - DbUser, - "insert into account (id, username, ap_id, private_key, public_key, inbox, outbox, local, user_type) values ($1, $2, $3, $4, $5, $6, $7, $8, $9) returning *", - id, - username, - stub, - keys.private_key, - keys.public_key, - &format!("{stub}/inbox"), - &format!("{stub}/outbox"), - true, - kind.to_string(), - ).fetch_one(&services.postgres).await?; - Self::try_from(user) + pub fn new(pk: &str, user: sellershut_core::users::User) -> Result { + Ok(Self { + username: user.username.clone(), + ap_id: Url::parse(&user.id)?.into(), + private_key: Some(pk.to_owned()), + description: user.description.clone(), + user_type: user.user_type().into(), + public_key: user.public_key, + inbox: Url::parse(&user.inbox)?, + outbox: Some(Url::parse(&user.outbox)?), + avatar_url: None, + }) } pub(crate) async fn send( @@ -317,7 +285,6 @@ impl Object for User { #[doc = " create and update, so an `upsert` operation should be used."] async fn from_json(json: Self::Kind, data: &Data) -> Result { Ok(Self { - id: todo!(), username: todo!(), ap_id: todo!(), private_key: todo!(), diff --git a/crates/sellershut/src/state.rs b/crates/sellershut/src/state.rs index cc58507..539217a 100644 --- a/crates/sellershut/src/state.rs +++ b/crates/sellershut/src/state.rs @@ -1,10 +1,15 @@ use std::{ops::Deref, sync::Arc}; use activitypub_federation::config::FederationConfig; -use sellershut_core::auth::auth_client::AuthClient; +use sellershut_core::{ + auth::{GetPrivateKeyRequest, RegisterUserRequest, auth_client::AuthClient}, + users::{ + CompleteUserRequest, GetUserRequest, UserType, users_service_client::UsersServiceClient, + }, +}; use sqlx::PgPool; use stack_up::{Configuration, Environment}; -use tonic::transport::Endpoint; +use tonic::{IntoRequest, transport::Endpoint}; use tracing::error; use crate::{ @@ -35,6 +40,7 @@ pub struct AppState { pub environment: Environment, pub protocol: Arc, pub auth_client: AuthClient, + pub users_client: UsersServiceClient, } impl AppState { @@ -44,30 +50,74 @@ impl AppState { ) -> Result, AppError> { let hut_config: LocalConfig = serde_json::from_value(configuration.misc.clone())?; - let user_id = &format!( - "{}://{}/users/{}", - match configuration.application.env { - Environment::Development => "http", - Environment::Production => "https", - }, - hut_config.hostname, - hut_config.instance_name + let protocol = match configuration.application.env { + Environment::Development => "http", + Environment::Production => "https", + }; + + let user_id = format!( + "{protocol}://{}/users/{}", + hut_config.hostname, hut_config.instance_name ); - let user = User::new( - &hut_config.instance_name, - &hut_config.hostname, - &services, - configuration.application.env, - ) - .await?; + let channel = Endpoint::new(hut_config.users_endpoint.to_string())? + .connect() + .await + .inspect_err(|e| error!("could not connect to users service: {e}"))?; + + let mut users_client = UsersServiceClient::with_interceptor(channel, MyInterceptor); let channel = Endpoint::new(hut_config.auth_endpoint.to_string())? .connect() .await .inspect_err(|e| error!("could not connect to auth service: {e}"))?; - let auth_client = AuthClient::with_interceptor(channel, MyInterceptor); + let mut auth_client = AuthClient::with_interceptor(channel, MyInterceptor); + + let user = if let Some(user) = users_client + .get_user( + GetUserRequest { + id: user_id.clone(), + } + .into_request(), + ) + .await? + .into_inner() + .user + { + user + } else { + let rg = auth_client + .register_user(RegisterUserRequest { + email: "email@example.com".into(), + account: None, + }) + .await? + .into_inner(); + + users_client + .complete_user(CompleteUserRequest { + id: rg.profile_id, + username: hut_config.instance_name.to_owned(), + inbox: format!("{user_id}/inbox"), + outbox: format!("{user_id}/outbox"), + local: true, + user_type: UserType::Service.into(), + ..Default::default() + }) + .await? + .into_inner() + }; + + let pk = auth_client + .get_private_key(GetPrivateKeyRequest { + email: user.email().to_owned(), + }) + .await? + .into_inner() + .private_key; + + let user = User::new(&pk, user)?; let config = FederationConfig::builder() .domain(&hut_config.hostname) @@ -75,12 +125,9 @@ impl AppState { .app_data(AppHandle(Arc::new(Self { services, environment: configuration.application.env, - protocol: match configuration.application.env { - Environment::Development => "http", - Environment::Production => "https", - } - .into(), + protocol: protocol.into(), auth_client, + users_client, }))) // .url_verifier(Box::new(MyUrlVerifier())) .debug(configuration.application.env == Environment::Development) diff --git a/crates/users-service/src/server/manager.rs b/crates/users-service/src/server/manager.rs index 05ee4fe..add385f 100644 --- a/crates/users-service/src/server/manager.rs +++ b/crates/users-service/src/server/manager.rs @@ -1,7 +1,7 @@ use prost::Message; use sellershut_core::users::{ - CompleteUserRequest, CreateUserRequest, CreateUserResponse, User, UserType, - users_service_server::UsersService, + CompleteUserRequest, CreateUserRequest, CreateUserResponse, GetUserRequest, GetUserResponse, + User, UserType, users_service_server::UsersService, }; use stack_up::redis::AsyncCommands; use time::OffsetDateTime; @@ -132,4 +132,30 @@ impl UsersService for AppHandle { Ok(Response::new(user)) } + + async fn get_user( + &self, + request: Request, + ) -> Result, Status> { + let inner = request.into_inner().id; + + let mut cache = self.cache().await?; + let resp = cache.get::<_, Vec>(inner).await.unwrap(); + // TODO: read from cache + + let user = sqlx::query_as!(DbUser, "select * from profile where id = $1", inner) + .fetch_optional(&self.services.postgres) + .await + .map_err(|_e| Status::internal("storage error"))?; + // TODO: save to cache + + let resp = GetUserResponse { + user: match user { + Some(user) => Some(User::try_from(user)?), + None => None, + }, + }; + + Ok(Response::new(resp)) + } } diff --git a/lib/sellershut-core/proto/auth/auth.proto b/lib/sellershut-core/proto/auth/auth.proto index 65792bf..5271d15 100644 --- a/lib/sellershut-core/proto/auth/auth.proto +++ b/lib/sellershut-core/proto/auth/auth.proto @@ -39,6 +39,17 @@ message ValidationResponse { bool valid = 1; } +// Define a message for the result of a token validation +message GetPrivateKeyRequest { + // Indicates whether the token is valid + string email = 1; +} +// Define a message for the result of a token validation +message GetPrivateKeyResponse { + // Indicates whether the token is valid + string private_key = 1; +} + // Define a message for the result of a token validation message RegisterUserResponse { // Indicates whether the token is valid @@ -66,4 +77,6 @@ service Auth { rpc ValidateAuthToken (ValidationRequest) returns (ValidationResponse); // Register User rpc RegisterUser (RegisterUserRequest) returns (RegisterUserResponse); + // Register User + rpc GetPrivateKey (GetPrivateKeyRequest) returns (GetPrivateKeyResponse); } diff --git a/lib/sellershut-core/proto/users/users.proto b/lib/sellershut-core/proto/users/users.proto index bbc8188..02cf338 100644 --- a/lib/sellershut-core/proto/users/users.proto +++ b/lib/sellershut-core/proto/users/users.proto @@ -5,10 +5,15 @@ package users; import "google/protobuf/timestamp.proto"; enum UserType { + // Person PERSON = 0; + // Application APPLICATION = 1; + // Group GROUP = 2; + // Organisation ORGANIZATION = 3; + // Service SERVICE = 4; } @@ -76,10 +81,24 @@ message CompleteUserRequest { UserType user_type = 8; } +// Message to get user +message GetUserRequest { + // ID of the user to get + string id = 1; +} + +// Message to get user +message GetUserResponse { + // ID of the user to get + User user = 1; +} + // Users gRPC service service UsersService { // Create a new user rpc CreateUser (CreateUserRequest) returns (CreateUserResponse); // Complete user rpc CompleteUser (CompleteUserRequest) returns (User); + // Get User + rpc GetUser (GetUserRequest) returns (GetUserResponse); } -- cgit v1.2.3