basic support for user authentication (currently does nothing)
This commit is contained in:
parent
812eee1a5f
commit
56064c6259
9 changed files with 279 additions and 8 deletions
138
Cargo.lock
generated
138
Cargo.lock
generated
|
@ -241,6 +241,17 @@ version = "1.0.95"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "34ac096ce696dc2fcabef30516bb13c0a68a11d30131d3df6f04711467681b04"
|
||||
|
||||
[[package]]
|
||||
name = "async-trait"
|
||||
version = "0.1.86"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "atomic-waker"
|
||||
version = "1.1.2"
|
||||
|
@ -634,6 +645,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8"
|
||||
dependencies = [
|
||||
"axum-core",
|
||||
"axum-macros",
|
||||
"bytes",
|
||||
"form_urlencoded",
|
||||
"futures-util",
|
||||
|
@ -681,6 +693,17 @@ dependencies = [
|
|||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "axum-macros"
|
||||
version = "0.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "604fde5e028fea851ce1d8570bbdc034bec850d157f7569d10f347d06808c05c"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "backtrace"
|
||||
version = "0.3.74"
|
||||
|
@ -869,6 +892,17 @@ version = "0.4.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
|
||||
|
||||
[[package]]
|
||||
name = "cookie"
|
||||
version = "0.18.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
|
||||
dependencies = [
|
||||
"percent-encoding",
|
||||
"time",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "core-foundation"
|
||||
version = "0.9.4"
|
||||
|
@ -1191,6 +1225,7 @@ dependencies = [
|
|||
"serde_json",
|
||||
"tera",
|
||||
"tokio",
|
||||
"tower-sessions",
|
||||
"unicode-segmentation",
|
||||
"url",
|
||||
]
|
||||
|
@ -1204,6 +1239,20 @@ dependencies = [
|
|||
"percent-encoding",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
|
||||
dependencies = [
|
||||
"futures-channel",
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.31"
|
||||
|
@ -1211,6 +1260,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-sink",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1219,6 +1269,23 @@ version = "0.3.31"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
|
||||
|
||||
[[package]]
|
||||
name = "futures-io"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
|
||||
|
||||
[[package]]
|
||||
name = "futures-macro"
|
||||
version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.31"
|
||||
|
@ -1238,9 +1305,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"pin-project-lite",
|
||||
"pin-utils",
|
||||
"slab",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -1910,6 +1980,7 @@ checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17"
|
|||
dependencies = [
|
||||
"autocfg",
|
||||
"scopeguard",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -3189,6 +3260,22 @@ dependencies = [
|
|||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-cookies"
|
||||
version = "0.11.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "151b5a3e3c45df17466454bb74e9ecedecc955269bdedbf4d150dfa393b55a36"
|
||||
dependencies = [
|
||||
"axum-core",
|
||||
"cookie",
|
||||
"futures-util",
|
||||
"http 1.2.0",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-layer"
|
||||
version = "0.3.3"
|
||||
|
@ -3201,6 +3288,57 @@ version = "0.3.3"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
|
||||
|
||||
[[package]]
|
||||
name = "tower-sessions"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43a05911f23e8fae446005fe9b7b97e66d95b6db589dc1c4d59f6a2d4d4927d3"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"http 1.2.0",
|
||||
"time",
|
||||
"tokio",
|
||||
"tower-cookies",
|
||||
"tower-layer",
|
||||
"tower-service",
|
||||
"tower-sessions-core",
|
||||
"tower-sessions-memory-store",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-sessions-core"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ce8cce604865576b7751b7a6bc3058f754569a60d689328bb74c52b1d87e355b"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum-core",
|
||||
"base64 0.22.1",
|
||||
"futures",
|
||||
"http 1.2.0",
|
||||
"parking_lot",
|
||||
"rand",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"thiserror 2.0.9",
|
||||
"time",
|
||||
"tokio",
|
||||
"tracing",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tower-sessions-memory-store"
|
||||
version = "0.14.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fb05909f2e1420135a831dd5df9f5596d69196d0a64c3499ca474c4bd3d33242"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"time",
|
||||
"tokio",
|
||||
"tower-sessions-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "tracing"
|
||||
version = "0.1.41"
|
||||
|
|
|
@ -14,7 +14,7 @@ url = "2"
|
|||
anyhow = "1"
|
||||
serde_json = "1"
|
||||
rand = "0.8"
|
||||
axum = { version = "0.8", features = ["form"] }
|
||||
axum = { version = "0.8", features = ["form", "macros"] }
|
||||
tera = "1"
|
||||
lazy_static = "1"
|
||||
actix-files = "0.6"
|
||||
|
@ -24,6 +24,7 @@ include_dir = "0.7"
|
|||
aws-config = { version = "1.1.7", features = ["behavior-version-latest"] }
|
||||
aws-sdk-s3 = "1.66.0"
|
||||
new_mime_guess = "4"
|
||||
tower-sessions = "0.14"
|
||||
|
||||
[profile.profiling]
|
||||
inherits = "dev"
|
||||
|
|
|
@ -60,3 +60,4 @@ Environment variables read when `STORAGE_BACKEND=s3`:
|
|||
- auth: add support for connecting to the forge using oauth?
|
||||
- improve error handling? currently the app will panic if writing to the storage
|
||||
backend fails. Can we do better?
|
||||
- error reporting when the auth token is not working/outdated and background jobs fail
|
||||
|
|
File diff suppressed because one or more lines are too long
59
src/main.rs
59
src/main.rs
|
@ -3,7 +3,7 @@ use axum::{
|
|||
extract::{OriginalUri, Path, Query, State},
|
||||
http::{header, StatusCode},
|
||||
response::{Html, IntoResponse},
|
||||
routing::get,
|
||||
routing::{get, post},
|
||||
Form, Router,
|
||||
};
|
||||
use forgejo_api::{Auth, Forgejo};
|
||||
|
@ -14,6 +14,7 @@ use std::collections::HashMap;
|
|||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use tera::Tera;
|
||||
use tower_sessions::{MemoryStore, Session, SessionManagerLayer};
|
||||
use url::Url;
|
||||
|
||||
mod classifier;
|
||||
|
@ -21,6 +22,7 @@ mod data;
|
|||
mod db;
|
||||
mod email;
|
||||
mod scrape;
|
||||
mod session;
|
||||
mod storage;
|
||||
mod userdb;
|
||||
mod util;
|
||||
|
@ -29,6 +31,7 @@ mod workers;
|
|||
use data::*;
|
||||
use db::Db;
|
||||
use email::SmtpConfig;
|
||||
use session::AuthenticatedUser;
|
||||
use storage::Storage;
|
||||
use userdb::{IsSpam, UserDb};
|
||||
use util::env_var;
|
||||
|
@ -280,9 +283,11 @@ fn approx_score(score: f32) -> ApproxScore {
|
|||
}
|
||||
}
|
||||
|
||||
#[axum::debug_handler]
|
||||
async fn get_index(
|
||||
State(data): State<Arc<AppState>>,
|
||||
Query(q): Query<SortSetting>,
|
||||
_user: AuthenticatedUser,
|
||||
OriginalUri(uri): OriginalUri,
|
||||
) -> Result<Html<String>, AppError> {
|
||||
eprintln!("GET {}", uri);
|
||||
|
@ -347,8 +352,9 @@ async fn get_index(
|
|||
|
||||
async fn post_classified(
|
||||
State(data): State<Arc<AppState>>,
|
||||
Form(form): Form<HashMap<i64, String>>,
|
||||
_user: AuthenticatedUser,
|
||||
OriginalUri(uri): OriginalUri,
|
||||
Form(form): Form<HashMap<i64, String>>,
|
||||
overwrite: bool,
|
||||
) -> Result<impl IntoResponse, AppError> {
|
||||
eprintln!("POST {}", uri);
|
||||
|
@ -381,22 +387,25 @@ async fn post_classified(
|
|||
|
||||
async fn post_classified_index(
|
||||
data: State<Arc<AppState>>,
|
||||
user: AuthenticatedUser,
|
||||
uri: OriginalUri,
|
||||
form: Form<HashMap<i64, String>>,
|
||||
) -> impl IntoResponse {
|
||||
post_classified(data, form, uri, false).await
|
||||
post_classified(data, user, uri, form, false).await
|
||||
}
|
||||
|
||||
async fn post_classified_edit(
|
||||
data: State<Arc<AppState>>,
|
||||
user: AuthenticatedUser,
|
||||
uri: OriginalUri,
|
||||
form: Form<HashMap<i64, String>>,
|
||||
) -> impl IntoResponse {
|
||||
post_classified(data, form, uri, true).await
|
||||
post_classified(data, user, uri, form, true).await
|
||||
}
|
||||
|
||||
async fn get_classified(
|
||||
State(data): State<Arc<AppState>>,
|
||||
_user: AuthenticatedUser,
|
||||
OriginalUri(uri): OriginalUri,
|
||||
) -> Result<Html<String>, AppError> {
|
||||
eprintln!("GET {}", uri);
|
||||
|
@ -442,6 +451,41 @@ async fn get_static_(
|
|||
}
|
||||
}
|
||||
|
||||
async fn get_auth(
|
||||
State(data): State<Arc<AppState>>,
|
||||
OriginalUri(uri): OriginalUri,
|
||||
) -> Result<Html<String>, AppError> {
|
||||
eprintln!("GET {}", uri);
|
||||
let mut context = tera::Context::new();
|
||||
context.insert("forge_url", &data.config.forge_url.to_string());
|
||||
let page = TEMPLATES.render("auth.html", &context)?;
|
||||
Ok(Html::from(page))
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct AuthForm {
|
||||
token: String,
|
||||
}
|
||||
|
||||
async fn post_auth(
|
||||
OriginalUri(uri): OriginalUri,
|
||||
session: Session,
|
||||
Form(form): Form<AuthForm>,
|
||||
) -> impl IntoResponse {
|
||||
eprintln!("POST {}", uri);
|
||||
session
|
||||
.insert(session::TOKEN_KEY, &form.token)
|
||||
.await
|
||||
.unwrap();
|
||||
(StatusCode::SEE_OTHER, [(header::LOCATION, "/")])
|
||||
}
|
||||
|
||||
async fn post_logout(OriginalUri(uri): OriginalUri, session: Session) -> impl IntoResponse {
|
||||
eprintln!("POST {}", uri);
|
||||
session.flush().await.unwrap();
|
||||
(StatusCode::SEE_OTHER, [(header::LOCATION, "/auth")])
|
||||
}
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> anyhow::Result<()> {
|
||||
let config = Arc::new(Config::from_env().await?);
|
||||
|
@ -491,6 +535,8 @@ async fn main() -> anyhow::Result<()> {
|
|||
|
||||
println!("Listening on http://{}", &config.bind_addr);
|
||||
|
||||
let session_store = MemoryStore::default();
|
||||
let session_layer = SessionManagerLayer::new(session_store);
|
||||
let app = Router::new()
|
||||
.route("/", get(get_index).post(post_classified_index))
|
||||
.route(
|
||||
|
@ -498,7 +544,10 @@ async fn main() -> anyhow::Result<()> {
|
|||
get(get_classified).post(post_classified_edit),
|
||||
)
|
||||
.route("/static/{*filename}", get(get_static_))
|
||||
.with_state(shared_state);
|
||||
.route("/auth", get(get_auth).post(post_auth))
|
||||
.route("/logout", post(post_logout))
|
||||
.with_state(shared_state)
|
||||
.layer(session_layer);
|
||||
|
||||
let webserver = {
|
||||
let listener = tokio::net::TcpListener::bind(&config.bind_addr)
|
||||
|
|
46
src/session.rs
Normal file
46
src/session.rs
Normal file
|
@ -0,0 +1,46 @@
|
|||
use axum::extract::FromRequestParts;
|
||||
use axum::http::{header, request::Parts, HeaderMap, HeaderValue, StatusCode};
|
||||
use tower_sessions::Session;
|
||||
|
||||
#[allow(dead_code)] // TODO: WIP
|
||||
pub struct AuthenticatedUser {
|
||||
session: Session,
|
||||
api_token: String,
|
||||
}
|
||||
|
||||
pub const TOKEN_KEY: &str = "API_TOKEN";
|
||||
|
||||
impl<S> FromRequestParts<S> for AuthenticatedUser
|
||||
where
|
||||
S: Send + Sync,
|
||||
{
|
||||
type Rejection = (StatusCode, HeaderMap, String);
|
||||
|
||||
async fn from_request_parts(req: &mut Parts, state: &S) -> Result<Self, Self::Rejection> {
|
||||
let session = match Session::from_request_parts(req, state).await {
|
||||
Ok(session) => session,
|
||||
Err((status, body)) => return Err((status, HeaderMap::new(), body.to_string())),
|
||||
};
|
||||
|
||||
let api_token: Option<String> = session.get(TOKEN_KEY).await.unwrap(); // TODO: is it safe to [unwrap] here?
|
||||
|
||||
match api_token {
|
||||
Some(api_token) => Ok(Self { session, api_token }),
|
||||
None => {
|
||||
// Unclear that this case can actually happen, but in doubt,
|
||||
// clear the session and force to re-auth.
|
||||
// It is safe to [unwrap] the result here because we use
|
||||
// [MemoryStore] as the underlying store, on which [delete]
|
||||
// never fails.
|
||||
session.flush().await.unwrap();
|
||||
Err((
|
||||
StatusCode::SEE_OTHER,
|
||||
HeaderMap::from_iter(
|
||||
[(header::LOCATION, HeaderValue::from_static("/auth"))].into_iter(),
|
||||
),
|
||||
"client is not authenticated, redirecting to /auth".to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -83,3 +83,14 @@ input.radio-legit:checked + label {
|
|||
.score-Low {
|
||||
background: #5fd770;
|
||||
}
|
||||
|
||||
.aslink {
|
||||
display: inline;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font: inherit;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
background: transparent;
|
||||
color: currentColor;
|
||||
}
|
||||
|
|
22
templates/auth.html
Normal file
22
templates/auth.html
Normal file
|
@ -0,0 +1,22 @@
|
|||
{% extends "base.html" %}
|
||||
{% block title %}Authentication{% endblock title %}
|
||||
|
||||
{% block content %}
|
||||
<h1>Authentication required</h1>
|
||||
|
||||
<p>
|
||||
To obtain a suitable API token, you must login to Forgejo using an
|
||||
administrator account and go to <a href="{{forge_url}}user/settings/applications">{{forge_url}}user/settings/applications</a>.
|
||||
Then, create an API token with access to all organizations and repos, allowing
|
||||
read and writet access to: "admin", "misc", "organization", "issue", "repository" and "user". Store
|
||||
the resulting token safely (e.g. in your password manager).
|
||||
</p>
|
||||
|
||||
<form method="post">
|
||||
<label for="token">Forgejo API token:</label>
|
||||
<input id="token" name="token" type="password" required/>
|
||||
|
||||
<input type="submit" value="Submit" class="button"/>
|
||||
</form>
|
||||
|
||||
{% endblock content %}
|
|
@ -8,7 +8,10 @@
|
|||
</div>
|
||||
|
||||
<div>
|
||||
<a href="/classified">Edit classified users</a>
|
||||
<a href="/classified">Edit classified users</a> |
|
||||
<form action="/logout" method="post" style="display: inline">
|
||||
<button name="logout" value="logout" class="aslink">Log out</button>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="sort-options">
|
||||
|
|
Loading…
Add table
Reference in a new issue