Add authentication
This commit is contained in:
parent
56ebf2e376
commit
7563905e66
169
Cargo.lock
generated
169
Cargo.lock
generated
|
@ -81,6 +81,22 @@ dependencies = [
|
|||
"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]]
|
||||
name = "actix-macros"
|
||||
version = "0.2.4"
|
||||
|
@ -144,6 +160,22 @@ dependencies = [
|
|||
"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]]
|
||||
name = "actix-utils"
|
||||
version = "3.0.1"
|
||||
|
@ -222,6 +254,41 @@ version = "1.0.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "ahash"
|
||||
version = "0.8.11"
|
||||
|
@ -314,6 +381,12 @@ dependencies = [
|
|||
"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]]
|
||||
name = "arrayvec"
|
||||
version = "0.7.4"
|
||||
|
@ -356,6 +429,12 @@ version = "2.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a3826fb6e98ec72c0c0db8c9a40af4932d16793021027469857e84c1b50a1e8f"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.20.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0ea22880d78093b0cbe17c89f64a7d457941e65759157ec6cb31a31d652b05e5"
|
||||
|
||||
[[package]]
|
||||
name = "base64"
|
||||
version = "0.21.7"
|
||||
|
@ -474,6 +553,16 @@ version = "1.0.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "colorchoice"
|
||||
version = "1.0.1"
|
||||
|
@ -498,7 +587,14 @@ version = "0.16.2"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"
|
||||
dependencies = [
|
||||
"aes-gcm",
|
||||
"base64 0.20.0",
|
||||
"hkdf",
|
||||
"hmac",
|
||||
"percent-encoding",
|
||||
"rand",
|
||||
"sha2",
|
||||
"subtle",
|
||||
"time",
|
||||
"version_check",
|
||||
]
|
||||
|
@ -558,9 +654,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||
checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"rand_core",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ctr"
|
||||
version = "0.9.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835"
|
||||
dependencies = [
|
||||
"cipher",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "datamatrix"
|
||||
version = "0.3.1"
|
||||
|
@ -789,6 +895,17 @@ version = "0.3.30"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "futures-sink"
|
||||
version = "0.3.30"
|
||||
|
@ -809,6 +926,7 @@ checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48"
|
|||
dependencies = [
|
||||
"futures-core",
|
||||
"futures-io",
|
||||
"futures-macro",
|
||||
"futures-sink",
|
||||
"futures-task",
|
||||
"memchr",
|
||||
|
@ -838,6 +956,16 @@ dependencies = [
|
|||
"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]]
|
||||
name = "gimli"
|
||||
version = "0.29.0"
|
||||
|
@ -979,6 +1107,15 @@ dependencies = [
|
|||
"hashbrown",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inout"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "is_terminal_polyfill"
|
||||
version = "1.70.0"
|
||||
|
@ -1029,10 +1166,14 @@ name = "li7y"
|
|||
version = "0.0.0"
|
||||
dependencies = [
|
||||
"actix-files",
|
||||
"actix-identity",
|
||||
"actix-session",
|
||||
"actix-web",
|
||||
"barcoders",
|
||||
"base64 0.22.1",
|
||||
"datamatrix",
|
||||
"env_logger",
|
||||
"futures-util",
|
||||
"log",
|
||||
"maud",
|
||||
"mime",
|
||||
|
@ -1296,6 +1437,12 @@ version = "1.19.0"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
|
||||
|
||||
[[package]]
|
||||
name = "opaque-debug"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
|
||||
|
||||
[[package]]
|
||||
name = "owned_ttf_parser"
|
||||
version = "0.19.0"
|
||||
|
@ -1388,6 +1535,18 @@ version = "0.3.30"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "pom"
|
||||
version = "3.4.0"
|
||||
|
@ -2201,6 +2360,16 @@ version = "0.1.1"
|
|||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
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]]
|
||||
name = "url"
|
||||
version = "2.5.2"
|
||||
|
|
|
@ -11,10 +11,14 @@ license = "AGPL-3.0-or-later"
|
|||
|
||||
[dependencies]
|
||||
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"] }
|
||||
base64 = "0.22.1"
|
||||
datamatrix = "0.3.1"
|
||||
env_logger = "0.11.3"
|
||||
futures-util = "0.3.30"
|
||||
log = "0.4.21"
|
||||
maud = { version = "0.26.0", features = ["actix-web"] }
|
||||
mime = "0.3.17"
|
||||
|
|
75
src/frontend/auth.rs
Normal file
75
src/frontend/auth.rs
Normal 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()
|
||||
}
|
|
@ -4,6 +4,7 @@
|
|||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use actix_identity::Identity;
|
||||
use actix_web::{error, get, post, web, Responder};
|
||||
use maud::html;
|
||||
use sqlx::PgPool;
|
||||
|
@ -26,6 +27,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
|||
async fn show_item(
|
||||
pool: web::Data<PgPool>,
|
||||
path: web::Path<Uuid>,
|
||||
user: Identity,
|
||||
) -> actix_web::Result<impl Responder> {
|
||||
let id = path.into_inner();
|
||||
|
||||
|
@ -66,6 +68,7 @@ async fn show_item(
|
|||
name: "Edit".to_string(),
|
||||
}),
|
||||
],
|
||||
user: Some(user),
|
||||
..Default::default()
|
||||
},
|
||||
html! {
|
||||
|
@ -108,7 +111,7 @@ async fn show_item(
|
|||
}
|
||||
|
||||
#[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)
|
||||
.await
|
||||
.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(),
|
||||
}),
|
||||
],
|
||||
user: Some(user),
|
||||
..Default::default()
|
||||
},
|
||||
html! {
|
||||
|
@ -184,6 +188,7 @@ async fn list_items(pool: web::Data<PgPool>) -> actix_web::Result<impl Responder
|
|||
async fn add_item(
|
||||
pool: web::Data<PgPool>,
|
||||
form: web::Query<NewItemForm>,
|
||||
user: Identity,
|
||||
) -> actix_web::Result<impl Responder> {
|
||||
let datalist_items = datalist::items(&pool)
|
||||
.await
|
||||
|
@ -199,6 +204,7 @@ async fn add_item(
|
|||
title: Some("Add Item"),
|
||||
page_title: Some(Box::new("Add Item")),
|
||||
datalists: vec![&datalist_items, &datalist_item_classes],
|
||||
user: Some(user),
|
||||
..Default::default()
|
||||
},
|
||||
html! {
|
||||
|
@ -238,6 +244,7 @@ async fn add_item(
|
|||
async fn add_item_post(
|
||||
data: web::Form<NewItem>,
|
||||
pool: web::Data<PgPool>,
|
||||
_user: Identity,
|
||||
) -> actix_web::Result<impl Responder> {
|
||||
let item = manage::item::add(&pool, data.into_inner())
|
||||
.await
|
||||
|
@ -249,6 +256,7 @@ async fn add_item_post(
|
|||
async fn edit_item(
|
||||
pool: web::Data<PgPool>,
|
||||
path: web::Path<Uuid>,
|
||||
user: Identity,
|
||||
) -> actix_web::Result<impl Responder> {
|
||||
let id = path.into_inner();
|
||||
|
||||
|
@ -278,6 +286,7 @@ async fn edit_item(
|
|||
title: Some(&title),
|
||||
page_title: Some(Box::new(item_name.clone())),
|
||||
datalists: vec![&datalist_items, &datalist_item_classes],
|
||||
user: Some(user),
|
||||
..Default::default()
|
||||
},
|
||||
html! {
|
||||
|
@ -329,6 +338,7 @@ async fn edit_item_post(
|
|||
pool: web::Data<PgPool>,
|
||||
path: web::Path<Uuid>,
|
||||
data: web::Form<NewItem>,
|
||||
_user: Identity,
|
||||
) -> actix_web::Result<impl Responder> {
|
||||
let id = path.into_inner();
|
||||
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use actix_identity::Identity;
|
||||
use actix_web::{error, get, post, web, Responder};
|
||||
use maud::html;
|
||||
use sqlx::PgPool;
|
||||
|
@ -24,6 +25,7 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
|||
async fn show_item_class(
|
||||
pool: web::Data<PgPool>,
|
||||
path: web::Path<Uuid>,
|
||||
user: Identity,
|
||||
) -> actix_web::Result<impl Responder> {
|
||||
let id = path.into_inner();
|
||||
|
||||
|
@ -70,6 +72,7 @@ async fn show_item_class(
|
|||
title: Some(&title),
|
||||
page_title: Some(Box::new(item_class.name.clone())),
|
||||
page_actions,
|
||||
user: Some(user),
|
||||
..Default::default()
|
||||
},
|
||||
html! {
|
||||
|
@ -106,7 +109,10 @@ async fn show_item_class(
|
|||
}
|
||||
|
||||
#[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")
|
||||
.fetch_all(pool.as_ref())
|
||||
.await
|
||||
|
@ -127,6 +133,7 @@ async fn list_item_classes(pool: web::Data<PgPool>) -> actix_web::Result<impl Re
|
|||
name: "Add".to_string(),
|
||||
}),
|
||||
],
|
||||
user: Some(user),
|
||||
..Default::default()
|
||||
},
|
||||
html! {
|
||||
|
@ -162,6 +169,7 @@ async fn list_item_classes(pool: web::Data<PgPool>) -> actix_web::Result<impl Re
|
|||
async fn add_item_class(
|
||||
pool: web::Data<PgPool>,
|
||||
form: web::Query<NewItemClassForm>,
|
||||
user: Identity,
|
||||
) -> actix_web::Result<impl Responder> {
|
||||
let datalist_item_classes = datalist::item_classes(&pool)
|
||||
.await
|
||||
|
@ -173,6 +181,7 @@ async fn add_item_class(
|
|||
title: Some("Add Item Class"),
|
||||
page_title: Some(Box::new("Add Item Class")),
|
||||
datalists: vec![&datalist_item_classes],
|
||||
user: Some(user),
|
||||
..Default::default()
|
||||
},
|
||||
html! {
|
||||
|
@ -205,6 +214,7 @@ async fn add_item_class(
|
|||
async fn add_item_class_post(
|
||||
data: web::Form<NewItemClass>,
|
||||
pool: web::Data<PgPool>,
|
||||
_user: Identity,
|
||||
) -> actix_web::Result<impl Responder> {
|
||||
let item = manage::item_class::add(&pool, data.into_inner())
|
||||
.await
|
||||
|
@ -216,6 +226,7 @@ async fn add_item_class_post(
|
|||
async fn edit_item_class(
|
||||
pool: web::Data<PgPool>,
|
||||
path: web::Path<Uuid>,
|
||||
user: Identity,
|
||||
) -> actix_web::Result<impl Responder> {
|
||||
let id = path.into_inner();
|
||||
|
||||
|
@ -236,6 +247,7 @@ async fn edit_item_class(
|
|||
title: Some(&title),
|
||||
page_title: Some(Box::new(item_class.name.clone())),
|
||||
datalists: vec![&datalist_item_classes],
|
||||
user: Some(user),
|
||||
..Default::default()
|
||||
},
|
||||
html! {
|
||||
|
@ -278,6 +290,7 @@ async fn edit_item_class_post(
|
|||
pool: web::Data<PgPool>,
|
||||
path: web::Path<Uuid>,
|
||||
data: web::Form<NewItemClass>,
|
||||
_user: Identity,
|
||||
) -> actix_web::Result<impl Responder> {
|
||||
let id = path.into_inner();
|
||||
|
||||
|
|
|
@ -2,10 +2,12 @@
|
|||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
mod auth;
|
||||
mod item;
|
||||
mod item_class;
|
||||
mod templates;
|
||||
|
||||
use actix_identity::Identity;
|
||||
use actix_web::{error, get, web, Responder};
|
||||
use maud::html;
|
||||
use serde::Deserialize;
|
||||
|
@ -18,13 +20,20 @@ use crate::models::EntityType;
|
|||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(index)
|
||||
.service(jump)
|
||||
.configure(auth::config)
|
||||
.configure(item::config)
|
||||
.configure(item_class::config);
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
async fn index() -> impl Responder {
|
||||
templates::base(templates::TemplateConfig::default(), html! {})
|
||||
async fn index(user: Identity) -> impl Responder {
|
||||
templates::base(
|
||||
templates::TemplateConfig {
|
||||
user: Some(user),
|
||||
..Default::default()
|
||||
},
|
||||
html! {},
|
||||
)
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
|
@ -36,6 +45,7 @@ struct JumpData {
|
|||
async fn jump(
|
||||
pool: web::Data<PgPool>,
|
||||
data: web::Query<JumpData>,
|
||||
_user: Identity, // this endpoint leaks information about the existence of items
|
||||
) -> Result<impl Responder, error::Error> {
|
||||
let mut id = data.id.clone();
|
||||
|
||||
|
|
|
@ -6,6 +6,7 @@ pub mod datalist;
|
|||
pub mod forms;
|
||||
pub mod helpers;
|
||||
|
||||
use actix_identity::Identity;
|
||||
use maud::{html, Markup, Render, DOCTYPE};
|
||||
|
||||
use datalist::Datalist;
|
||||
|
@ -19,7 +20,7 @@ const NAVBAR_ITEMS: &[(&str, &str)] = &[
|
|||
("/item-classes", "Item Classes"),
|
||||
];
|
||||
|
||||
fn navbar(path: &str) -> Markup {
|
||||
fn navbar(config: &TemplateConfig) -> Markup {
|
||||
html! {
|
||||
nav .navbar.navbar-expand-lg.bg-body-secondary {
|
||||
div .container {
|
||||
|
@ -32,13 +33,21 @@ fn navbar(path: &str) -> Markup {
|
|||
div .collapse.navbar-collapse #navbar-expander {
|
||||
ul .navbar-nav.me-md-3.mb-2.mb-md-0 {
|
||||
@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" {
|
||||
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_js: Vec<Js<'a>>,
|
||||
pub datalists: Vec<&'a Datalist>,
|
||||
pub user: Option<Identity>,
|
||||
}
|
||||
|
||||
impl Default for TemplateConfig<'_> {
|
||||
|
@ -75,6 +85,7 @@ impl Default for TemplateConfig<'_> {
|
|||
extra_css: Vec::new(),
|
||||
extra_js: 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";
|
||||
|
||||
(Css::File("/static/vendor/bootstrap.min.css"))
|
||||
@for css in config.extra_css {
|
||||
@for css in &config.extra_css {
|
||||
(css)
|
||||
}
|
||||
}
|
||||
|
||||
body .d-flex.flex-column.h-100 {
|
||||
(navbar(config.path))
|
||||
(navbar(&config))
|
||||
|
||||
main .container.my-4 {
|
||||
div .d-flex.justify-content-between.mb-3 {
|
||||
|
|
|
@ -6,4 +6,5 @@ pub mod api;
|
|||
pub mod frontend;
|
||||
pub mod label;
|
||||
pub mod manage;
|
||||
pub mod middleware;
|
||||
pub mod models;
|
||||
|
|
36
src/main.rs
36
src/main.rs
|
@ -4,13 +4,30 @@
|
|||
|
||||
use std::env;
|
||||
|
||||
use actix_web::{web, App, HttpServer};
|
||||
use log::{debug, info};
|
||||
use actix_identity::IdentityMiddleware;
|
||||
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]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
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(
|
||||
&env::var("DATABASE_URL").expect("DATABASE_URL must be set"),
|
||||
)
|
||||
|
@ -34,9 +51,22 @@ async fn main() -> std::io::Result<()> {
|
|||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.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))
|
||||
.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))?
|
||||
.run()
|
||||
|
|
19
src/middleware/error_handlers.rs
Normal file
19
src/middleware/error_handlers.rs
Normal 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(),
|
||||
),
|
||||
))
|
||||
}
|
60
src/middleware/force_identity.rs
Normal file
60
src/middleware/force_identity.rs
Normal 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
8
src/middleware/mod.rs
Normal 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;
|
Loading…
Reference in a new issue