Pick any line to see why it is the way it is: the commit, the pull request and issue it came from, and what the agent was thinking.
| A last-commits walk answers within 2.5 s with what it has, and keeps only finished answers | 1 | //! Staff-only views of workspaces, for sudo.g1t.sh. |
| 2 | //! | |
| 3 | //! These methods take no viewer and check no membership. Only sudo calls | |
| 4 | //! them, over its service binding, once it has verified a Cloudflare Access | |
| 5 | //! sign-in against its staff list; nothing a customer can reach forwards to | |
| 6 | //! them. They read, and never change, anything. | |
| 7 | //! | |
| 8 | //! Every workspace is listed: there is one kind, and a person's own space | |
| 9 | //! is simply a workspace with one member. A user with no workspace has | |
| 10 | //! nothing to list, as nothing can exist outside one. | |
| 11 | ||
| 12 | use std::collections::HashMap; | |
| 13 | ||
| 14 | use g1t_contracts::Role; | |
| 15 | use g1t_contracts::identity::*; | |
| 16 | use serde::Deserialize; | |
| 17 | use worker::Result; | |
| 18 | ||
| 19 | use crate::Identity; | |
| 20 | ||
| 21 | #[derive(Deserialize)] | |
| 22 | struct ListRow { | |
| 23 | id: String, | |
| 24 | slug: String, | |
| 25 | name: String, | |
| 26 | created_at: String, | |
| 27 | member_count: u32, | |
| 28 | } | |
| 29 | ||
| 30 | #[derive(Deserialize)] | |
| 31 | struct OwnerRow { | |
| 32 | workspace_id: String, | |
| 33 | username: String, | |
| 34 | email: Option<String>, | |
| 35 | } | |
| 36 | ||
| 37 | #[derive(Deserialize)] | |
| 38 | struct DetailRow { | |
| 39 | id: String, | |
| 40 | slug: String, | |
| 41 | name: String, | |
| 42 | description: Option<String>, | |
| 43 | created_at: String, | |
| 44 | #[serde(default)] | |
| 45 | protected: u8, | |
| 46 | } | |
| 47 | ||
| 48 | #[derive(Deserialize)] | |
| 49 | struct MemberRow { | |
| 50 | username: String, | |
| 51 | email: Option<String>, | |
| 52 | role: Role, | |
| 53 | joined: String, | |
| 54 | } | |
| 55 | ||
| 56 | /// A `LIKE` pattern matching `query` anywhere, lowercased, with `%`, `_` | |
| 57 | /// and the escape character itself taken literally. None for a blank query. | |
| 58 | pub(crate) fn like_pattern(query: Option<&str>) -> Option<String> { | |
| 59 | let query = query.map(str::trim).filter(|q| !q.is_empty())?; | |
| 60 | let mut pattern = String::from("%"); | |
| 61 | for c in query.to_lowercase().chars().take(100) { | |
| 62 | if matches!(c, '%' | '_' | '\\') { | |
| 63 | pattern.push('\\'); | |
| 64 | } | |
| 65 | pattern.push(c); | |
| 66 | } | |
| 67 | pattern.push('%'); | |
| 68 | Some(pattern) | |
| 69 | } | |
| 70 | ||
| 71 | /// The workspaces, in their order, each with its owners. | |
| 72 | fn with_owners(rows: Vec<ListRow>, owners: Vec<OwnerRow>) -> Vec<AdminWorkspace> { | |
| 73 | let mut by_workspace: HashMap<String, Vec<AdminOwner>> = HashMap::new(); | |
| 74 | for owner in owners { | |
| 75 | by_workspace.entry(owner.workspace_id).or_default().push(AdminOwner { | |
| 76 | username: owner.username, | |
| 77 | email: owner.email, | |
| 78 | }); | |
| 79 | } | |
| 80 | rows.into_iter() | |
| 81 | .map(|row| AdminWorkspace { | |
| 82 | owners: by_workspace.remove(&row.id).unwrap_or_default(), | |
| 83 | slug: row.slug, | |
| 84 | name: row.name, | |
| 85 | created_at: row.created_at, | |
| 86 | member_count: row.member_count, | |
| 87 | }) | |
| 88 | .collect() | |
| 89 | } | |
| 90 | ||
| 91 | impl Identity { | |
| 92 | /// The workspaces staff can see, newest first. Staff only: see the | |
| 93 | /// module's note. | |
| 94 | /// Emails each owner of the workspace with a confirmed address. Only | |
| 95 | /// other services call this; the link must stay on g1t.sh, so a notice | |
| 96 | /// can never point owners anywhere else. | |
| 97 | pub async fn notify_owners(&self, a: NotifyOwnersArgs) -> Result<u32> { | |
| 98 | if !a.link.starts_with("https://g1t.sh/") { | |
| 99 | return Ok(0); | |
| 100 | } | |
| 101 | #[derive(Deserialize)] | |
| 102 | struct Owner { | |
| 103 | email: Option<String>, | |
| 104 | } | |
| 105 | let owners = self | |
| 106 | .db | |
| 107 | .prepare( | |
| 108 | "SELECT u.email FROM workspace_members m | |
| 109 | JOIN users u ON u.id = m.user_id | |
| 110 | JOIN workspaces w ON w.id = m.workspace_id | |
| 111 | WHERE w.slug = ? AND w.deleted_at IS NULL AND m.role = 'owner' AND u.email_verified_at IS NOT NULL", | |
| 112 | ) | |
| 113 | .bind(&[a.workspace.to_lowercase().into()])? | |
| 114 | .all() | |
| 115 | .await? | |
| 116 | .results::<Owner>()?; | |
| 117 | let mut sent = 0; | |
| 118 | for email in owners.into_iter().filter_map(|owner| owner.email) { | |
| 119 | match crate::email::send_link(&self.env, &email, &a.subject, &a.intro, &a.action, &a.link, &a.footer).await { | |
| 120 | Ok(()) => sent += 1, | |
| 121 | Err(error) => worker::console_error!("could not email an owner of {}: {error}", a.workspace), | |
| 122 | } | |
| 123 | } | |
| 124 | Ok(sent) | |
| 125 | } | |
| 126 | ||
| 127 | pub async fn admin_workspaces(&self, a: AdminWorkspacesArgs) -> Result<Vec<AdminWorkspace>> { | |
| 128 | let pattern = like_pattern(a.query.as_deref()); | |
| 129 | // The same filter picks the workspaces and, below, their owners. | |
| 130 | // Deleted ones are listed apart (`admin_deleted_workspaces`). | |
| 131 | let filter = if pattern.is_some() { | |
| 132 | "WHERE w.deleted_at IS NULL AND (w.slug LIKE ?1 ESCAPE '\\' OR lower(w.name) LIKE ?1 ESCAPE '\\' | |
| 133 | OR EXISTS (SELECT 1 FROM workspace_members om JOIN users ou ON ou.id = om.user_id | |
| 134 | WHERE om.workspace_id = w.id AND om.role = 'owner' | |
| 135 | AND (lower(ou.username) LIKE ?1 ESCAPE '\\' OR lower(ou.email) LIKE ?1 ESCAPE '\\')))" | |
| 136 | } else { | |
| 137 | "WHERE w.deleted_at IS NULL" | |
| 138 | }; | |
| 139 | let chosen = format!( | |
| 140 | "SELECT w.id FROM workspaces w {filter} | |
| 141 | ORDER BY w.created_at DESC, w.slug LIMIT {ADMIN_WORKSPACES_LIMIT}" | |
| 142 | ); | |
| 143 | let binds: Vec<worker::wasm_bindgen::JsValue> = pattern.into_iter().map(Into::into).collect(); | |
| 144 | let rows = self | |
| 145 | .db | |
| 146 | .prepare(format!( | |
| 147 | "SELECT w.id, w.slug, w.name, w.created_at, | |
| 148 | (SELECT count(*) FROM workspace_members m WHERE m.workspace_id = w.id) AS member_count | |
| 149 | FROM workspaces w WHERE w.id IN ({chosen}) | |
| 150 | ORDER BY w.created_at DESC, w.slug" | |
| 151 | )) | |
| 152 | .bind(&binds)? | |
| 153 | .all() | |
| 154 | .await? | |
| 155 | .results::<ListRow>()?; | |
| 156 | let owners = self | |
| 157 | .db | |
| 158 | .prepare(format!( | |
| 159 | "SELECT m.workspace_id, u.username, u.email FROM workspace_members m | |
| 160 | JOIN users u ON u.id = m.user_id | |
| 161 | WHERE m.role = 'owner' AND m.workspace_id IN ({chosen}) | |
| 162 | ORDER BY m.created_at, u.username" | |
| 163 | )) | |
| 164 | .bind(&binds)? | |
| 165 | .all() | |
| 166 | .await? | |
| 167 | .results::<OwnerRow>()?; | |
| 168 | Ok(with_owners(rows, owners)) | |
| 169 | } | |
| 170 | ||
| 171 | /// One workspace with every member, or None. Staff only: see the | |
| 172 | /// module's note. | |
| 173 | pub async fn admin_workspace(&self, a: SlugArgs) -> Result<Option<AdminWorkspaceDetail>> { | |
| 174 | let Some(row) = self | |
| 175 | .db | |
| 176 | .prepare("SELECT id, slug, name, description, created_at, protected FROM workspaces WHERE slug = ?") | |
| 177 | .bind(&[a.slug.trim().to_lowercase().into()])? | |
| 178 | .first::<DetailRow>(None) | |
| 179 | .await? | |
| 180 | else { | |
| 181 | return Ok(None); | |
| 182 | }; | |
| 183 | let members = self | |
| 184 | .db | |
| 185 | .prepare( | |
| 186 | "SELECT u.username, u.email, m.role, m.created_at AS joined FROM workspace_members m | |
| 187 | JOIN users u ON u.id = m.user_id | |
| 188 | WHERE m.workspace_id = ? | |
| 189 | ORDER BY m.role DESC, u.username", | |
| 190 | ) | |
| 191 | .bind(&[row.id.as_str().into()])? | |
| 192 | .all() | |
| 193 | .await? | |
| 194 | .results::<MemberRow>()? | |
| 195 | .into_iter() | |
| 196 | .map(|m| AdminMember { | |
| 197 | username: m.username, | |
| 198 | email: m.email, | |
| 199 | role: m.role, | |
| 200 | joined: m.joined, | |
| 201 | }) | |
| 202 | .collect(); | |
| 203 | let protected = self.is_protected(&row.id, &row.slug, row.protected != 0).await?; | |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 204 | let suspended = self.workspace_suspension(&row.slug).await?; |
| A last-commits walk answers within 2.5 s with what it has, and keeps only finished answers | 205 | Ok(Some(AdminWorkspaceDetail { |
| 206 | protected, | |
| Merge main into rm-agents: mirroring removal, open signups, free-tier limits, the g1t CLI and runner fixes, with the agent removal kept. The CLI's table of operations is regenerated from the API without the agent, customize and AI credit operations; no CLI command used them. | 207 | suspended, |
| A last-commits walk answers within 2.5 s with what it has, and keeps only finished answers | 208 | slug: row.slug, |
| 209 | name: row.name, | |
| 210 | description: row.description, | |
| 211 | created_at: row.created_at, | |
| 212 | members, | |
| 213 | })) | |
| 214 | } | |
| 215 | } | |
| 216 | ||
| 217 | #[cfg(test)] | |
| 218 | mod tests { | |
| 219 | use super::*; | |
| 220 | ||
| 221 | #[test] | |
| 222 | fn a_blank_query_matches_everything() { | |
| 223 | assert_eq!(like_pattern(None), None); | |
| 224 | assert_eq!(like_pattern(Some(" ")), None); | |
| 225 | } | |
| 226 | ||
| 227 | #[test] | |
| 228 | fn a_query_is_matched_literally_anywhere() { | |
| 229 | assert_eq!(like_pattern(Some(" Acme ")).as_deref(), Some("%acme%")); | |
| 230 | assert_eq!(like_pattern(Some("50%_off\\")).as_deref(), Some("%50\\%\\_off\\\\%")); | |
| 231 | } | |
| 232 | ||
| 233 | #[test] | |
| 234 | fn owners_go_to_their_workspaces_in_order() { | |
| 235 | let row = |id: &str, slug: &str| ListRow { | |
| 236 | id: id.into(), | |
| 237 | slug: slug.into(), | |
| 238 | name: slug.into(), | |
| 239 | created_at: "2026-10-04T00:00:00Z".into(), | |
| 240 | member_count: 2, | |
| 241 | }; | |
| 242 | let owner = |workspace_id: &str, username: &str| OwnerRow { | |
| 243 | workspace_id: workspace_id.into(), | |
| 244 | username: username.into(), | |
| 245 | email: Some(format!("{username}@example.com")), | |
| 246 | }; | |
| 247 | let listed = with_owners( | |
| 248 | vec![row("wsp_2", "newer"), row("wsp_1", "older"), row("wsp_3", "orphan")], | |
| 249 | vec![owner("wsp_1", "ada"), owner("wsp_2", "bob"), owner("wsp_1", "cy")], | |
| 250 | ); | |
| 251 | let slugs: Vec<_> = listed.iter().map(|w| w.slug.as_str()).collect(); | |
| 252 | assert_eq!(slugs, ["newer", "older", "orphan"]); | |
| 253 | let owners = |i: usize| listed[i].owners.iter().map(|o| o.username.as_str()).collect::<Vec<_>>(); | |
| 254 | assert_eq!(owners(0), ["bob"]); | |
| 255 | assert_eq!(owners(1), ["ada", "cy"]); | |
| 256 | assert!(owners(2).is_empty()); | |
| 257 | assert_eq!(listed[1].owners[0].email.as_deref(), Some("ada@example.com")); | |
| 258 | } | |
| 259 | } |
This file's history is long; its oldest lines are credited to the oldest commit read.