OXYGENHUB
# df68c58de947 oxygenhub / crates / hub / src / web / refs.rs
7.7 KiB · Rust · last changed Delete bookmarks: a button for writers, pushed deletions checked 1 month ago source blame history raw
1
//! Bookmarks, tags, conflicts, trust and pushes pages.
2
3
use super::settings::{csrf, require};
4
use super::{filters, lookup, lookup_role, Error, Page, RepoShell, Shell};
5
use crate::auth::{Access, Role, Viewer};
6
use crate::repos::view;
7
use crate::{db, Shared};
8
use askama::Template;
9
use axum::extract::{Form, Path, State};
10
use serde::Deserialize;
11
12
#[derive(Template)]
13
#[template(path = "bookmarks.html")]
14
pub struct BookmarksPage {
15
    shell: Shell,
16
    repo: RepoShell,
17
    bookmarks: Vec<view::BookmarkView>,
18
    /// Whether the viewer may delete bookmarks (write access).
19
    can_delete: bool,
20
    notice: Option<String>,
21
    error: Option<String>,
22
}
23
24
async fn bookmarks_page(
25
    app: &Shared,
26
    shell: Shell,
27
    repo: crate::repos::RepoName,
28
    row: db::repos::RepoRow,
29
    role: Role,
30
    notice: Option<String>,
31
    error: Option<String>,
32
) -> Result<Page<BookmarksPage>, Error> {
33
    let default = row.default_bookmark.clone();
34
    let bookmarks =
35
        app.repos.read(&repo, move |r, cache| view::bookmarks(r, cache, &default)).await.map_err(|e| Error::from_repo(shell.clone(), e))?;
36
    let can_delete = shell.user.is_some() && role >= Role::Write;
37
    Ok(Page(BookmarksPage { shell, repo: RepoShell::new(app, &row, "bookmarks").with_role(role), bookmarks, can_delete, notice, error }))
38
}
39
40
pub async fn bookmarks(
41
    State(app): State<Shared>,
42
    viewer: Viewer,
43
    Path((owner, name)): Path<(String, String)>,
44
) -> Result<Page<BookmarksPage>, Error> {
45
    let shell = Shell::new(&app, &format!("/{owner}/{name}/bookmarks"), &viewer);
46
    let (repo, row, role) = lookup(&app, &shell, &viewer, &owner, &name).await?;
47
    bookmarks_page(&app, shell, repo, row, role, None, None).await
48
}
49
50
#[derive(Deserialize)]
51
pub struct DeleteBookmarkForm {
52
    #[serde(rename = "_csrf")]
53
    csrf_token: String,
54
    bookmark: String,
55
}
56
57
/// Delete a bookmark from the web (hub spec §6.4): what `oxy push --delete`
58
/// does, through the same checks. Never the default bookmark; refused under
59
/// a trust policy, which needs a signed attestation only a client can make.
60
pub async fn delete_bookmark(
61
    State(app): State<Shared>,
62
    viewer: Viewer,
63
    Path((owner, name)): Path<(String, String)>,
64
    Form(f): Form<DeleteBookmarkForm>,
65
) -> Result<Page<BookmarksPage>, Error> {
66
    let shell = Shell::new(&app, &format!("/{owner}/{name}/bookmarks"), &viewer);
67
    let me = require(&viewer, &shell)?;
68
    let (repo, row, role) = lookup_role(&app, &shell, &viewer, &owner, &name, Access::Write).await?;
69
    csrf(&viewer, &f.csrf_token, &shell)?;
70
    let bookmark = f.bookmark.trim().to_string();
71
    if bookmark == row.default_bookmark {
72
        let msg = format!("{bookmark} is the default bookmark; choose another default in Settings first.");
73
        return bookmarks_page(&app, shell, repo, row, role, None, Some(msg)).await;
74
    }
75
    let target = bookmark.clone();
76
    let outcome = app
77
        .repos
78
        .write(&repo, move |r, _| {
79
            let Some(state) = r.view()?.bookmarks.get(&target).copied() else {
80
                return Ok(Err(format!("There is no bookmark {target}.")));
81
            };
82
            let update = oxy_core::protocol::RefUpdate {
83
                name: target.clone(),
84
                old_state: state,
85
                new_state: oxy_core::hash::ObjectRef::ZERO,
86
                force: true,
87
                attestation: None,
88
            };
89
            let report = r.receive_push_report(Default::default(), vec![update], None, vec![])?;
90
            Ok(match report.results.first() {
91
                Some(res) if res.ok => Ok(state.to_hex()),
92
                Some(res) => Err(res.reason.clone()),
93
                None => Err("nothing happened".into()),
94
            })
95
        })
96
        .await
97
        .map_err(|e| Error::from_repo(shell.clone(), e))?;
98
    let old_state = match outcome {
99
        Ok(s) => s,
100
        Err(msg) => return bookmarks_page(&app, shell, repo, row, role, None, Some(format!("Could not delete {bookmark}: {msg}"))).await,
101
    };
102
    let result = db::repos::PushOutcome {
103
        reference: bookmark.clone(),
104
        ok: true,
105
        reason: "deleted on the web".into(),
106
        old_state: old_state.clone(),
107
        new_state: oxy_core::hash::ObjectRef::ZERO.to_hex(),
108
        heads: 0,
109
        changes: 0,
110
    };
111
    db::repos::record_push(&app.db, row.id, Some((me.account.id, &me.account.username)), &[result]).await.ok();
112
    crate::hooks::emit(
113
        &app,
114
        &row,
115
        "push",
116
        &me.account.username,
117
        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" }),
118
    )
119
    .await;
120
    db::accounts::audit(&app.db, Some(&me.account), "bookmark.deleted", &format!("{owner}/{name}"), &bookmark).await.ok();
121
    bookmarks_page(
122
        &app,
123
        shell,
124
        repo,
125
        row,
126
        role,
127
        Some(format!("Deleted {bookmark}. Its changes stay while anything else reaches them.")),
128
        None,
129
    )
130
    .await
131
}
132
133
#[derive(Template)]
134
#[template(path = "tags.html")]
135
pub struct TagsPage {
136
    shell: Shell,
137
    repo: RepoShell,
138
    tags: Vec<view::TagView>,
139
}
140
141
pub async fn tags(State(app): State<Shared>, viewer: Viewer, Path((owner, name)): Path<(String, String)>) -> Result<Page<TagsPage>, Error> {
142
    let shell = Shell::new(&app, &format!("/{owner}/{name}/tags"), &viewer);
143
    let (repo, row, role) = lookup(&app, &shell, &viewer, &owner, &name).await?;
144
    let tags = app.repos.read(&repo, |r, _| view::tags(r)).await.map_err(|e| Error::from_repo(shell.clone(), e))?;
145
    Ok(Page(TagsPage { shell, repo: RepoShell::new(&app, &row, "tags").with_role(role), tags }))
146
}
147
148
#[derive(Template)]
149
#[template(path = "conflicts.html")]
150
pub struct ConflictsPage {
151
    shell: Shell,
152
    repo: RepoShell,
153
    conflicts: Vec<view::ConflictView>,
154
}
155
156
pub async fn conflicts(
157
    State(app): State<Shared>,
158
    viewer: Viewer,
159
    Path((owner, name)): Path<(String, String)>,
160
) -> Result<Page<ConflictsPage>, Error> {
161
    let shell = Shell::new(&app, &format!("/{owner}/{name}/conflicts"), &viewer);
162
    let (repo, row, role) = lookup(&app, &shell, &viewer, &owner, &name).await?;
163
    let conflicts = app.repos.read(&repo, view::conflicts).await.map_err(|e| Error::from_repo(shell.clone(), e))?;
164
    Ok(Page(ConflictsPage { shell, repo: RepoShell::new(&app, &row, "conflicts").with_role(role), conflicts }))
165
}
166
167
#[derive(Template)]
168
#[template(path = "trust.html")]
169
pub struct TrustPage {
170
    shell: Shell,
171
    repo: RepoShell,
172
    t: view::Trust,
173
}
174
175
pub async fn trust(
176
    State(app): State<Shared>,
177
    viewer: Viewer,
178
    Path((owner, name)): Path<(String, String)>,
179
) -> Result<Page<TrustPage>, Error> {
180
    let shell = Shell::new(&app, &format!("/{owner}/{name}/trust"), &viewer);
181
    let (repo, row, role) = lookup(&app, &shell, &viewer, &owner, &name).await?;
182
    let key = app.hub_key.key;
183
    let t = app.repos.read(&repo, move |r, cache| view::trust(r, cache, &key)).await.map_err(|e| Error::from_repo(shell.clone(), e))?;
184
    Ok(Page(TrustPage { shell, repo: RepoShell::new(&app, &row, "trust").with_role(role), t }))
185
}
186
187
#[derive(Template)]
188
#[template(path = "pushes.html")]
189
pub struct PushesPage {
190
    shell: Shell,
191
    repo: RepoShell,
192
    pushes: Vec<db::repos::PushRow>,
193
}
194
195
pub async fn pushes(
196
    State(app): State<Shared>,
197
    viewer: Viewer,
198
    Path((owner, name)): Path<(String, String)>,
199
) -> Result<Page<PushesPage>, Error> {
200
    let shell = Shell::new(&app, &format!("/{owner}/{name}/pushes"), &viewer);
201
    let (_, row, role) = lookup(&app, &shell, &viewer, &owner, &name).await?;
202
    let pushes = db::repos::pushes(&app.db, row.id, 200).await.map_err(|e| Error::from_db(shell.clone(), e))?;
203
    Ok(Page(PushesPage { shell, repo: RepoShell::new(&app, &row, "pushes").with_role(role), pushes }))
204
}