diff --git a/.idea/workspace.xml b/.idea/workspace.xml
index 321fb86..0faba63 100644
--- a/.idea/workspace.xml
+++ b/.idea/workspace.xml
@@ -10,15 +10,13 @@
-
+
+
-
-
+
-
-
@@ -30,6 +28,7 @@
@@ -158,7 +157,8 @@
-
+
+
@@ -264,7 +264,15 @@
1736241771673
-
+
+
+ 1739198426101
+
+
+
+ 1739198426101
+
+
@@ -295,7 +303,8 @@
-
+
+
diff --git a/bookkeeper/Cargo.lock b/bookkeeper/Cargo.lock
index 858dfa0..c471d0e 100644
--- a/bookkeeper/Cargo.lock
+++ b/bookkeeper/Cargo.lock
@@ -17,13 +17,48 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627"
+[[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.7.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9"
dependencies = [
- "getrandom",
+ "getrandom 0.2.15",
"once_cell",
"version_check",
]
@@ -352,6 +387,19 @@ version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b"
+[[package]]
+name = "bcrypt"
+version = "0.17.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "92758ad6077e4c76a6cadbce5005f666df70d4f13b19976b1a8062eef880040f"
+dependencies = [
+ "base64",
+ "blowfish",
+ "getrandom 0.3.1",
+ "subtle",
+ "zeroize",
+]
+
[[package]]
name = "bigdecimal"
version = "0.4.7"
@@ -422,9 +470,20 @@ dependencies = [
]
[[package]]
-name = "bookkeeper"
-version = "0.1.3"
+name = "blowfish"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7"
dependencies = [
+ "byteorder",
+ "cipher",
+]
+
+[[package]]
+name = "bookkeeper"
+version = "0.2.0"
+dependencies = [
+ "bcrypt",
"chrono",
"entity",
"migration",
@@ -574,6 +633,16 @@ dependencies = [
"phf_codegen",
]
+[[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 = "clap"
version = "4.5.23"
@@ -641,7 +710,13 @@ version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
dependencies = [
+ "aes-gcm",
+ "base64",
+ "hkdf",
"percent-encoding",
+ "rand",
+ "sha2",
+ "subtle",
"time",
"version_check",
]
@@ -726,9 +801,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 = "darling"
version = "0.20.10"
@@ -1169,7 +1254,29 @@ checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7"
dependencies = [
"cfg-if",
"libc",
- "wasi",
+ "wasi 0.11.0+wasi-snapshot-preview1",
+]
+
+[[package]]
+name = "getrandom"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "43a49c392881ce6d5c3b8cb70f98717b7c07aabbdff06687b9030dbfbe2725f8"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "wasi 0.13.3+wasi-0.2.2",
+ "windows-targets 0.52.6",
+]
+
+[[package]]
+name = "ghash"
+version = "0.5.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1"
+dependencies = [
+ "opaque-debug",
+ "polyval",
]
[[package]]
@@ -1640,6 +1747,15 @@ dependencies = [
"libc",
]
+[[package]]
+name = "inout"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01"
+dependencies = [
+ "generic-array",
+]
+
[[package]]
name = "is-terminal"
version = "0.4.13"
@@ -1884,7 +2000,7 @@ checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
dependencies = [
"libc",
"log",
- "wasi",
+ "wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.48.0",
]
@@ -1895,7 +2011,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd"
dependencies = [
"libc",
- "wasi",
+ "wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys 0.52.0",
]
@@ -2054,6 +2170,12 @@ version = "1.20.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775"
+[[package]]
+name = "opaque-debug"
+version = "0.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
+
[[package]]
name = "ordered-float"
version = "3.9.2"
@@ -2324,6 +2446,18 @@ dependencies = [
"windows-sys 0.59.0",
]
+[[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 = "powerfmt"
version = "0.2.0"
@@ -2467,7 +2601,7 @@ version = "0.6.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
dependencies = [
- "getrandom",
+ "getrandom 0.2.15",
]
[[package]]
@@ -2560,7 +2694,7 @@ checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d"
dependencies = [
"cc",
"cfg-if",
- "getrandom",
+ "getrandom 0.2.15",
"libc",
"spin",
"untrusted",
@@ -3934,6 +4068,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 = "untrusted"
version = "0.9.0"
@@ -4033,6 +4177,15 @@ version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423"
+[[package]]
+name = "wasi"
+version = "0.13.3+wasi-0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "26816d2e1a4a36a2940b96c5296ce403917633dff8f3440e9b236ed6f6bacad2"
+dependencies = [
+ "wit-bindgen-rt",
+]
+
[[package]]
name = "wasite"
version = "0.1.0"
@@ -4341,6 +4494,15 @@ dependencies = [
"memchr",
]
+[[package]]
+name = "wit-bindgen-rt"
+version = "0.33.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3268f3d866458b787f390cf61f4bbb563b922d091359f9608842999eaee3943c"
+dependencies = [
+ "bitflags 2.6.0",
+]
+
[[package]]
name = "write16"
version = "1.0.0"
diff --git a/bookkeeper/Cargo.toml b/bookkeeper/Cargo.toml
index a79b7d2..62ed8b6 100644
--- a/bookkeeper/Cargo.toml
+++ b/bookkeeper/Cargo.toml
@@ -1,11 +1,11 @@
[package]
name = "bookkeeper"
-version = "0.1.3"
+version = "0.2.0"
edition = "2021"
[dependencies]
chrono = "0.4.39"
-rocket = "0.5.1"
+rocket = { version = "0.5.1", features = ["secrets"] }
rocket_dyn_templates = { version = "0.2.0", features = ["tera", "minijinja"]}
migration = { path = "migration" }
entity ={ path = "entity"}
@@ -13,6 +13,7 @@ rust_decimal = "1.36.0"
sea-orm-rocket = "0.5.4"
serde_json = "1.0.134"
serde = { version = "1.0.216", features = ["derive"] }
+bcrypt = "0.17.0"
[workspace]
members = [".", "entity", "migration"]
diff --git a/bookkeeper/Rocket.toml b/bookkeeper/Rocket.toml
index adae926..03b8ecc 100644
--- a/bookkeeper/Rocket.toml
+++ b/bookkeeper/Rocket.toml
@@ -2,6 +2,7 @@
[default]
address = "0.0.0.0"
limits = { form = "64 kB", json = "1 MiB" }
+secret_key = "b613679a0814d9ec772f95d778c35fc5ff1697c493715653c6c712144292c5ad"
[default.databases.bookkeeper]
url = "sqlite://bookkeeper.db"
@@ -20,7 +21,6 @@ port = 9001
[release]
port = 9999
ip_header = false
-secret_key = "hPrYyЭRiMyµ5sBB1π+CMæ1køFsåqKvBiQJxBVHQk="
[global.templates]
dir = "templates"
\ No newline at end of file
diff --git a/bookkeeper/src/main.rs b/bookkeeper/src/main.rs
index 2df34e5..bf9c519 100644
--- a/bookkeeper/src/main.rs
+++ b/bookkeeper/src/main.rs
@@ -1,6 +1,7 @@
#[macro_use]
extern crate rocket;
+use bcrypt::{hash_with_salt, DEFAULT_COST};
use chrono::prelude::*;
use chrono::NaiveDate;
use chrono::TimeDelta;
@@ -10,8 +11,10 @@ use migration::MigratorTrait;
use rocket::fairing::{self, AdHoc};
use rocket::form::Form;
use rocket::fs::FileServer;
-use rocket::http::Status;
-use rocket::response::{Flash, Redirect};
+use rocket::http::{Cookie, CookieJar, Status};
+use rocket::request::{FromRequest, Outcome, Request};
+use rocket::response::Redirect;
+use rocket::State;
use rocket::{Build, Rocket};
use rocket_dyn_templates::{context, Engines, Template};
use rust_decimal::Decimal;
@@ -20,8 +23,121 @@ use sea_orm::ActiveValue::Set;
use sea_orm::{ActiveModelTrait, ColumnTrait, ConnectOptions, DatabaseConnection, EntityTrait};
use sea_orm_rocket::{rocket::figment::Figment, Config, Connection, Database};
use serde::{Deserialize, Serialize};
+use std::collections::HashMap;
use std::str::FromStr;
-use std::time::Duration;
+use std::sync::Mutex;
+use std::time::{Duration, SystemTime};
+
+// static CORRECT_PASSWORD: &str = "Admin!23-ca$hc0w";
+static CORRECT_HASH: &str = "$2y$12$Q1TvUkuzKBThOlfOUjOuKexKJCDtVvz2RznuUbQntD1TTYLo4PWwy";
+
+/// 记录单个 IP 的错误登录记录和封禁截止时间
+#[derive(Default)]
+struct LoginAttempt {
+ /// 记录每次错误的时间(只保留5分钟内的记录)
+ failed_times: Vec,
+ /// 封禁截止时间,如果未封禁则为 None
+ banned_until: Option,
+}
+
+/// 全局状态,记录每个 IP 的登录尝试情况
+struct LoginState {
+ attempts: Mutex>,
+}
+
+impl LoginState {
+ fn new() -> Self {
+ LoginState {
+ attempts: Mutex::new(HashMap::new()),
+ }
+ }
+}
+
+/// 登录表单数据
+#[derive(FromForm)]
+struct LoginForm {
+ password: String,
+}
+
+/// 自定义请求守卫,用于提取客户端 IP 地址
+struct ClientIP(String);
+
+#[rocket::async_trait]
+impl<'r> FromRequest<'r> for ClientIP {
+ type Error = ();
+
+ async fn from_request(req: &'r Request<'_>) -> Outcome {
+ if let Some(ip) = req.client_ip() {
+ Outcome::Success(ClientIP(ip.to_string()))
+ } else {
+ Outcome::Error((Status::BadRequest, ()))
+ }
+ }
+}
+
+/// GET /login 显示登录页面
+#[get("/login")]
+fn login_page() -> Template {
+ let context = HashMap::::new();
+ Template::render("login", &context)
+}
+
+/// POST /login 处理登录请求
+#[post("/login", data = "")]
+fn login_post(
+ login_form: Form,
+ cookies: &CookieJar<'_>,
+ state: &State,
+ client_ip: ClientIP,
+) -> Result {
+ let ip = client_ip.0;
+ let now = SystemTime::now();
+ let mut attempts = state.attempts.lock().unwrap();
+ let entry = attempts
+ .entry(ip.clone())
+ .or_insert(LoginAttempt::default());
+
+ // 检查该 IP 是否处于封禁状态
+ if let Some(banned_until) = entry.banned_until {
+ if now < banned_until {
+ let mut context = HashMap::new();
+ context.insert(
+ "error".to_string(),
+ "您的IP已被禁止访问,请稍后再试。".to_string(),
+ );
+ return Err(Template::render("login", &context));
+ } else {
+ // 封禁时间已过,清除记录
+ entry.banned_until = None;
+ entry.failed_times.clear();
+ }
+ }
+
+ // 仅保留过去5分钟内的错误记录
+ entry.failed_times.retain(|&t| {
+ now.duration_since(t).unwrap_or(Duration::new(0, 0)) < Duration::from_secs(300)
+ });
+
+ let pass_hash = hash_with_salt(&login_form.password, DEFAULT_COST, *b"KuqZl505cBxPZT02")
+ .unwrap()
+ .to_string();
+ // 使用 bcrypt 对输入的密码进行验证
+ if pass_hash == CORRECT_HASH {
+ // 密码正确:清除错误记录,设置登录 Cookie
+ entry.failed_times.clear();
+ cookies.add_private(Cookie::new("auth", pass_hash));
+ Ok(Redirect::to("/"))
+ } else {
+ // 密码错误:记录错误时间,判断是否需要封禁IP
+ entry.failed_times.push(now);
+ if entry.failed_times.len() > 5 {
+ entry.banned_until = Some(now + Duration::from_secs(7200));
+ }
+ let mut context = HashMap::new();
+ context.insert("error".to_string(), "密码错误!".to_string());
+ Err(Template::render("login", &context))
+ }
+}
#[derive(sea_orm_rocket::Database, Debug)]
#[database("bookkeeper")]
@@ -57,13 +173,28 @@ impl sea_orm_rocket::Pool for SeaOrmPool {
}
}
+fn verify(cookies: &CookieJar<'_>) -> bool {
+ if let Some(c) = cookies.get_private("auth") {
+ // println!("{} ? {}", c.value_trimmed(), )
+ if c.value_trimmed() == CORRECT_HASH {
+ return true;
+ }
+ }
+ false
+}
+
#[get("/?&&")]
async fn index(
conn: Connection<'_, Db>,
last: Option,
name: Option<&str>,
is_done: Option,
-) -> Template {
+ cookies: &CookieJar<'_>,
+) -> Result {
+ if !verify(cookies) {
+ return Err(Redirect::to("/login"));
+ }
+
let mut partial = TE::find();
if let Some(last) = last {
@@ -103,11 +234,11 @@ async fn index(
net_gain += ng;
}
}
-
+
let pct = gain / bought;
let net_pct = net_gain / net_bought;
- Template::render(
+ Ok(Template::render(
"index",
context! {
rows: rows,
@@ -117,7 +248,7 @@ async fn index(
pct: pct,
net_pct: net_pct,
},
- )
+ ))
}
#[derive(FromForm, Debug)]
@@ -150,7 +281,14 @@ struct SplitPiece {
}
#[get("/tx/")]
-async fn get_tx(conn: Connection<'_, Db>, id: i32) -> Result {
+async fn get_tx(
+ conn: Connection<'_, Db>,
+ id: i32,
+ cookies: &CookieJar<'_>,
+) -> Result {
+ if !verify(cookies) {
+ return Err(Status::Forbidden);
+ }
let row = TE::find_by_id(id).one(conn.into_inner()).await.unwrap();
match row {
None => Err(Status::NotFound),
@@ -169,7 +307,11 @@ async fn post_tx(
data: Form>,
conn: Connection<'_, Db>,
id: i32,
+ cookies: &CookieJar<'_>,
) -> Result {
+ if !verify(cookies) {
+ return Err(Status::Forbidden);
+ }
let mut record = translate(data);
record.id = Set(id);
match record.update(conn.into_inner()).await {
@@ -179,7 +321,14 @@ async fn post_tx(
}
#[delete("/tx/")]
-async fn delete_tx(conn: Connection<'_, Db>, id: i32) -> Result {
+async fn delete_tx(
+ conn: Connection<'_, Db>,
+ id: i32,
+ cookies: &CookieJar<'_>,
+) -> Result {
+ if !verify(cookies) {
+ return Err(Status::Forbidden);
+ }
match TE::delete_by_id(id).exec(conn.into_inner()).await {
Err(_) => Err(Status::InternalServerError),
Ok(res) => {
@@ -193,23 +342,34 @@ async fn delete_tx(conn: Connection<'_, Db>, id: i32) -> Result Template {
- Template::render(
+async fn get_add(cookies: &CookieJar<'_>) -> Result {
+ if !verify(cookies) {
+ return Err(Redirect::to("/login"));
+ }
+ Ok(Template::render(
"tx",
context! {
r: (),
target: "/add",
},
- )
+ ))
}
#[post("/add", data = "")]
-async fn add(trans: Form>, conn: Connection<'_, Db>) -> Flash {
+async fn add(
+ trans: Form>,
+ conn: Connection<'_, Db>,
+ cookies: &CookieJar<'_>,
+) -> Result {
+ if !verify(cookies) {
+ return Err(Status::Forbidden);
+ }
+
let record = translate(trans);
record.insert(conn.into_inner()).await.unwrap();
- Flash::success(Redirect::to("/"), "OK!")
+ Ok(Redirect::to("/"))
}
/// translate from form to active model of transaction
@@ -268,6 +428,7 @@ async fn rocket() -> _ {
rocket::build()
.attach(Db::init())
.attach(AdHoc::try_on_ignite("Migrations", run_migrations))
+ .manage(LoginState::new())
.attach(Template::custom(|engines: &mut Engines| {
engines
.minijinja
@@ -281,7 +442,7 @@ async fn rocket() -> _ {
}))
.mount(
"/",
- routes![index, add, get_add, get_tx, post_tx, delete_tx],
+ routes![index, add, get_add, get_tx, post_tx, delete_tx, login_page, login_post],
)
.mount("/static", FileServer::from("static"))
}
diff --git a/bookkeeper/templates/login.html.j2 b/bookkeeper/templates/login.html.j2
new file mode 100644
index 0000000..a5c04d9
--- /dev/null
+++ b/bookkeeper/templates/login.html.j2
@@ -0,0 +1,17 @@
+
+
+
+
+ 登录
+
+
+登录页面
+{% if error %}
+{{ error }}
+{% endif %}
+
+
+