diff --git a/.sqlx/query-b56ee50882e7eae84981cbfc91b5a53b1150d57a2da8fad5eebb9b1a88d7ccab.json b/.sqlx/query-b56ee50882e7eae84981cbfc91b5a53b1150d57a2da8fad5eebb9b1a88d7ccab.json new file mode 100644 index 0000000..9832255 --- /dev/null +++ b/.sqlx/query-b56ee50882e7eae84981cbfc91b5a53b1150d57a2da8fad5eebb9b1a88d7ccab.json @@ -0,0 +1,16 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO user_settings VALUES ($1, $2, $3) ON CONFLICT (username, name) DO UPDATE SET value = $3", + "describe": { + "columns": [], + "parameters": { + "Left": [ + "Text", + "Text", + "Text" + ] + }, + "nullable": [] + }, + "hash": "b56ee50882e7eae84981cbfc91b5a53b1150d57a2da8fad5eebb9b1a88d7ccab" +} diff --git a/.sqlx/query-df25d6977ebaeea72c5f1648100fbbd30365d0e20194a80fd291f979e73d2e7c.json b/.sqlx/query-df25d6977ebaeea72c5f1648100fbbd30365d0e20194a80fd291f979e73d2e7c.json new file mode 100644 index 0000000..6fe8469 --- /dev/null +++ b/.sqlx/query-df25d6977ebaeea72c5f1648100fbbd30365d0e20194a80fd291f979e73d2e7c.json @@ -0,0 +1,34 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT * FROM user_settings WHERE username = $1", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "username", + "type_info": "Text" + }, + { + "ordinal": 1, + "name": "name", + "type_info": "Text" + }, + { + "ordinal": 2, + "name": "value", + "type_info": "Text" + } + ], + "parameters": { + "Left": [ + "Text" + ] + }, + "nullable": [ + false, + false, + false + ] + }, + "hash": "df25d6977ebaeea72c5f1648100fbbd30365d0e20194a80fd291f979e73d2e7c" +} diff --git a/migrations/20230902095036_user_settings.sql b/migrations/20230902095036_user_settings.sql new file mode 100644 index 0000000..f83f20a --- /dev/null +++ b/migrations/20230902095036_user_settings.sql @@ -0,0 +1,8 @@ +CREATE TABLE IF NOT EXISTS user_settings +( + username TEXT NOT NULL, + name TEXT NOT NULL, + value TEXT NOT NULL +); + +CREATE UNIQUE INDEX unique_per_name ON user_settings (username, name); \ No newline at end of file diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 44c8a1d..88cddc6 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -5,6 +5,7 @@ pub mod user; use anyhow::Error; use async_trait::async_trait; +use serde_json::Value; use crate::backend::git::GitBackendError; use crate::{ @@ -99,4 +100,11 @@ pub trait UserBackend: AuthBackend { async fn bio(&mut self, request: UserBioRequest) -> Result; async fn exists(&mut self, user: &User) -> Result; + + async fn settings(&mut self, user: &User) -> Result, Error>; + async fn write_settings( + &mut self, + user: &User, + settings: &[(String, Value)], + ) -> Result<(), Error>; } diff --git a/src/backend/user.rs b/src/backend/user.rs index 471712b..da7cf80 100644 --- a/src/backend/user.rs +++ b/src/backend/user.rs @@ -5,12 +5,14 @@ use anyhow::Error; use aes_gcm::{aead::Aead, AeadCore, Aes256Gcm, Key, KeyInit}; use argon2::{password_hash::SaltString, Argon2, PasswordHasher}; use base64::{engine::general_purpose::STANDARD, Engine as _}; +use futures_util::StreamExt; use rsa::{ pkcs8::{EncodePrivateKey, EncodePublicKey}, rand_core::OsRng, RsaPrivateKey, RsaPublicKey, }; -use sqlx::PgPool; +use serde_json::Value; +use sqlx::{Either, PgPool}; use tokio::sync::Mutex; use crate::{ @@ -110,6 +112,49 @@ impl UserBackend for UserAuth { .await .is_err()) } + + async fn settings(&mut self, user: &User) -> Result, Error> { + let settings = sqlx::query_as!( + UserSettingRow, + r#"SELECT * FROM user_settings WHERE username = $1"#, + user.username + ) + .fetch_many(&self.pg_pool) + .filter_map(|result| async move { + if let Ok(Either::Right(row)) = result { + Some(row) + } else { + None + } + }) + .filter_map(|row| async move { + if let Ok(value) = serde_json::from_str(&row.value) { + Some((row.name, value)) + } else { + None + } + }) + .collect::>() + .await; + + Ok(settings) + } + + async fn write_settings( + &mut self, + user: &User, + settings: &[(String, Value)], + ) -> Result<(), Error> { + for (name, value) in settings { + let serialized = serde_json::to_string(value)?; + + sqlx::query!("INSERT INTO user_settings VALUES ($1, $2, $3) ON CONFLICT (username, name) DO UPDATE SET value = $3", + user.username, name, serialized) + .execute(&self.pg_pool).await?; + } + + Ok(()) + } } #[async_trait::async_trait] @@ -214,3 +259,11 @@ struct UserRow { pub public_key: String, pub enc_private_key: Vec, } + +#[allow(unused)] +#[derive(Debug, sqlx::FromRow)] +struct UserSettingRow { + pub username: String, + pub name: String, + pub value: String, +} diff --git a/src/connection/user.rs b/src/connection/user.rs index fa2fa9e..8a20020 100644 --- a/src/connection/user.rs +++ b/src/connection/user.rs @@ -1,5 +1,8 @@ use anyhow::Error; +use crate::messages::user::{ + UserSettingsRequest, UserSettingsResponse, UserWriteSettingsRequest, UserWriteSettingsResponse, +}; use crate::model::authenticated::AuthenticatedUser; use crate::model::user::User; use crate::{ @@ -38,6 +41,16 @@ pub async fn user_handle( Ok(true) } + "&giterated_daemon::messages::user::UserSettingsRequest" => { + user_settings.handle_message(&message, state).await?; + + Ok(true) + } + "&giterated_daemon::messages::user::UserWriteSettingsRequest" => { + write_user_settings.handle_message(&message, state).await?; + + Ok(true) + } _ => Ok(false), } } @@ -119,6 +132,52 @@ async fn repositories( Ok(()) } +async fn user_settings( + Message(request): Message, + State(connection_state): State, + AuthenticatedUser(requesting_user): AuthenticatedUser, +) -> Result<(), UserError> { + if request.user != requesting_user { + return Err(UserError::InvalidUser(request.user)); + } + + let mut user_backend = connection_state.user_backend.lock().await; + let mut settings = user_backend.settings(&request.user).await?; + + drop(user_backend); + + let response = UserSettingsResponse { + settings: settings.drain(..).collect(), + }; + + connection_state.send(response).await?; + + Ok(()) +} + +async fn write_user_settings( + Message(request): Message, + State(connection_state): State, + AuthenticatedUser(requesting_user): AuthenticatedUser, +) -> Result<(), UserError> { + if request.user != requesting_user { + return Err(UserError::InvalidUser(request.user)); + } + + let mut user_backend = connection_state.user_backend.lock().await; + user_backend + .write_settings(&request.user, &request.settings) + .await?; + + drop(user_backend); + + let response = UserWriteSettingsResponse {}; + + connection_state.send(response).await?; + + Ok(()) +} + #[derive(Debug, thiserror::Error)] pub enum UserError { #[error("invalid user {0}")] diff --git a/src/connection/wrapper.rs b/src/connection/wrapper.rs index f544091..7218238 100644 --- a/src/connection/wrapper.rs +++ b/src/connection/wrapper.rs @@ -1,4 +1,5 @@ use std::{ + collections::HashMap, net::SocketAddr, sync::{ atomic::{AtomicBool, Ordering}, @@ -8,9 +9,13 @@ use std::{ use anyhow::Error; use futures_util::{SinkExt, StreamExt}; +use rsa::RsaPublicKey; use serde::Serialize; use serde_json::Value; -use tokio::{net::TcpStream, sync::Mutex}; +use tokio::{ + net::TcpStream, + sync::{Mutex, RwLock}, +}; use tokio_tungstenite::{tungstenite::Message, WebSocketStream}; use crate::{ @@ -43,6 +48,7 @@ pub async fn connection_wrapper( addr, instance: instance.to_owned(), handshaked: Arc::new(AtomicBool::new(false)), + cached_keys: Arc::default(), }; let mut handshaked = false; @@ -138,6 +144,7 @@ pub struct ConnectionState { pub addr: SocketAddr, pub instance: Instance, pub handshaked: Arc, + pub cached_keys: Arc>>, } impl ConnectionState { diff --git a/src/messages/user.rs b/src/messages/user.rs index 6fe1431..291368c 100644 --- a/src/messages/user.rs +++ b/src/messages/user.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; + use serde::{Deserialize, Serialize}; use crate::model::{repository::RepositorySummary, user::User}; @@ -41,3 +43,24 @@ pub struct UserRepositoriesRequest { pub struct UserRepositoriesResponse { pub repositories: Vec, } + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct UserSettingsRequest { + pub user: User, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct UserSettingsResponse { + pub settings: HashMap, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct UserWriteSettingsRequest { + pub user: User, + pub settings: Vec<(String, serde_json::Value)>, +} + +#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] +pub struct UserWriteSettingsResponse { + // IDK? +} diff --git a/src/model/authenticated.rs b/src/model/authenticated.rs index 6248463..8706451 100644 --- a/src/model/authenticated.rs +++ b/src/model/authenticated.rs @@ -261,7 +261,7 @@ impl FromMessage for AuthenticatedInstance { let (instance, signature) = message .source .iter() - .filter_map(|auth| { + .filter_map(|auth: &AuthenticationSource| { if let AuthenticationSource::Instance { instance, signature, @@ -276,19 +276,29 @@ impl FromMessage for AuthenticatedInstance { // TODO: Instance authentication error .ok_or_else(|| UserAuthenticationError::Missing)?; - let public_key = public_key(instance).await?; - let public_key = RsaPublicKey::from_pkcs1_pem(&public_key).unwrap(); + let public_key = { + let cached_keys = state.cached_keys.read().await; + + if let Some(key) = cached_keys.get(&instance) { + key.clone() + } else { + drop(cached_keys); + let mut cached_keys = state.cached_keys.write().await; + let key = public_key(instance).await?; + let public_key = RsaPublicKey::from_pkcs1_pem(&key).unwrap(); + cached_keys.insert(instance.clone(), public_key.clone()); + public_key + } + }; let verifying_key: VerifyingKey = VerifyingKey::new(public_key); let message_json = serde_json::to_vec(&message.message).unwrap(); - verifying_key - .verify( - &message_json, - &Signature::try_from(signature.as_ref()).unwrap(), - ) - .unwrap(); + verifying_key.verify( + &message_json, + &Signature::try_from(signature.as_ref()).unwrap(), + )?; Ok(AuthenticatedInstance(instance.clone())) } diff --git a/src/model/mod.rs b/src/model/mod.rs index 80fcc43..e09885d 100644 --- a/src/model/mod.rs +++ b/src/model/mod.rs @@ -7,4 +7,5 @@ pub mod authenticated; pub mod discovery; pub mod instance; pub mod repository; +pub mod settings; pub mod user; diff --git a/src/model/settings.rs b/src/model/settings.rs new file mode 100644 index 0000000..a3ea173 --- /dev/null +++ b/src/model/settings.rs @@ -0,0 +1,23 @@ +use serde::{Deserialize, Serialize}; + +pub trait Setting: Serialize { + fn name(&self) -> &'static str; +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct UserBio(pub String); + +impl Setting for UserBio { + fn name(&self) -> &'static str { + "Bio" + } +} + +#[derive(Debug, Serialize, Deserialize)] +pub struct UserDisplayName(pub String); + +impl Setting for UserDisplayName { + fn name(&self) -> &'static str { + "Display Name" + } +}