Add authentication

This commit is contained in:
Simon Bruder 2024-07-13 13:41:23 +02:00
parent d4b2ecfcea
commit 3a222986c1
Signed by: simon
GPG key ID: 347FF8699CDA0776
12 changed files with 422 additions and 12 deletions

169
Cargo.lock generated
View file

@ -81,6 +81,22 @@ dependencies = [
"zstd", "zstd",
] ]
[[package]]
name = "actix-identity"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2c99b7a5614b72a78f04aa2021e5370fc1aef2475fffeffc0c1266b99007062"
dependencies = [
"actix-service",
"actix-session",
"actix-utils",
"actix-web",
"derive_more",
"futures-core",
"serde",
"tracing",
]
[[package]] [[package]]
name = "actix-macros" name = "actix-macros"
version = "0.2.4" version = "0.2.4"
@ -144,6 +160,22 @@ dependencies = [
"pin-project-lite", "pin-project-lite",
] ]
[[package]]
name = "actix-session"
version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b671404ec72194d8af58c2bdaf51e3c477a0595056bd5010148405870dda8df2"
dependencies = [
"actix-service",
"actix-utils",
"actix-web",
"anyhow",
"derive_more",
"serde",
"serde_json",
"tracing",
]
[[package]] [[package]]
name = "actix-utils" name = "actix-utils"
version = "3.0.1" version = "3.0.1"
@ -222,6 +254,41 @@ version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe"
[[package]]
name = "aead"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0"
dependencies = [
"crypto-common",
"generic-array",
]
[[package]]
name = "aes"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0"
dependencies = [
"cfg-if",
"cipher",
"cpufeatures",
]
[[package]]
name = "aes-gcm"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1"
dependencies = [
"aead",
"aes",
"cipher",
"ctr",
"ghash",
"subtle",
]
[[package]] [[package]]
name = "ahash" name = "ahash"
version = "0.8.11" version = "0.8.11"
@ -314,6 +381,12 @@ dependencies = [
"windows-sys 0.52.0", "windows-sys 0.52.0",
] ]
[[package]]
name = "anyhow"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]] [[package]]
name = "arrayvec" name = "arrayvec"
version = "0.7.4" version = "0.7.4"
@ -356,6 +429,12 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a3826fb6e98ec72c0c0db8c9a40af4932d16793021027469857e84c1b50a1e8f" checksum = "a3826fb6e98ec72c0c0db8c9a40af4932d16793021027469857e84c1b50a1e8f"
[[package]]
name = "base64"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5"
[[package]] [[package]]
name = "base64" name = "base64"
version = "0.21.7" version = "0.21.7"
@ -474,6 +553,16 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cipher"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad"
dependencies = [
"crypto-common",
"inout",
]
[[package]] [[package]]
name = "colorchoice" name = "colorchoice"
version = "1.0.1" version = "1.0.1"
@ -498,7 +587,14 @@ version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb" checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"
dependencies = [ dependencies = [
"aes-gcm",
"base64 0.20.0",
"hkdf",
"hmac",
"percent-encoding", "percent-encoding",
"rand",
"sha2",
"subtle",
"time", "time",
"version_check", "version_check",
] ]
@ -558,9 +654,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
dependencies = [ dependencies = [
"generic-array", "generic-array",
"rand_core",
"typenum", "typenum",
] ]
[[package]]
name = "ctr"
version = "0.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
dependencies = [
"cipher",
]
[[package]] [[package]]
name = "datamatrix" name = "datamatrix"
version = "0.3.1" version = "0.3.1"
@ -789,6 +895,17 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1"
[[package]]
name = "futures-macro"
version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac"
dependencies = [
"proc-macro2",
"quote",
"syn 2.0.68",
]
[[package]] [[package]]
name = "futures-sink" name = "futures-sink"
version = "0.3.30" version = "0.3.30"
@ -809,6 +926,7 @@ checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-io", "futures-io",
"futures-macro",
"futures-sink", "futures-sink",
"futures-task", "futures-task",
"memchr", "memchr",
@ -838,6 +956,16 @@ dependencies = [
"wasi", "wasi",
] ]
[[package]]
name = "ghash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
dependencies = [
"opaque-debug",
"polyval",
]
[[package]] [[package]]
name = "gimli" name = "gimli"
version = "0.29.0" version = "0.29.0"
@ -979,6 +1107,15 @@ dependencies = [
"hashbrown", "hashbrown",
] ]
[[package]]
name = "inout"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
dependencies = [
"generic-array",
]
[[package]] [[package]]
name = "is_terminal_polyfill" name = "is_terminal_polyfill"
version = "1.70.0" version = "1.70.0"
@ -1029,10 +1166,14 @@ name = "li7y"
version = "0.0.0" version = "0.0.0"
dependencies = [ dependencies = [
"actix-files", "actix-files",
"actix-identity",
"actix-session",
"actix-web", "actix-web",
"barcoders", "barcoders",
"base64 0.22.1",
"datamatrix", "datamatrix",
"env_logger", "env_logger",
"futures-util",
"log", "log",
"maud", "maud",
"mime", "mime",
@ -1296,6 +1437,12 @@ version = "1.19.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "opaque-debug"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
[[package]] [[package]]
name = "owned_ttf_parser" name = "owned_ttf_parser"
version = "0.19.0" version = "0.19.0"
@ -1388,6 +1535,18 @@ version = "0.3.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec"
[[package]]
name = "polyval"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25"
dependencies = [
"cfg-if",
"cpufeatures",
"opaque-debug",
"universal-hash",
]
[[package]] [[package]]
name = "pom" name = "pom"
version = "3.4.0" version = "3.4.0"
@ -2201,6 +2360,16 @@ version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e"
[[package]]
name = "universal-hash"
version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea"
dependencies = [
"crypto-common",
"subtle",
]
[[package]] [[package]]
name = "url" name = "url"
version = "2.5.2" version = "2.5.2"

View file

@ -11,10 +11,14 @@ license = "AGPL-3.0-or-later"
[dependencies] [dependencies]
actix-files = "0.6.6" actix-files = "0.6.6"
actix-web = "4.8.0" actix-identity = "0.7.1"
actix-session = { version = "0.9.0", features = ["cookie-session"] }
actix-web = { version = "4.8.0", features = ["cookies"] }
barcoders = { version = "2.0.0", default-features = false, features = ["std"] } barcoders = { version = "2.0.0", default-features = false, features = ["std"] }
base64 = "0.22.1"
datamatrix = "0.3.1" datamatrix = "0.3.1"
env_logger = "0.11.3" env_logger = "0.11.3"
futures-util = "0.3.30"
log = "0.4.21" log = "0.4.21"
maud = { version = "0.26.0", features = ["actix-web"] } maud = { version = "0.26.0", features = ["actix-web"] }
mime = "0.3.17" mime = "0.3.17"

75
src/frontend/auth.rs Normal file
View file

@ -0,0 +1,75 @@
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use actix_identity::Identity;
use actix_web::{error, get, post, web, HttpMessage, HttpRequest, HttpResponse, Responder};
use maud::html;
use serde::Deserialize;
use super::templates;
pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(login_form).service(login).service(logout);
}
#[get("/login")]
async fn login_form(req: HttpRequest, user: Option<Identity>) -> HttpResponse {
if user.is_some() {
return web::Redirect::to("/")
.see_other()
.respond_to(&req)
.map_into_boxed_body();
}
templates::base(
templates::TemplateConfig {
path: "/login",
title: Some("Login"),
page_title: Some(Box::new("Login")),
user,
..Default::default()
},
html! {
form .w-25.mb-4 method="POST" {
div .mb-3 {
label .form-label for="password" { "Password" };
input .form-control type="password" id="password" name="password";
}
button .btn.btn-primary type="submit" { "Login" }
}
},
)
.respond_to(&req)
.map_into_boxed_body()
}
#[derive(Deserialize)]
struct LoginForm {
password: String,
}
#[post("/login")]
async fn login(
req: HttpRequest,
form: web::Form<LoginForm>,
) -> Result<impl Responder, error::Error> {
// Very basic authentication for now (only password, hardcoded in environment variable)
if form.password
== std::env::var("SUPERUSER_PASSWORD")
.map_err(|_| error::ErrorInternalServerError("login disabled (no password set)"))?
{
Identity::login(&req.extensions(), "superuser".into())
.map_err(error::ErrorInternalServerError)?;
Ok(web::Redirect::to("/".to_owned()).see_other())
} else {
Ok(web::Redirect::to("/login".to_owned()).see_other())
}
}
#[post("/logout")]
async fn logout(user: Identity) -> impl Responder {
user.logout();
web::Redirect::to("/login".to_owned()).see_other()
}

View file

@ -4,6 +4,7 @@
use std::collections::HashMap; use std::collections::HashMap;
use actix_identity::Identity;
use actix_web::{error, get, post, web, Responder}; use actix_web::{error, get, post, web, Responder};
use maud::html; use maud::html;
use sqlx::PgPool; use sqlx::PgPool;
@ -26,6 +27,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
async fn show_item( async fn show_item(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
path: web::Path<Uuid>, path: web::Path<Uuid>,
user: Identity,
) -> actix_web::Result<impl Responder> { ) -> actix_web::Result<impl Responder> {
let id = path.into_inner(); let id = path.into_inner();
@ -66,6 +68,7 @@ async fn show_item(
name: "Edit".to_string(), name: "Edit".to_string(),
}), }),
], ],
user: Some(user),
..Default::default() ..Default::default()
}, },
html! { html! {
@ -108,7 +111,7 @@ async fn show_item(
} }
#[get("/items")] #[get("/items")]
async fn list_items(pool: web::Data<PgPool>) -> actix_web::Result<impl Responder> { async fn list_items(pool: web::Data<PgPool>, user: Identity) -> actix_web::Result<impl Responder> {
let item_list = manage::item::get_all(&pool) let item_list = manage::item::get_all(&pool)
.await .await
.map_err(error::ErrorInternalServerError)?; .map_err(error::ErrorInternalServerError)?;
@ -150,6 +153,7 @@ async fn list_items(pool: web::Data<PgPool>) -> actix_web::Result<impl Responder
name: "Add".to_string(), name: "Add".to_string(),
}), }),
], ],
user: Some(user),
..Default::default() ..Default::default()
}, },
html! { html! {
@ -184,6 +188,7 @@ async fn list_items(pool: web::Data<PgPool>) -> actix_web::Result<impl Responder
async fn add_item( async fn add_item(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
form: web::Query<NewItemForm>, form: web::Query<NewItemForm>,
user: Identity,
) -> actix_web::Result<impl Responder> { ) -> actix_web::Result<impl Responder> {
let datalist_items = datalist::items(&pool) let datalist_items = datalist::items(&pool)
.await .await
@ -199,6 +204,7 @@ async fn add_item(
title: Some("Add Item"), title: Some("Add Item"),
page_title: Some(Box::new("Add Item")), page_title: Some(Box::new("Add Item")),
datalists: vec![&datalist_items, &datalist_item_classes], datalists: vec![&datalist_items, &datalist_item_classes],
user: Some(user),
..Default::default() ..Default::default()
}, },
html! { html! {
@ -238,6 +244,7 @@ async fn add_item(
async fn add_item_post( async fn add_item_post(
data: web::Form<NewItem>, data: web::Form<NewItem>,
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
_user: Identity,
) -> actix_web::Result<impl Responder> { ) -> actix_web::Result<impl Responder> {
let item = manage::item::add(&pool, data.into_inner()) let item = manage::item::add(&pool, data.into_inner())
.await .await
@ -249,6 +256,7 @@ async fn add_item_post(
async fn edit_item( async fn edit_item(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
path: web::Path<Uuid>, path: web::Path<Uuid>,
user: Identity,
) -> actix_web::Result<impl Responder> { ) -> actix_web::Result<impl Responder> {
let id = path.into_inner(); let id = path.into_inner();
@ -278,6 +286,7 @@ async fn edit_item(
title: Some(&title), title: Some(&title),
page_title: Some(Box::new(item_name.clone())), page_title: Some(Box::new(item_name.clone())),
datalists: vec![&datalist_items, &datalist_item_classes], datalists: vec![&datalist_items, &datalist_item_classes],
user: Some(user),
..Default::default() ..Default::default()
}, },
html! { html! {
@ -329,6 +338,7 @@ async fn edit_item_post(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
path: web::Path<Uuid>, path: web::Path<Uuid>,
data: web::Form<NewItem>, data: web::Form<NewItem>,
_user: Identity,
) -> actix_web::Result<impl Responder> { ) -> actix_web::Result<impl Responder> {
let id = path.into_inner(); let id = path.into_inner();

View file

@ -2,6 +2,7 @@
// //
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
use actix_identity::Identity;
use actix_web::{error, get, post, web, Responder}; use actix_web::{error, get, post, web, Responder};
use maud::html; use maud::html;
use sqlx::PgPool; use sqlx::PgPool;
@ -24,6 +25,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
async fn show_item_class( async fn show_item_class(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
path: web::Path<Uuid>, path: web::Path<Uuid>,
user: Identity,
) -> actix_web::Result<impl Responder> { ) -> actix_web::Result<impl Responder> {
let id = path.into_inner(); let id = path.into_inner();
@ -70,6 +72,7 @@ async fn show_item_class(
title: Some(&title), title: Some(&title),
page_title: Some(Box::new(item_class.name.clone())), page_title: Some(Box::new(item_class.name.clone())),
page_actions, page_actions,
user: Some(user),
..Default::default() ..Default::default()
}, },
html! { html! {
@ -106,7 +109,10 @@ async fn show_item_class(
} }
#[get("/item-classes")] #[get("/item-classes")]
async fn list_item_classes(pool: web::Data<PgPool>) -> actix_web::Result<impl Responder> { async fn list_item_classes(
pool: web::Data<PgPool>,
user: Identity,
) -> actix_web::Result<impl Responder> {
let item_classes_ids = sqlx::query_scalar!("SELECT id FROM item_classes ORDER BY created_at") let item_classes_ids = sqlx::query_scalar!("SELECT id FROM item_classes ORDER BY created_at")
.fetch_all(pool.as_ref()) .fetch_all(pool.as_ref())
.await .await
@ -127,6 +133,7 @@ async fn list_item_classes(pool: web::Data<PgPool>) -> actix_web::Result<impl Re
name: "Add".to_string(), name: "Add".to_string(),
}), }),
], ],
user: Some(user),
..Default::default() ..Default::default()
}, },
html! { html! {
@ -162,6 +169,7 @@ async fn list_item_classes(pool: web::Data<PgPool>) -> actix_web::Result<impl Re
async fn add_item_class( async fn add_item_class(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
form: web::Query<NewItemClassForm>, form: web::Query<NewItemClassForm>,
user: Identity,
) -> actix_web::Result<impl Responder> { ) -> actix_web::Result<impl Responder> {
let datalist_item_classes = datalist::item_classes(&pool) let datalist_item_classes = datalist::item_classes(&pool)
.await .await
@ -173,6 +181,7 @@ async fn add_item_class(
title: Some("Add Item Class"), title: Some("Add Item Class"),
page_title: Some(Box::new("Add Item Class")), page_title: Some(Box::new("Add Item Class")),
datalists: vec![&datalist_item_classes], datalists: vec![&datalist_item_classes],
user: Some(user),
..Default::default() ..Default::default()
}, },
html! { html! {
@ -205,6 +214,7 @@ async fn add_item_class(
async fn add_item_class_post( async fn add_item_class_post(
data: web::Form<NewItemClass>, data: web::Form<NewItemClass>,
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
_user: Identity,
) -> actix_web::Result<impl Responder> { ) -> actix_web::Result<impl Responder> {
let item = manage::item_class::add(&pool, data.into_inner()) let item = manage::item_class::add(&pool, data.into_inner())
.await .await
@ -216,6 +226,7 @@ async fn add_item_class_post(
async fn edit_item_class( async fn edit_item_class(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
path: web::Path<Uuid>, path: web::Path<Uuid>,
user: Identity,
) -> actix_web::Result<impl Responder> { ) -> actix_web::Result<impl Responder> {
let id = path.into_inner(); let id = path.into_inner();
@ -236,6 +247,7 @@ async fn edit_item_class(
title: Some(&title), title: Some(&title),
page_title: Some(Box::new(item_class.name.clone())), page_title: Some(Box::new(item_class.name.clone())),
datalists: vec![&datalist_item_classes], datalists: vec![&datalist_item_classes],
user: Some(user),
..Default::default() ..Default::default()
}, },
html! { html! {
@ -278,6 +290,7 @@ async fn edit_item_class_post(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
path: web::Path<Uuid>, path: web::Path<Uuid>,
data: web::Form<NewItemClass>, data: web::Form<NewItemClass>,
_user: Identity,
) -> actix_web::Result<impl Responder> { ) -> actix_web::Result<impl Responder> {
let id = path.into_inner(); let id = path.into_inner();

View file

@ -2,10 +2,12 @@
// //
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
mod auth;
mod item; mod item;
mod item_class; mod item_class;
mod templates; mod templates;
use actix_identity::Identity;
use actix_web::{error, get, web, Responder}; use actix_web::{error, get, web, Responder};
use maud::html; use maud::html;
use serde::Deserialize; use serde::Deserialize;
@ -18,13 +20,20 @@ use crate::models::EntityType;
pub fn config(cfg: &mut web::ServiceConfig) { pub fn config(cfg: &mut web::ServiceConfig) {
cfg.service(index) cfg.service(index)
.service(jump) .service(jump)
.configure(auth::config)
.configure(item::config) .configure(item::config)
.configure(item_class::config); .configure(item_class::config);
} }
#[get("/")] #[get("/")]
async fn index() -> impl Responder { async fn index(user: Identity) -> impl Responder {
templates::base(templates::TemplateConfig::default(), html! {}) templates::base(
templates::TemplateConfig {
user: Some(user),
..Default::default()
},
html! {},
)
} }
#[derive(Deserialize)] #[derive(Deserialize)]
@ -36,6 +45,7 @@ struct JumpData {
async fn jump( async fn jump(
pool: web::Data<PgPool>, pool: web::Data<PgPool>,
data: web::Query<JumpData>, data: web::Query<JumpData>,
_user: Identity, // this endpoint leaks information about the existence of items
) -> Result<impl Responder, error::Error> { ) -> Result<impl Responder, error::Error> {
let mut id = data.id.clone(); let mut id = data.id.clone();

View file

@ -6,6 +6,7 @@ pub mod datalist;
pub mod forms; pub mod forms;
pub mod helpers; pub mod helpers;
use actix_identity::Identity;
use maud::{html, Markup, Render, DOCTYPE}; use maud::{html, Markup, Render, DOCTYPE};
use datalist::Datalist; use datalist::Datalist;
@ -19,7 +20,7 @@ const NAVBAR_ITEMS: &[(&str, &str)] = &[
("/item-classes", "Item Classes"), ("/item-classes", "Item Classes"),
]; ];
fn navbar(path: &str) -> Markup { fn navbar(config: &TemplateConfig) -> Markup {
html! { html! {
nav .navbar.navbar-expand-lg.bg-body-secondary { nav .navbar.navbar-expand-lg.bg-body-secondary {
div .container { div .container {
@ -32,13 +33,21 @@ fn navbar(path: &str) -> Markup {
div .collapse.navbar-collapse #navbar-expander { div .collapse.navbar-collapse #navbar-expander {
ul .navbar-nav.me-md-3.mb-2.mb-md-0 { ul .navbar-nav.me-md-3.mb-2.mb-md-0 {
@for (target, name) in NAVBAR_ITEMS { @for (target, name) in NAVBAR_ITEMS {
li .nav-item { a .nav-link .active[path == *target] href=(target) { (name) } } li .nav-item { a .nav-link .active[config.path == *target] href=(target) { (name) } }
} }
} }
form .d-flex.flex-fill role="search" action="/jump" { form .d-flex.flex-fill role="search" action="/jump" {
input .form-control.me-2 type="search" name="id" placeholder="Jump to ID"; input .form-control.me-2 type="search" name="id" placeholder="Jump to ID";
} }
@if config.user.is_some() {
form action="/logout" method="POST" {
button .btn.btn-outline-secondary type="submit" { "Logout" }
}
} @else {
a .btn.btn-primary href="/login" { "Login" }
}
} }
} }
} }
@ -63,6 +72,7 @@ pub struct TemplateConfig<'a> {
pub extra_css: Vec<Css<'a>>, pub extra_css: Vec<Css<'a>>,
pub extra_js: Vec<Js<'a>>, pub extra_js: Vec<Js<'a>>,
pub datalists: Vec<&'a Datalist>, pub datalists: Vec<&'a Datalist>,
pub user: Option<Identity>,
} }
impl Default for TemplateConfig<'_> { impl Default for TemplateConfig<'_> {
@ -75,6 +85,7 @@ impl Default for TemplateConfig<'_> {
extra_css: Vec::new(), extra_css: Vec::new(),
extra_js: Vec::new(), extra_js: Vec::new(),
datalists: Vec::new(), datalists: Vec::new(),
user: None,
} }
} }
} }
@ -95,13 +106,13 @@ pub fn base(config: TemplateConfig, content: Markup) -> Markup {
meta name="viewport" content="width=device-width, initial-scale=1"; meta name="viewport" content="width=device-width, initial-scale=1";
(Css::File("/static/vendor/bootstrap.min.css")) (Css::File("/static/vendor/bootstrap.min.css"))
@for css in config.extra_css { @for css in &config.extra_css {
(css) (css)
} }
} }
body .d-flex.flex-column.h-100 { body .d-flex.flex-column.h-100 {
(navbar(config.path)) (navbar(&config))
main .container.my-4 { main .container.my-4 {
div .d-flex.justify-content-between.mb-3 { div .d-flex.justify-content-between.mb-3 {

View file

@ -6,4 +6,5 @@ pub mod api;
pub mod frontend; pub mod frontend;
pub mod label; pub mod label;
pub mod manage; pub mod manage;
pub mod middleware;
pub mod models; pub mod models;

View file

@ -4,13 +4,30 @@
use std::env; use std::env;
use actix_web::{web, App, HttpServer}; use actix_identity::IdentityMiddleware;
use log::{debug, info}; use actix_session::{storage::CookieSessionStore, SessionMiddleware};
use actix_web::middleware::ErrorHandlers;
use actix_web::{cookie::Key, http::StatusCode, web, App, HttpServer};
use base64::prelude::{Engine as _, BASE64_STANDARD};
use log::{debug, info, warn};
#[actix_web::main] #[actix_web::main]
async fn main() -> std::io::Result<()> { async fn main() -> std::io::Result<()> {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init(); env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
// generate a secret key with head -c 64 /dev/urandom | base64 -w0
let secret_key = match env::var("SECRET_KEY") {
Ok(encoded) => Key::from(
&BASE64_STANDARD
.decode(encoded)
.expect("failed to decode base64 in SECRET_KEY"),
),
Err(_) => {
warn!("SECRET_KEY was not specified, using randomly generated key");
Key::generate()
}
};
let pool: sqlx::PgPool = sqlx::Pool::<sqlx::postgres::Postgres>::connect( let pool: sqlx::PgPool = sqlx::Pool::<sqlx::postgres::Postgres>::connect(
&env::var("DATABASE_URL").expect("DATABASE_URL must be set"), &env::var("DATABASE_URL").expect("DATABASE_URL must be set"),
) )
@ -34,9 +51,22 @@ async fn main() -> std::io::Result<()> {
HttpServer::new(move || { HttpServer::new(move || {
App::new() App::new()
.app_data(web::Data::new(pool.clone())) .app_data(web::Data::new(pool.clone()))
.service(web::scope("/api/v1").configure(li7y::api::v1::config)) .service(
web::scope("/api/v1")
.wrap(li7y::middleware::ForceIdentity)
.configure(li7y::api::v1::config),
)
.service(actix_files::Files::new("/static", &static_root)) .service(actix_files::Files::new("/static", &static_root))
.configure(li7y::frontend::config) .configure(li7y::frontend::config)
.wrap(ErrorHandlers::new().handler(
StatusCode::UNAUTHORIZED,
li7y::middleware::error_handlers::redirect_to_login,
))
.wrap(IdentityMiddleware::default())
.wrap(SessionMiddleware::new(
CookieSessionStore::default(),
secret_key.clone(),
))
}) })
.bind((address, port))? .bind((address, port))?
.run() .run()

View file

@ -0,0 +1,19 @@
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use actix_web::middleware::ErrorHandlerResponse;
use actix_web::{dev::ServiceResponse, error, HttpResponse};
pub fn redirect_to_login<B>(
res: ServiceResponse<B>,
) -> Result<ErrorHandlerResponse<B>, error::Error> {
Ok(ErrorHandlerResponse::Response(
res.into_response(
HttpResponse::SeeOther()
.insert_header(("Location", "/login"))
.finish()
.map_into_right_body(),
),
))
}

View file

@ -0,0 +1,60 @@
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
use std::future::{ready, Ready};
use actix_identity::Identity;
use actix_web::{
dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform},
Error,
};
use futures_util::future::LocalBoxFuture;
pub struct ForceIdentity;
impl<S, B> Transform<S, ServiceRequest> for ForceIdentity
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type InitError = ();
type Transform = ForceIdentityMiddleware<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(ForceIdentityMiddleware { service }))
}
}
pub struct ForceIdentityMiddleware<S> {
service: S,
}
impl<S, B> Service<ServiceRequest> for ForceIdentityMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
S::Future: 'static,
B: 'static,
{
type Response = ServiceResponse<B>;
type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
forward_ready!(service);
fn call(&self, mut req: ServiceRequest) -> Self::Future {
let user = req.extract::<Identity>();
let fut = self.service.call(req);
Box::pin(async move {
let res = fut.await?;
let _ = user.await?;
Ok(res)
})
}
}

8
src/middleware/mod.rs Normal file
View file

@ -0,0 +1,8 @@
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
//
// SPDX-License-Identifier: AGPL-3.0-or-later
pub mod error_handlers;
mod force_identity;
pub use force_identity::ForceIdentity;