Author:
Hash:
Timestamp:
+35 -139 +/-7 browse
Kevin Schoon [me@kevinschoon.com]
c8e85d2f60a74cf36b47380f19881b8e43badc2a
Sat, 26 Jul 2025 18:08:10 +0000 (3 months ago)
| 1 | diff --git a/Cargo.lock b/Cargo.lock |
| 2 | index de317ae..d3fc63c 100644 |
| 3 | --- a/Cargo.lock |
| 4 | +++ b/Cargo.lock |
| 5 | @@ -324,6 +324,7 @@ dependencies = [ |
| 6 | "ayllu_api", |
| 7 | "ayllu_config", |
| 8 | "ayllu_git", |
| 9 | + "ayllu_identity", |
| 10 | "bytes", |
| 11 | "cc", |
| 12 | "clap 4.5.41", |
| 13 | @@ -431,6 +432,7 @@ version = "0.1.0" |
| 14 | dependencies = [ |
| 15 | "openssh-keys", |
| 16 | "serde", |
| 17 | + "url", |
| 18 | ] |
| 19 | |
| 20 | [[package]] |
| 21 | diff --git a/Cargo.toml b/Cargo.toml |
| 22 | index e428cbe..717542d 100644 |
| 23 | --- a/Cargo.toml |
| 24 | +++ b/Cargo.toml |
| 25 | @@ -35,6 +35,7 @@ futures-util = "0.3.31" |
| 26 | sqlx = { version = "0.8.6", features = [ "runtime-tokio-rustls", "sqlite", "macros", "time" ] } |
| 27 | tracing-subscriber = { version = "0.3.19", features = ["env-filter"] } |
| 28 | openssh-keys = "0.6.4" |
| 29 | + url = { version = "2.5.4", features = ["serde"]} |
| 30 | |
| 31 | tokio = { version = "1.46.1", features = ["full"] } |
| 32 | tokio-util = { version = "0.7.15", features = ["io", "compat"] } |
| 33 | diff --git a/ayllu/Cargo.toml b/ayllu/Cargo.toml |
| 34 | index 8d7b6f2..b92e330 100644 |
| 35 | --- a/ayllu/Cargo.toml |
| 36 | +++ b/ayllu/Cargo.toml |
| 37 | @@ -10,6 +10,7 @@ name = "ayllu" |
| 38 | [dependencies] |
| 39 | ayllu_api = { path = "../crates/api" } |
| 40 | ayllu_git = { path = "../crates/git" } |
| 41 | + ayllu_identity = {path = "../crates/identity"} |
| 42 | ayllu_config = { path = "../crates/config" } |
| 43 | timeutil = {path = "../crates/timeutil"} |
| 44 | |
| 45 | @@ -20,6 +21,7 @@ futures = { workspace = true } |
| 46 | thiserror = { workspace = true } |
| 47 | tracing = { workspace = true } |
| 48 | rand = { workspace = true } |
| 49 | + url = {workspace = true} |
| 50 | tokio = { workspace = true } |
| 51 | tokio-stream = { workspace = true } |
| 52 | toml = { workspace = true } |
| 53 | @@ -28,7 +30,6 @@ tokio-util = { workspace = true } |
| 54 | |
| 55 | serde = { version = "1.0", features = ["derive"] } |
| 56 | comrak = { version = "0.39.1", default-features = false } |
| 57 | - url = "2.5.4" |
| 58 | tree-sitter-highlight = "0.25.6" |
| 59 | tokei = "12.1.2" |
| 60 | time = "0.3.41" |
| 61 | diff --git a/ayllu/src/config.rs b/ayllu/src/config.rs |
| 62 | index 78452ae..000e65e 100644 |
| 63 | --- a/ayllu/src/config.rs |
| 64 | +++ b/ayllu/src/config.rs |
| 65 | @@ -1,9 +1,9 @@ |
| 66 | use std::fs::metadata; |
| 67 | - use std::path::Path; |
| 68 | use std::{collections::HashMap, path::PathBuf}; |
| 69 | use url::Url; |
| 70 | |
| 71 | - use ayllu_config::{Configurable, Error, Reader}; |
| 72 | + use ayllu_config::{Configurable, Error}; |
| 73 | + use ayllu_identity::Identity; |
| 74 | |
| 75 | use serde::{Deserialize, Serialize}; |
| 76 | |
| 77 | @@ -225,110 +225,6 @@ pub struct Lfs { |
| 78 | pub url_template: String, |
| 79 | } |
| 80 | |
| 81 | - pub mod identity { |
| 82 | - use super::*; |
| 83 | - |
| 84 | - #[derive(Deserialize, Serialize, Clone, Debug, Default)] |
| 85 | - pub struct Link { |
| 86 | - pub rel: String, |
| 87 | - pub href: Option<String>, |
| 88 | - pub template: Option<String>, |
| 89 | - pub mime_template: Option<String>, |
| 90 | - } |
| 91 | - |
| 92 | - #[derive(Deserialize, Serialize, Clone, Debug, Default)] |
| 93 | - pub struct UrlLink { |
| 94 | - pub url: String, |
| 95 | - pub mime_type: Option<String>, |
| 96 | - } |
| 97 | - |
| 98 | - #[derive(Deserialize, Serialize, Clone, Debug, Default)] |
| 99 | - pub struct Profile { |
| 100 | - pub email: String, |
| 101 | - pub tagline: Option<String>, |
| 102 | - pub avatar: Option<UrlLink>, |
| 103 | - pub profiles: Option<Vec<UrlLink>>, |
| 104 | - pub keys: Option<Vec<PublicKey>>, |
| 105 | - } |
| 106 | - |
| 107 | - /// An identity that has been verified and loaded from the host |
| 108 | - #[derive(Clone, Debug)] |
| 109 | - #[allow(dead_code)] |
| 110 | - pub struct Resolved { |
| 111 | - // user configuration in ~/.config/ayllu/config.toml (me) |
| 112 | - pub profile: Profile, |
| 113 | - pub collections: Vec<Collection>, |
| 114 | - // system user account |
| 115 | - pub user: nix::unistd::User, |
| 116 | - // fully qualified repositories path |
| 117 | - pub repositories: PathBuf, |
| 118 | - } |
| 119 | - |
| 120 | - #[derive(Deserialize, Serialize, Clone, Debug, Default)] |
| 121 | - pub struct Identity { |
| 122 | - pub username: String, |
| 123 | - #[serde(default = "Identity::default_repositories_path")] |
| 124 | - pub repositories_path: PathBuf, |
| 125 | - } |
| 126 | - |
| 127 | - impl Identity { |
| 128 | - fn default_repositories_path() -> PathBuf { |
| 129 | - Path::new("repos").to_path_buf() |
| 130 | - } |
| 131 | - } |
| 132 | - |
| 133 | - /// Configuration exposed to users |
| 134 | - #[derive(Deserialize, Serialize, Clone, Debug)] |
| 135 | - pub struct Config { |
| 136 | - #[serde(rename = "me")] |
| 137 | - pub self_: Option<Profile>, |
| 138 | - #[serde(default = "Vec::new")] |
| 139 | - pub collections: Vec<Collection>, |
| 140 | - } |
| 141 | - |
| 142 | - impl Configurable for Config { |
| 143 | - fn validate(&mut self) -> Result<(), Error> { |
| 144 | - Ok(()) |
| 145 | - } |
| 146 | - } |
| 147 | - |
| 148 | - pub(crate) fn resolve(identity: &Identity) -> Result<Resolved, Error> { |
| 149 | - let name = &identity.username; |
| 150 | - match nix::unistd::User::from_name(&identity.username) |
| 151 | - .map_err(|e| Error::Initialization(e.to_string()))? |
| 152 | - { |
| 153 | - Some(user) => { |
| 154 | - match Reader::<Config>::load(Some( |
| 155 | - user.dir.join(".config/ayllu/config.toml").as_path(), |
| 156 | - )) { |
| 157 | - Ok(user_config) => { |
| 158 | - match user_config.self_ { |
| 159 | - Some(profile) => Ok(Resolved { |
| 160 | - profile, |
| 161 | - collections: user_config.collections, |
| 162 | - repositories: user.dir.join(&identity.repositories_path), |
| 163 | - user, |
| 164 | - }), |
| 165 | - None => { |
| 166 | - // If the user did not specify their identity skip |
| 167 | - // them entirely. |
| 168 | - eprintln!("User {name} has no identity configuration, skipping",); |
| 169 | - Err(Error::Initialization(name.to_string())) |
| 170 | - } |
| 171 | - } |
| 172 | - } |
| 173 | - Err(err) => { |
| 174 | - // invalid user config will just be ignored |
| 175 | - eprintln!("Failed to load user {name} configuration {err}",); |
| 176 | - Err(Error::Initialization(name.to_string())) |
| 177 | - } |
| 178 | - } |
| 179 | - } |
| 180 | - None => Err(Error::Initialization(name.to_string())), |
| 181 | - } |
| 182 | - } |
| 183 | - } |
| 184 | - |
| 185 | #[derive(Deserialize, Serialize, Clone, Debug)] |
| 186 | pub struct Language { |
| 187 | pub name: String, |
| 188 | @@ -386,9 +282,8 @@ pub struct Config { |
| 189 | pub tree_sitter: Option<TreeSitter>, |
| 190 | pub languages: Option<Languages>, |
| 191 | pub lfs: Option<Lfs>, |
| 192 | - pub identities: Option<Vec<identity::Identity>>, |
| 193 | - #[serde(skip)] |
| 194 | - profiles: Vec<identity::Profile>, |
| 195 | + #[serde(default = "Vec::new")] |
| 196 | + pub identities: Vec<Identity>, |
| 197 | } |
| 198 | |
| 199 | impl Configurable for Config { |
| 200 | @@ -431,26 +326,11 @@ impl Configurable for Config { |
| 201 | ))); |
| 202 | } |
| 203 | |
| 204 | - // resolve user collections |
| 205 | - if let Some(identities) = &self.identities { |
| 206 | - self.collections.extend(identities.iter().try_fold( |
| 207 | - Vec::new(), |
| 208 | - |mut accm, ident| { |
| 209 | - let resolved = identity::resolve(ident)?; |
| 210 | - accm.extend(resolved.collections); |
| 211 | - Ok::<Vec<Collection>, Error>(accm) |
| 212 | - }, |
| 213 | - )?); |
| 214 | - } |
| 215 | Ok(()) |
| 216 | } |
| 217 | } |
| 218 | |
| 219 | impl Config { |
| 220 | - pub fn profiles(&self) -> &[identity::Profile] { |
| 221 | - self.profiles.as_slice() |
| 222 | - } |
| 223 | - |
| 224 | fn default_robots_txt() -> String { |
| 225 | String::from(DEFAULT_ROBOTS_TXT.trim_start()) |
| 226 | } |
| 227 | diff --git a/ayllu/src/web2/routes/finger.rs b/ayllu/src/web2/routes/finger.rs |
| 228 | index 30c2157..899c917 100644 |
| 229 | --- a/ayllu/src/web2/routes/finger.rs |
| 230 | +++ b/ayllu/src/web2/routes/finger.rs |
| 231 | @@ -2,11 +2,12 @@ use std::collections::HashMap; |
| 232 | use std::path::PathBuf; |
| 233 | |
| 234 | use axum::{extract::Extension, http::Uri, response::Json}; |
| 235 | + use ayllu_identity::Identity; |
| 236 | use url::Url; |
| 237 | use webfinger_rs::{Link, WebFingerRequest, WebFingerResponse}; |
| 238 | |
| 239 | use crate::{ |
| 240 | - config::{identity::Profile, Collection, Config}, |
| 241 | + config::{Collection, Config}, |
| 242 | web2::error::Error, |
| 243 | }; |
| 244 | use ayllu_git::{name, Error as GitError, Wrapper as Repository}; |
| 245 | @@ -47,7 +48,7 @@ fn get_all(collections: &[Collection]) -> Result<Vec<Pair>, ayllu_git::Error> { |
| 246 | |
| 247 | #[derive(Clone, Debug)] |
| 248 | enum Resource { |
| 249 | - Acct(Profile), |
| 250 | + Acct(Identity), |
| 251 | Collection((Collection, Vec<String>)), |
| 252 | Repository((Collection, PathBuf)), |
| 253 | Index, |
| 254 | @@ -58,7 +59,7 @@ enum Resource { |
| 255 | #[derive(Clone)] |
| 256 | pub struct Resolver { |
| 257 | pub collections: Vec<Collection>, |
| 258 | - pub identities: HashMap<String, Profile>, |
| 259 | + pub identities: HashMap<String, Identity>, |
| 260 | origin: Url, |
| 261 | } |
| 262 | |
| 263 | @@ -66,9 +67,9 @@ impl Resolver { |
| 264 | pub fn new(config: &Config) -> Self { |
| 265 | let identities = HashMap::from_iter( |
| 266 | config |
| 267 | - .profiles() |
| 268 | + .identities |
| 269 | .iter() |
| 270 | - .map(|profile| (profile.email.clone(), profile.clone())), |
| 271 | + .map(|identity| (identity.email.clone(), identity.clone())), |
| 272 | ); |
| 273 | Resolver { |
| 274 | origin: Url::parse(&config.origin).expect("Origin URL is invalid"), |
| 275 | @@ -152,14 +153,12 @@ impl Resolver { |
| 276 | match self.hint(resource)? { |
| 277 | Resource::Acct(author) => { |
| 278 | let mut links: Vec<Link> = Vec::new(); |
| 279 | - if let Some(profiles) = author.profiles.as_ref() { |
| 280 | - profiles.iter().for_each(|profile| { |
| 281 | - let mut link = Link::new(PROFILE_PAGE.into()); |
| 282 | - link.href = Some(profile.url.clone()); |
| 283 | - link.r#type = profile.mime_type.clone(); |
| 284 | - links.push(link); |
| 285 | - }); |
| 286 | - } |
| 287 | + author.profiles.iter().for_each(|profile| { |
| 288 | + let mut link = Link::new(PROFILE_PAGE.into()); |
| 289 | + link.href = Some(profile.url.to_string()); |
| 290 | + link.r#type = profile.mime_type.clone(); |
| 291 | + links.push(link); |
| 292 | + }); |
| 293 | if let Some(avatar) = author.avatar.as_ref() { |
| 294 | links.push(Link::builder(AVATAR).href(avatar.url.to_string()).build()); |
| 295 | } |
| 296 | diff --git a/crates/identity/Cargo.toml b/crates/identity/Cargo.toml |
| 297 | index 343b20c..33dc60d 100644 |
| 298 | --- a/crates/identity/Cargo.toml |
| 299 | +++ b/crates/identity/Cargo.toml |
| 300 | @@ -6,3 +6,4 @@ edition = "2024" |
| 301 | [dependencies] |
| 302 | serde = { workspace = true } |
| 303 | openssh-keys = { workspace = true } |
| 304 | + url = {workspace = true} |
| 305 | diff --git a/crates/identity/src/lib.rs b/crates/identity/src/lib.rs |
| 306 | index e8871e3..84f8f05 100644 |
| 307 | --- a/crates/identity/src/lib.rs |
| 308 | +++ b/crates/identity/src/lib.rs |
| 309 | @@ -1,6 +1,7 @@ |
| 310 | use serde::{Deserialize, Serialize}; |
| 311 | |
| 312 | pub use openssh_keys::PublicKey; |
| 313 | + use url::Url; |
| 314 | |
| 315 | #[derive(Clone, Debug)] |
| 316 | pub struct WrappedKey(pub PublicKey); |
| 317 | @@ -25,9 +26,20 @@ impl<'de> Deserialize<'de> for WrappedKey { |
| 318 | } |
| 319 | } |
| 320 | |
| 321 | - #[derive(Serialize, Deserialize, Clone, Default)] |
| 322 | + #[derive(Serialize, Deserialize, Clone, Debug)] |
| 323 | + pub struct WebItem { |
| 324 | + pub url: Url, |
| 325 | + pub mime_type: Option<String>, |
| 326 | + } |
| 327 | + |
| 328 | + #[derive(Serialize, Deserialize, Clone, Debug)] |
| 329 | pub struct Identity { |
| 330 | pub username: String, |
| 331 | #[serde(default = "Vec::new")] |
| 332 | pub authorized_keys: Vec<WrappedKey>, |
| 333 | + pub email: String, |
| 334 | + pub avatar: Option<WebItem>, |
| 335 | + pub tagline: Option<String>, |
| 336 | + #[serde(default = "Vec::new")] |
| 337 | + pub profiles: Vec<WebItem>, |
| 338 | } |