Jump to…
snowinitial commitqoxwzsukwmkx1mo
Matt W1//! `GET /{owner}` — a user or organization profile (spec §7).
Matt W2//!
Matt W3//! Users and organizations share one handle namespace (enforced by a trigger in
Matt W4//! the schema), so one route serves both and the lookup tries users first.
Matt W5//!
Matt W6//! The repository list is filtered by the same rule the repository pages use:
Matt W7//! public repos to everyone, private repos only where the viewer has a
Matt W8//! collaborator row, an org membership, ownership, or site admin. Getting this
Matt W9//! query wrong is the private-repo leak in its most direct form — a profile page
Matt W10//! that enumerates repositories the viewer cannot open.
Matt W11
Matt W12use axum::extract::{Path as UrlPath, State};
Matt W13use axum::response::{IntoResponse, Response};
Matt W14use serde::Deserialize;
Matt W15use uuid::Uuid;
Matt W16
Matt W17use crate::error::{AppError, AppResult};
Matt W18use crate::state::{AppState, CsrfToken, CurrentUser, Nonce};
Matt W19use crate::views::settings as v;
Matt W20use crate::views::{self, Chrome};
Matt W21
Matt W22/// `GET /{owner}`
Matt W23pub async fn show(
Matt W24 State(state): State<AppState>,
Matt W25 UrlPath(handle): UrlPath<String>,
Matt W26 CurrentUser(user): CurrentUser,
Matt W27 CsrfToken(csrf): CsrfToken,
Matt W28 Nonce(nonce): Nonce,
Matt W29) -> AppResult<Response> {
Matt W30 let viewer = user.as_ref().map(|u| u.id);
Matt W31 let is_site_admin = user.as_ref().is_some_and(|u| u.is_admin);
Matt W32
Matt W33 let owner = load_owner(&state, &handle).await?.ok_or(AppError::NotFound)?;
Matt W34
Matt W35 let repos = load_repos(&state, &owner, viewer, is_site_admin).await?;
Matt W36
Matt W37 let members = if owner.is_org {
Matt W38 load_members(&state, owner.id).await?
Matt W39 } else {
Matt W40 Vec::new()
Matt W41 };
Matt W42
Matt W43 Ok(views::page(
Matt W44 Chrome {
Matt W45 title: &owner.handle,
Matt W46 user: user.as_deref(),
Matt W47 csrf: &csrf,
Matt W48 nonce: &nonce,
Matt W49 },
Matt W50 v::profile(v::Profile {
Matt W51 handle: &owner.handle,
Matt W52 display_name: owner.display_name.as_deref(),
Matt W53 description: owner.description.as_deref(),
Matt W54 is_org: owner.is_org,
Matt W55 repos: &repos,
Matt W56 members: &members,
Matt W57 joined: owner.created_at,
Matt W58 }),
Matt W59 )
Matt W60 .into_response())
Matt W61}
Matt W62
Matt W63struct Owner {
Matt W64 id: Uuid,
Matt W65 handle: String,
Matt W66 display_name: Option<String>,
Matt W67 description: Option<String>,
Matt W68 is_org: bool,
Matt W69 created_at: chrono::DateTime<chrono::Utc>,
Matt W70}
Matt W71
Matt W72#[derive(sqlx::FromRow)]
Matt W73struct OwnerRow {
Matt W74 id: Uuid,
Matt W75 handle: String,
Matt W76 display_name: Option<String>,
Matt W77 description: Option<String>,
Matt W78 is_org: bool,
Matt W79 created_at: chrono::DateTime<chrono::Utc>,
Matt W80}
Matt W81
Matt W82async fn load_owner(state: &AppState, handle: &str) -> AppResult<Option<Owner>> {
Matt W83 // One statement over both tables. The handle namespace is shared, so at most
Matt W84 // one row can match — and doing it in one query means a user and an org can
Matt W85 // never both be found and silently resolved in some arbitrary order.
Matt W86 let row: Option<OwnerRow> = sqlx::query_as(
Matt W87 r#"
Matt W88 SELECT id, handle::text AS handle, display_name, NULL::text AS description,
Matt W89 false AS is_org, created_at
Matt W90 FROM users WHERE handle = $1
Matt W91 UNION ALL
Matt W92 SELECT id, handle::text AS handle, display_name, description,
Matt W93 true AS is_org, created_at
Matt W94 FROM orgs WHERE handle = $1
Matt W95 "#,
Matt W96 )
Matt W97 .bind(handle)
Matt W98 .fetch_optional(&state.db)
Matt W99 .await?;
Matt W100
Matt W101 Ok(row.map(|r| Owner {
Matt W102 id: r.id,
Matt W103 handle: r.handle,
Matt W104 display_name: r.display_name,
Matt W105 description: r.description,
Matt W106 is_org: r.is_org,
Matt W107 created_at: r.created_at,
Matt W108 }))
Matt W109}
Matt W110
Matt W111#[derive(Deserialize, sqlx::FromRow)]
Matt W112struct RepoRow {
Matt W113 name: String,
Matt W114 description: Option<String>,
Matt W115 private: bool,
Matt W116 pushed_at: Option<chrono::DateTime<chrono::Utc>>,
Matt W117}
Matt W118
Matt W119async fn load_repos(
Matt W120 state: &AppState,
Matt W121 owner: &Owner,
Matt W122 viewer: Option<Uuid>,
Matt W123 is_site_admin: bool,
Matt W124) -> AppResult<Vec<v::ProfileRepo>> {
Matt W125 // The visibility predicate is written out here rather than filtered in Rust
Matt W126 // so a private repository is never loaded in the first place. Every branch
Matt W127 // after the first requires `$2` (the viewer) to be non-null, so an anonymous
Matt W128 // request can only ever match the public branch.
Matt W129 let rows: Vec<RepoRow> = sqlx::query_as(
Matt W130 r#"
Matt W131 SELECT r.name::text AS name,
Matt W132 r.description,
Matt W133 (r.visibility = 'private') AS private,
Matt W134 r.pushed_at
Matt W135 FROM repos r
Matt W136 WHERE (r.owner_user_id = $1 OR r.owner_org_id = $1)
Matt W137 AND (
Matt W138 r.visibility = 'public'
Matt W139 OR $3
Matt W140 OR r.owner_user_id = $2
Matt W141 OR EXISTS (SELECT 1 FROM repo_collaborators c
Matt W142 WHERE c.repo_id = r.id AND c.user_id = $2)
Matt W143 OR EXISTS (SELECT 1 FROM org_members m
Matt W144 WHERE m.org_id = r.owner_org_id AND m.user_id = $2)
Matt W145 )
Matt W146 ORDER BY r.pushed_at DESC NULLS LAST, r.name
Matt W147 LIMIT 200
Matt W148 "#,
Matt W149 )
Matt W150 .bind(owner.id)
Matt W151 .bind(viewer)
Matt W152 .bind(is_site_admin)
Matt W153 .fetch_all(&state.db)
Matt W154 .await?;
Matt W155
Matt W156 Ok(rows
Matt W157 .into_iter()
Matt W158 .map(|r| v::ProfileRepo {
Matt W159 name: r.name,
Matt W160 description: r.description,
Matt W161 private: r.private,
Matt W162 pushed_at: r.pushed_at,
Matt W163 })
Matt W164 .collect())
Matt W165}
Matt W166
Matt W167async fn load_members(
Matt W168 state: &AppState,
Matt W169 org_id: Uuid,
Matt W170) -> AppResult<Vec<(String, df_db::models::OrgRole)>> {
Matt W171 Ok(sqlx::query_as(
Matt W172 "SELECT u.handle::text, m.role
Matt W173 FROM org_members m JOIN users u ON u.id = m.user_id
Matt W174 WHERE m.org_id = $1
Matt W175 ORDER BY m.role DESC, u.handle",
Matt W176 )
Matt W177 .bind(org_id)
Matt W178 .fetch_all(&state.db)
Matt W179 .await?)
Matt W180}

180 lines · Rust