//! The front page, explore, owner pages and small endpoints. use super::{filters, Error, Page, Shell}; use crate::auth::Viewer; use crate::{db, Shared}; use askama::Template; use axum::extract::{Path, State}; use axum::http::header; use axum::response::{IntoResponse, Response}; use hub_api::Visibility; #[derive(Template)] #[template(path = "home.html")] pub struct HomePage { shell: Shell, description: String, repos: Vec, owner: Option, } /// The signed-in dashboard (hub spec §6): the review queue, the /// viewer's own proposals and assigned issues, their repositories and /// recent activity across everything they can see. #[derive(Template)] #[template(path = "dashboard.html")] pub struct Dashboard { shell: Shell, mine: Vec, reviews: Vec, authored: Vec, assigned: Vec, feed: Vec, } /// How many activity lines the dashboard shows. const FEED_LIMIT: i64 = 40; /// `/`: the dashboard when signed in, every visible repository otherwise. pub async fn index(State(app): State, viewer: Viewer) -> Result { let Some(account) = viewer.account().cloned() else { return Ok(explore(State(app), viewer).await?.into_response()); }; let shell = Shell::new(&app, "/", &viewer); let db_err = |e| Error::from_db(shell.clone(), e); let all = db::repos::list(&app.db).await.map_err(db_err)?; let member: Vec = db::accounts::visible_repo_ids(&app.db, account.id).await.map_err(db_err)?; let seen = visible(&app, &shell, &viewer, all).await?; let seen_ids: Vec = seen.iter().map(|r| r.id).collect(); let mut mine: Vec = seen.into_iter().filter(|r| member.contains(&r.id)).collect(); mine.sort_by_key(|r| std::cmp::Reverse(r.updated_ms)); let reviews = db::activity::awaiting_review(&app.db, account.id, &member).await.map_err(db_err)?; let authored = db::activity::authored(&app.db, account.id, &seen_ids).await.map_err(db_err)?; let assigned = db::activity::assigned(&app.db, account.id, &seen_ids).await.map_err(db_err)?; let feed = db::activity::feed(&app.db, &seen_ids, FEED_LIMIT).await.map_err(db_err)?; Ok(Page(Dashboard { shell, mine, reviews, authored, assigned, feed }).into_response()) } /// `/explore`: every repository the viewer may see. pub async fn explore(State(app): State, viewer: Viewer) -> Result, Error> { let shell = Shell::new(&app, "/explore", &viewer); let repos = visible(&app, &shell, &viewer, db::repos::list(&app.db).await.map_err(|e| Error::from_db(shell.clone(), e))?).await?; Ok(Page(HomePage { description: app.config.site.description.clone(), shell, repos, owner: None })) } pub async fn owner(State(app): State, viewer: Viewer, Path(owner): Path) -> Result, Error> { let shell = Shell::new(&app, &format!("/{owner}"), &viewer); if !crate::repos::names::valid_owner(&owner) { return Err(Error::not_found(shell)); } let repos = visible(&app, &shell, &viewer, db::repos::list_by_owner(&app.db, &owner).await.map_err(|e| Error::from_db(shell.clone(), e))?) .await?; let known = db::accounts::account_by_name(&app.db, &owner).await.map_err(|e| Error::from_db(shell.clone(), e))?.is_some() || db::accounts::org_by_name(&app.db, &owner).await.map_err(|e| Error::from_db(shell.clone(), e))?.is_some(); if repos.is_empty() && !known { return Err(Error::not_found(shell)); } Ok(Page(HomePage { description: String::new(), shell, repos, owner: Some(owner) })) } /// The repositories among `rows` the viewer may see: public ones, internal /// ones when signed in, and those the viewer owns or collaborates on. async fn visible(app: &Shared, shell: &Shell, viewer: &Viewer, rows: Vec) -> Result, Error> { let mine: Vec = match viewer.account() { Some(a) if a.is_admin => rows.iter().map(|r| r.id).collect(), Some(a) => db::accounts::visible_repo_ids(&app.db, a.id).await.map_err(|e| Error::from_db(shell.clone(), e))?, None => vec![], }; let signed_in = viewer.signed_in(); Ok(rows .into_iter() .filter(|r| match r.visibility() { Visibility::Public => true, Visibility::Internal => signed_in, Visibility::Private => mine.contains(&r.id), }) .collect()) } pub async fn health(State(app): State) -> Response { let body = serde_json::json!({ "ok": true, "version": env!("CARGO_PKG_VERSION"), "uptime_s": app.started.elapsed().as_secs(), }); axum::Json(body).into_response() } /// The hub delegate identity object (hub spec §15.5), as `oxy identity /// export` writes one, so policy holders can admit it. pub async fn hub_key(State(app): State) -> Response { ( [(header::CONTENT_TYPE, "application/oxy-identity"), (header::CONTENT_DISPOSITION, "attachment; filename=\"hub.identity\"")], app.hub_key.bytes.clone(), ) .into_response() }