Commit

Author:

Hash:

Timestamp:

+35 -139 +/-7 browse

Kevin Schoon [me@kevinschoon.com]

c8e85d2f60a74cf36b47380f19881b8e43badc2a

Sat, 26 Jul 2025 18:08:10 +0000 (3 months ago)

move ayllu web app to new identities module
1diff --git a/Cargo.lock b/Cargo.lock
2index 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
22index 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
34index 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
62index 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
228index 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
297index 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
306index 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 }