Add settings
parent: tbd commit: 0448edb
Showing 11 changed files with 253 insertions and 11 deletions
.sqlx/query-b56ee50882e7eae84981cbfc91b5a53b1150d57a2da8fad5eebb9b1a88d7ccab.json
@@ -0,0 +1,16 @@ | ||
1 | { | |
2 | "db_name": "PostgreSQL", | |
3 | "query": "INSERT INTO user_settings VALUES ($1, $2, $3) ON CONFLICT (username, name) DO UPDATE SET value = $3", | |
4 | "describe": { | |
5 | "columns": [], | |
6 | "parameters": { | |
7 | "Left": [ | |
8 | "Text", | |
9 | "Text", | |
10 | "Text" | |
11 | ] | |
12 | }, | |
13 | "nullable": [] | |
14 | }, | |
15 | "hash": "b56ee50882e7eae84981cbfc91b5a53b1150d57a2da8fad5eebb9b1a88d7ccab" | |
16 | } |
.sqlx/query-df25d6977ebaeea72c5f1648100fbbd30365d0e20194a80fd291f979e73d2e7c.json
@@ -0,0 +1,34 @@ | ||
1 | { | |
2 | "db_name": "PostgreSQL", | |
3 | "query": "SELECT * FROM user_settings WHERE username = $1", | |
4 | "describe": { | |
5 | "columns": [ | |
6 | { | |
7 | "ordinal": 0, | |
8 | "name": "username", | |
9 | "type_info": "Text" | |
10 | }, | |
11 | { | |
12 | "ordinal": 1, | |
13 | "name": "name", | |
14 | "type_info": "Text" | |
15 | }, | |
16 | { | |
17 | "ordinal": 2, | |
18 | "name": "value", | |
19 | "type_info": "Text" | |
20 | } | |
21 | ], | |
22 | "parameters": { | |
23 | "Left": [ | |
24 | "Text" | |
25 | ] | |
26 | }, | |
27 | "nullable": [ | |
28 | false, | |
29 | false, | |
30 | false | |
31 | ] | |
32 | }, | |
33 | "hash": "df25d6977ebaeea72c5f1648100fbbd30365d0e20194a80fd291f979e73d2e7c" | |
34 | } |
migrations/20230902095036_user_settings.sql
@@ -0,0 +1,8 @@ | ||
1 | CREATE TABLE IF NOT EXISTS user_settings | |
2 | ( | |
3 | username TEXT NOT NULL, | |
4 | name TEXT NOT NULL, | |
5 | value TEXT NOT NULL | |
6 | ); | |
7 | ||
8 | CREATE UNIQUE INDEX unique_per_name ON user_settings (username, name); | |
8 | \ No newline at end of file |
src/backend/mod.rs
@@ -5,6 +5,7 @@ pub mod user; | ||
5 | 5 | |
6 | 6 | use anyhow::Error; |
7 | 7 | use async_trait::async_trait; |
8 | use serde_json::Value; | |
8 | 9 | |
9 | 10 | use crate::backend::git::GitBackendError; |
10 | 11 | use crate::{ |
@@ -99,4 +100,11 @@ pub trait UserBackend: AuthBackend { | ||
99 | 100 | |
100 | 101 | async fn bio(&mut self, request: UserBioRequest) -> Result<UserBioResponse, Error>; |
101 | 102 | async fn exists(&mut self, user: &User) -> Result<bool, Error>; |
103 | ||
104 | async fn settings(&mut self, user: &User) -> Result<Vec<(String, Value)>, Error>; | |
105 | async fn write_settings( | |
106 | &mut self, | |
107 | user: &User, | |
108 | settings: &[(String, Value)], | |
109 | ) -> Result<(), Error>; | |
102 | 110 | } |
src/backend/user.rs
@@ -5,12 +5,14 @@ use anyhow::Error; | ||
5 | 5 | use aes_gcm::{aead::Aead, AeadCore, Aes256Gcm, Key, KeyInit}; |
6 | 6 | use argon2::{password_hash::SaltString, Argon2, PasswordHasher}; |
7 | 7 | use base64::{engine::general_purpose::STANDARD, Engine as _}; |
8 | use futures_util::StreamExt; | |
8 | 9 | use rsa::{ |
9 | 10 | pkcs8::{EncodePrivateKey, EncodePublicKey}, |
10 | 11 | rand_core::OsRng, |
11 | 12 | RsaPrivateKey, RsaPublicKey, |
12 | 13 | }; |
13 | use sqlx::PgPool; | |
14 | use serde_json::Value; | |
15 | use sqlx::{Either, PgPool}; | |
14 | 16 | use tokio::sync::Mutex; |
15 | 17 | |
16 | 18 | use crate::{ |
@@ -110,6 +112,49 @@ impl UserBackend for UserAuth { | ||
110 | 112 | .await |
111 | 113 | .is_err()) |
112 | 114 | } |
115 | ||
116 | async fn settings(&mut self, user: &User) -> Result<Vec<(String, Value)>, Error> { | |
117 | let settings = sqlx::query_as!( | |
118 | UserSettingRow, | |
119 | r#"SELECT * FROM user_settings WHERE username = $1"#, | |
120 | user.username | |
121 | ) | |
122 | .fetch_many(&self.pg_pool) | |
123 | .filter_map(|result| async move { | |
124 | if let Ok(Either::Right(row)) = result { | |
125 | Some(row) | |
126 | } else { | |
127 | None | |
128 | } | |
129 | }) | |
130 | .filter_map(|row| async move { | |
131 | if let Ok(value) = serde_json::from_str(&row.value) { | |
132 | Some((row.name, value)) | |
133 | } else { | |
134 | None | |
135 | } | |
136 | }) | |
137 | .collect::<Vec<_>>() | |
138 | .await; | |
139 | ||
140 | Ok(settings) | |
141 | } | |
142 | ||
143 | async fn write_settings( | |
144 | &mut self, | |
145 | user: &User, | |
146 | settings: &[(String, Value)], | |
147 | ) -> Result<(), Error> { | |
148 | for (name, value) in settings { | |
149 | let serialized = serde_json::to_string(value)?; | |
150 | ||
151 | sqlx::query!("INSERT INTO user_settings VALUES ($1, $2, $3) ON CONFLICT (username, name) DO UPDATE SET value = $3", | |
152 | user.username, name, serialized) | |
153 | .execute(&self.pg_pool).await?; | |
154 | } | |
155 | ||
156 | Ok(()) | |
157 | } | |
113 | 158 | } |
114 | 159 | |
115 | 160 | #[async_trait::async_trait] |
@@ -214,3 +259,11 @@ struct UserRow { | ||
214 | 259 | pub public_key: String, |
215 | 260 | pub enc_private_key: Vec<u8>, |
216 | 261 | } |
262 | ||
263 | #[allow(unused)] | |
264 | #[derive(Debug, sqlx::FromRow)] | |
265 | struct UserSettingRow { | |
266 | pub username: String, | |
267 | pub name: String, | |
268 | pub value: String, | |
269 | } |
src/connection/user.rs
@@ -1,5 +1,8 @@ | ||
1 | 1 | use anyhow::Error; |
2 | 2 | |
3 | use crate::messages::user::{ | |
4 | UserSettingsRequest, UserSettingsResponse, UserWriteSettingsRequest, UserWriteSettingsResponse, | |
5 | }; | |
3 | 6 | use crate::model::authenticated::AuthenticatedUser; |
4 | 7 | use crate::model::user::User; |
5 | 8 | use crate::{ |
@@ -38,6 +41,16 @@ pub async fn user_handle( | ||
38 | 41 | |
39 | 42 | Ok(true) |
40 | 43 | } |
44 | "&giterated_daemon::messages::user::UserSettingsRequest" => { | |
45 | user_settings.handle_message(&message, state).await?; | |
46 | ||
47 | Ok(true) | |
48 | } | |
49 | "&giterated_daemon::messages::user::UserWriteSettingsRequest" => { | |
50 | write_user_settings.handle_message(&message, state).await?; | |
51 | ||
52 | Ok(true) | |
53 | } | |
41 | 54 | _ => Ok(false), |
42 | 55 | } |
43 | 56 | } |
@@ -119,6 +132,52 @@ async fn repositories( | ||
119 | 132 | Ok(()) |
120 | 133 | } |
121 | 134 | |
135 | async fn user_settings( | |
136 | Message(request): Message<UserSettingsRequest>, | |
137 | State(connection_state): State<ConnectionState>, | |
138 | AuthenticatedUser(requesting_user): AuthenticatedUser, | |
139 | ) -> Result<(), UserError> { | |
140 | if request.user != requesting_user { | |
141 | return Err(UserError::InvalidUser(request.user)); | |
142 | } | |
143 | ||
144 | let mut user_backend = connection_state.user_backend.lock().await; | |
145 | let mut settings = user_backend.settings(&request.user).await?; | |
146 | ||
147 | drop(user_backend); | |
148 | ||
149 | let response = UserSettingsResponse { | |
150 | settings: settings.drain(..).collect(), | |
151 | }; | |
152 | ||
153 | connection_state.send(response).await?; | |
154 | ||
155 | Ok(()) | |
156 | } | |
157 | ||
158 | async fn write_user_settings( | |
159 | Message(request): Message<UserWriteSettingsRequest>, | |
160 | State(connection_state): State<ConnectionState>, | |
161 | AuthenticatedUser(requesting_user): AuthenticatedUser, | |
162 | ) -> Result<(), UserError> { | |
163 | if request.user != requesting_user { | |
164 | return Err(UserError::InvalidUser(request.user)); | |
165 | } | |
166 | ||
167 | let mut user_backend = connection_state.user_backend.lock().await; | |
168 | user_backend | |
169 | .write_settings(&request.user, &request.settings) | |
170 | .await?; | |
171 | ||
172 | drop(user_backend); | |
173 | ||
174 | let response = UserWriteSettingsResponse {}; | |
175 | ||
176 | connection_state.send(response).await?; | |
177 | ||
178 | Ok(()) | |
179 | } | |
180 | ||
122 | 181 | #[derive(Debug, thiserror::Error)] |
123 | 182 | pub enum UserError { |
124 | 183 | #[error("invalid user {0}")] |
src/connection/wrapper.rs
@@ -1,4 +1,5 @@ | ||
1 | 1 | use std::{ |
2 | collections::HashMap, | |
2 | 3 | net::SocketAddr, |
3 | 4 | sync::{ |
4 | 5 | atomic::{AtomicBool, Ordering}, |
@@ -8,9 +9,13 @@ use std::{ | ||
8 | 9 | |
9 | 10 | use anyhow::Error; |
10 | 11 | use futures_util::{SinkExt, StreamExt}; |
12 | use rsa::RsaPublicKey; | |
11 | 13 | use serde::Serialize; |
12 | 14 | use serde_json::Value; |
13 | use tokio::{net::TcpStream, sync::Mutex}; | |
15 | use tokio::{ | |
16 | net::TcpStream, | |
17 | sync::{Mutex, RwLock}, | |
18 | }; | |
14 | 19 | use tokio_tungstenite::{tungstenite::Message, WebSocketStream}; |
15 | 20 | |
16 | 21 | use crate::{ |
@@ -43,6 +48,7 @@ pub async fn connection_wrapper( | ||
43 | 48 | addr, |
44 | 49 | instance: instance.to_owned(), |
45 | 50 | handshaked: Arc::new(AtomicBool::new(false)), |
51 | cached_keys: Arc::default(), | |
46 | 52 | }; |
47 | 53 | |
48 | 54 | let mut handshaked = false; |
@@ -138,6 +144,7 @@ pub struct ConnectionState { | ||
138 | 144 | pub addr: SocketAddr, |
139 | 145 | pub instance: Instance, |
140 | 146 | pub handshaked: Arc<AtomicBool>, |
147 | pub cached_keys: Arc<RwLock<HashMap<Instance, RsaPublicKey>>>, | |
141 | 148 | } |
142 | 149 | |
143 | 150 | impl ConnectionState { |
src/messages/user.rs
@@ -1,3 +1,5 @@ | ||
1 | use std::collections::HashMap; | |
2 | ||
1 | 3 | use serde::{Deserialize, Serialize}; |
2 | 4 | |
3 | 5 | use crate::model::{repository::RepositorySummary, user::User}; |
@@ -41,3 +43,24 @@ pub struct UserRepositoriesRequest { | ||
41 | 43 | pub struct UserRepositoriesResponse { |
42 | 44 | pub repositories: Vec<RepositorySummary>, |
43 | 45 | } |
46 | ||
47 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
48 | pub struct UserSettingsRequest { | |
49 | pub user: User, | |
50 | } | |
51 | ||
52 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
53 | pub struct UserSettingsResponse { | |
54 | pub settings: HashMap<String, serde_json::Value>, | |
55 | } | |
56 | ||
57 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
58 | pub struct UserWriteSettingsRequest { | |
59 | pub user: User, | |
60 | pub settings: Vec<(String, serde_json::Value)>, | |
61 | } | |
62 | ||
63 | #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] | |
64 | pub struct UserWriteSettingsResponse { | |
65 | // IDK? | |
66 | } |
src/model/authenticated.rs
@@ -261,7 +261,7 @@ impl FromMessage<ConnectionState> for AuthenticatedInstance { | ||
261 | 261 | let (instance, signature) = message |
262 | 262 | .source |
263 | 263 | .iter() |
264 | .filter_map(|auth| { | |
264 | .filter_map(|auth: &AuthenticationSource| { | |
265 | 265 | if let AuthenticationSource::Instance { |
266 | 266 | instance, |
267 | 267 | signature, |
@@ -276,19 +276,29 @@ impl FromMessage<ConnectionState> for AuthenticatedInstance { | ||
276 | 276 | // TODO: Instance authentication error |
277 | 277 | .ok_or_else(|| UserAuthenticationError::Missing)?; |
278 | 278 | |
279 | let public_key = public_key(instance).await?; | |
280 | let public_key = RsaPublicKey::from_pkcs1_pem(&public_key).unwrap(); | |
279 | let public_key = { | |
280 | let cached_keys = state.cached_keys.read().await; | |
281 | ||
282 | if let Some(key) = cached_keys.get(&instance) { | |
283 | key.clone() | |
284 | } else { | |
285 | drop(cached_keys); | |
286 | let mut cached_keys = state.cached_keys.write().await; | |
287 | let key = public_key(instance).await?; | |
288 | let public_key = RsaPublicKey::from_pkcs1_pem(&key).unwrap(); | |
289 | cached_keys.insert(instance.clone(), public_key.clone()); | |
290 | public_key | |
291 | } | |
292 | }; | |
281 | 293 | |
282 | 294 | let verifying_key: VerifyingKey<Sha256> = VerifyingKey::new(public_key); |
283 | 295 | |
284 | 296 | let message_json = serde_json::to_vec(&message.message).unwrap(); |
285 | 297 | |
286 | verifying_key | |
287 | .verify( | |
288 | &message_json, | |
289 | &Signature::try_from(signature.as_ref()).unwrap(), | |
290 | ) | |
291 | .unwrap(); | |
298 | verifying_key.verify( | |
299 | &message_json, | |
300 | &Signature::try_from(signature.as_ref()).unwrap(), | |
301 | )?; | |
292 | 302 | |
293 | 303 | Ok(AuthenticatedInstance(instance.clone())) |
294 | 304 | } |
src/model/mod.rs
@@ -7,4 +7,5 @@ pub mod authenticated; | ||
7 | 7 | pub mod discovery; |
8 | 8 | pub mod instance; |
9 | 9 | pub mod repository; |
10 | pub mod settings; | |
10 | 11 | pub mod user; |
src/model/settings.rs
@@ -0,0 +1,23 @@ | ||
1 | use serde::{Deserialize, Serialize}; | |
2 | ||
3 | pub trait Setting: Serialize { | |
4 | fn name(&self) -> &'static str; | |
5 | } | |
6 | ||
7 | #[derive(Debug, Serialize, Deserialize)] | |
8 | pub struct UserBio(pub String); | |
9 | ||
10 | impl Setting for UserBio { | |
11 | fn name(&self) -> &'static str { | |
12 | "Bio" | |
13 | } | |
14 | } | |
15 | ||
16 | #[derive(Debug, Serialize, Deserialize)] | |
17 | pub struct UserDisplayName(pub String); | |
18 | ||
19 | impl Setting for UserDisplayName { | |
20 | fn name(&self) -> &'static str { | |
21 | "Display Name" | |
22 | } | |
23 | } |