samjeston / openhub Public
Sam Jeston Working vertical 9aa3c45 · History
165 lines· 5.8 KB ·Rust
Raw
1mod account;
2pub mod assets;
3mod code;
4pub mod ctx;
5pub mod error;
6pub mod fmt;
7mod git_http;
8mod home;
9mod pulls;
10mod repo;
11
12use askama::Template;
13use axum::Router;
14use axum::extract::DefaultBodyLimit;
15use axum::http::{HeaderValue, StatusCode, header};
16use axum::response::{Html, IntoResponse, Response};
17use axum::routing::{get, post};
18use axum_extra::extract::cookie::Cookie;
19use tower_http::trace::TraceLayer;
20
21use crate::state::AppState;
22pub use ctx::Ctx;
23pub use error::AppError;
24
25pub type WebResult = Result<Response, AppError>;
26
27/// Fetch negotiation requests list every commit the client already has.
28const GIT_BODY_LIMIT: usize = 64 * 1024 * 1024;
29
30pub fn router(state: AppState) -> Router {
31 Router::new()
32 .route("/", get(home::index))
33 .route("/explore", get(home::explore))
34 .route("/login", get(account::login_form).post(account::login))
35 .route(
36 "/register",
37 get(account::register_form).post(account::register),
38 )
39 .route("/logout", post(account::logout))
40 .route("/new", get(repo::new_form).post(repo::create))
41 .route("/settings", get(account::settings_redirect))
42 .route(
43 "/settings/profile",
44 get(account::profile_form).post(account::update_profile),
45 )
46 .route("/settings/password", post(account::change_password))
47 .route("/settings/keys", get(account::keys).post(account::add_key))
48 .route("/settings/keys/{id}/delete", post(account::delete_key))
49 .route("/static/style.css", get(assets::stylesheet))
50 .route("/static/app.js", get(assets::script))
51 .route("/favicon.svg", get(assets::favicon))
52 .route("/{owner}", get(home::profile))
53 .route("/{owner}/{repo}", get(code::repo_home))
54 .route("/{owner}/{repo}/tree/{*rest}", get(code::tree))
55 .route("/{owner}/{repo}/blob/{*rest}", get(code::blob))
56 .route("/{owner}/{repo}/raw/{*rest}", get(code::raw))
57 .route("/{owner}/{repo}/archive/{*file}", get(code::archive))
58 .route("/{owner}/{repo}/commits", get(code::commits_default))
59 .route("/{owner}/{repo}/commits/{*rest}", get(code::commits))
60 .route("/{owner}/{repo}/commit/{sha}", get(code::commit))
61 .route("/{owner}/{repo}/branches", get(repo::branches))
62 .route("/{owner}/{repo}/branches/delete", post(repo::delete_branch))
63 .route("/{owner}/{repo}/tags", get(repo::tags))
64 .route("/{owner}/{repo}/compare", get(pulls::compare_default))
65 .route("/{owner}/{repo}/compare/{*spec}", get(pulls::compare))
66 .route(
67 "/{owner}/{repo}/pulls",
68 get(pulls::list).post(pulls::create),
69 )
70 .route("/{owner}/{repo}/pull/{number}", get(pulls::conversation))
71 .route("/{owner}/{repo}/pull/{number}/commits", get(pulls::commits))
72 .route("/{owner}/{repo}/pull/{number}/files", get(pulls::files))
73 .route(
74 "/{owner}/{repo}/pull/{number}/comment",
75 post(pulls::comment),
76 )
77 .route("/{owner}/{repo}/pull/{number}/merge", post(pulls::merge))
78 .route(
79 "/{owner}/{repo}/pull/{number}/state",
80 post(pulls::set_state),
81 )
82 .route("/{owner}/{repo}/pull/{number}/edit", post(pulls::edit))
83 .route(
84 "/{owner}/{repo}/settings",
85 get(repo::settings).post(repo::update_settings),
86 )
87 .route(
88 "/{owner}/{repo}/settings/collaborators",
89 post(repo::add_collaborator),
90 )
91 .route(
92 "/{owner}/{repo}/settings/collaborators/remove",
93 post(repo::remove_collaborator),
94 )
95 .route("/{owner}/{repo}/settings/delete", post(repo::delete))
96 .route("/{owner}/{repo}/info/refs", get(git_http::info_refs))
97 .route(
98 "/{owner}/{repo}/git-upload-pack",
99 post(git_http::upload_pack).layer(DefaultBodyLimit::max(GIT_BODY_LIMIT)),
100 )
101 .route(
102 "/{owner}/{repo}/git-receive-pack",
103 post(git_http::receive_pack),
104 )
105 .fallback(|| async { AppError::NotFound })
106 .layer(axum::middleware::from_fn_with_state(
107 state.clone(),
108 ctx::middleware,
109 ))
110 .layer(TraceLayer::new_for_http())
111 .with_state(state)
112}
113
114pub async fn serve(state: AppState) -> anyhow::Result<()> {
115 let addr = state.config.http_addr;
116 let listener = tokio::net::TcpListener::bind(addr).await?;
117 tracing::info!("web interface listening on http://{addr}");
118 axum::serve(listener, router(state)).await?;
119 Ok(())
120}
121
122pub fn render<T: Template>(template: &T) -> WebResult {
123 Ok(Html(template.render()?).into_response())
124}
125
126pub fn redirect(to: &str) -> Response {
127 let mut resp = StatusCode::SEE_OTHER.into_response();
128 if let Ok(v) = HeaderValue::from_str(to) {
129 resp.headers_mut().insert(header::LOCATION, v);
130 }
131 resp
132}
133
134pub fn with_cookie(mut resp: Response, cookie: Cookie<'_>) -> Response {
135 if let Ok(v) = HeaderValue::from_str(&cookie.to_string()) {
136 resp.headers_mut().append(header::SET_COOKIE, v);
137 }
138 resp
139}
140
141/// Redirect that shows a one-off message on the next page.
142pub fn redirect_flash(to: &str, kind: &str, message: &str) -> Response {
143 with_cookie(redirect(to), ctx::flash_cookie(kind, message))
144}
145
146/// Runs blocking repository work off the async runtime.
147pub async fn blocking<T, F>(f: F) -> Result<T, AppError>
148where
149 T: Send + 'static,
150 F: FnOnce() -> Result<T, AppError> + Send + 'static,
151{
152 tokio::task::spawn_blocking(f)
153 .await
154 .map_err(|e| AppError::Internal(e.into()))?
155}
156
157/// Returns `next` if it is a safe same-site path, otherwise `/`.
158pub fn safe_next(next: Option<&str>) -> String {
159 match next {
160 Some(n) if n.starts_with('/') && !n.starts_with("//") && !n.starts_with("/\\") => {
161 n.to_string()
162 }
163 _ => "/".to_string(),
164 }
165}