//! Bookmarks, tags, conflicts, trust and pushes pages. use super::settings::{csrf, require}; use super::{filters, lookup, lookup_role, Error, Page, RepoShell, Shell}; use crate::auth::{Access, Role, Viewer}; use crate::repos::view; use crate::{db, Shared}; use askama::Template; use axum::extract::{Form, Path, State}; use serde::Deserialize; #[derive(Template)] #[template(path = "bookmarks.html")] pub struct BookmarksPage { shell: Shell, repo: RepoShell, bookmarks: Vec, /// Whether the viewer may delete bookmarks (write access). can_delete: bool, /// Whether the viewer may run the pipeline of a bookmark. can_run: bool, notice: Option, error: Option, } async fn bookmarks_page( app: &Shared, shell: Shell, repo: crate::repos::RepoName, row: db::repos::RepoRow, role: Role, notice: Option, error: Option, ) -> Result, Error> { let default = row.default_bookmark.clone(); let bookmarks = app.repos.read(&repo, move |r, cache| view::bookmarks(r, cache, &default)).await.map_err(|e| Error::from_repo(shell.clone(), e))?; let can_delete = shell.user.is_some() && role >= Role::Write; let can_run = super::flow::why_not_run(crate::flow::enabled(app, &row), shell.user.is_some(), role).is_none(); Ok(Page(BookmarksPage { shell, repo: RepoShell::new(app, &row, "bookmarks").with_role(role), bookmarks, can_delete, can_run, notice, error, })) } pub async fn bookmarks( State(app): State, viewer: Viewer, Path((owner, name)): Path<(String, String)>, ) -> Result, Error> { let shell = Shell::new(&app, &format!("/{owner}/{name}/bookmarks"), &viewer); let (repo, row, role) = lookup(&app, &shell, &viewer, &owner, &name).await?; bookmarks_page(&app, shell, repo, row, role, None, None).await } #[derive(Deserialize)] pub struct DeleteBookmarkForm { #[serde(rename = "_csrf")] csrf_token: String, bookmark: String, } /// Delete a bookmark from the web (hub spec ยง6.4): what `oxy push --delete` /// does, through the same checks. Never the default bookmark; refused under /// a trust policy, which needs a signed attestation only a client can make. pub async fn delete_bookmark( State(app): State, viewer: Viewer, Path((owner, name)): Path<(String, String)>, Form(f): Form, ) -> Result, Error> { let shell = Shell::new(&app, &format!("/{owner}/{name}/bookmarks"), &viewer); let me = require(&viewer, &shell)?; let (repo, row, role) = lookup_role(&app, &shell, &viewer, &owner, &name, Access::Write).await?; csrf(&viewer, &f.csrf_token, &shell)?; let bookmark = f.bookmark.trim().to_string(); if bookmark == row.default_bookmark { let msg = format!("{bookmark} is the default bookmark; choose another default in Settings first."); return bookmarks_page(&app, shell, repo, row, role, None, Some(msg)).await; } let target = bookmark.clone(); let outcome = app .repos .write(&repo, move |r, _| { let Some(state) = r.view()?.bookmarks.get(&target).copied() else { return Ok(Err(format!("There is no bookmark {target}."))); }; let update = oxy_core::protocol::RefUpdate { name: target.clone(), old_state: state, new_state: oxy_core::hash::ObjectRef::ZERO, force: true, attestation: None, }; let report = r.receive_push_report(Default::default(), vec![update], None, vec![])?; Ok(match report.results.first() { Some(res) if res.ok => Ok(state.to_hex()), Some(res) => Err(res.reason.clone()), None => Err("nothing happened".into()), }) }) .await .map_err(|e| Error::from_repo(shell.clone(), e))?; let old_state = match outcome { Ok(s) => s, Err(msg) => return bookmarks_page(&app, shell, repo, row, role, None, Some(format!("Could not delete {bookmark}: {msg}"))).await, }; let result = db::repos::PushOutcome { reference: bookmark.clone(), ok: true, reason: "deleted on the web".into(), old_state: old_state.clone(), new_state: oxy_core::hash::ObjectRef::ZERO.to_hex(), heads: 0, changes: 0, }; db::repos::record_push(&app.db, row.id, Some((me.account.id, &me.account.username)), &[result]).await.ok(); crate::hooks::emit( &app, &row, "push", &me.account.username, serde_json::json!({ "updates": [{ "reference": bookmark, "old_state": old_state, "new_state": oxy_core::hash::ObjectRef::ZERO.to_hex(), "heads": 0, "changes": 0, "deleted": true }], "via": "web" }), ) .await; db::accounts::audit(&app.db, Some(&me.account), "bookmark.deleted", &format!("{owner}/{name}"), &bookmark).await.ok(); bookmarks_page( &app, shell, repo, row, role, Some(format!("Deleted {bookmark}. Its changes stay while anything else reaches them.")), None, ) .await } #[derive(Template)] #[template(path = "tags.html")] pub struct TagsPage { shell: Shell, repo: RepoShell, tags: Vec, } pub async fn tags(State(app): State, viewer: Viewer, Path((owner, name)): Path<(String, String)>) -> Result, Error> { let shell = Shell::new(&app, &format!("/{owner}/{name}/tags"), &viewer); let (repo, row, role) = lookup(&app, &shell, &viewer, &owner, &name).await?; let tags = app.repos.read(&repo, |r, _| view::tags(r)).await.map_err(|e| Error::from_repo(shell.clone(), e))?; Ok(Page(TagsPage { shell, repo: RepoShell::new(&app, &row, "tags").with_role(role), tags })) } #[derive(Template)] #[template(path = "conflicts.html")] pub struct ConflictsPage { shell: Shell, repo: RepoShell, conflicts: Vec, } pub async fn conflicts( State(app): State, viewer: Viewer, Path((owner, name)): Path<(String, String)>, ) -> Result, Error> { let shell = Shell::new(&app, &format!("/{owner}/{name}/conflicts"), &viewer); let (repo, row, role) = lookup(&app, &shell, &viewer, &owner, &name).await?; let conflicts = app.repos.read(&repo, view::conflicts).await.map_err(|e| Error::from_repo(shell.clone(), e))?; Ok(Page(ConflictsPage { shell, repo: RepoShell::new(&app, &row, "conflicts").with_role(role), conflicts })) } #[derive(Template)] #[template(path = "trust.html")] pub struct TrustPage { shell: Shell, repo: RepoShell, t: view::Trust, } pub async fn trust( State(app): State, viewer: Viewer, Path((owner, name)): Path<(String, String)>, ) -> Result, Error> { let shell = Shell::new(&app, &format!("/{owner}/{name}/trust"), &viewer); let (repo, row, role) = lookup(&app, &shell, &viewer, &owner, &name).await?; let key = app.hub_key.key; let t = app.repos.read(&repo, move |r, cache| view::trust(r, cache, &key)).await.map_err(|e| Error::from_repo(shell.clone(), e))?; Ok(Page(TrustPage { shell, repo: RepoShell::new(&app, &row, "trust").with_role(role), t })) } #[derive(Template)] #[template(path = "pushes.html")] pub struct PushesPage { shell: Shell, repo: RepoShell, pushes: Vec, } pub async fn pushes( State(app): State, viewer: Viewer, Path((owner, name)): Path<(String, String)>, ) -> Result, Error> { let shell = Shell::new(&app, &format!("/{owner}/{name}/pushes"), &viewer); let (_, row, role) = lookup(&app, &shell, &viewer, &owner, &name).await?; let pushes = db::repos::pushes(&app.db, row.id, 200).await.map_err(|e| Error::from_db(shell.clone(), e))?; Ok(Page(PushesPage { shell, repo: RepoShell::new(&app, &row, "pushes").with_role(role), pushes })) }