JavaScript is disabled, refresh for a better experience. ambee/giterated

ambee/giterated

Git repository hosting, collaboration, and discovery for the Fediverse.

Add settings

Amber - ⁨2⁩ years ago

parent: tbd commit: ⁨0448edb

Showing ⁨⁨11⁩ changed files⁩ with ⁨⁨253⁩ insertions⁩ and ⁨⁨11⁩ deletions⁩

.sqlx/query-b56ee50882e7eae84981cbfc91b5a53b1150d57a2da8fad5eebb9b1a88d7ccab.json

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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

View file
@@ -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 }