Compare commits

..

1 Commits

Author SHA1 Message Date
realaravinth
469ede1917 feat: WIP docker compose from source configuration 2022-05-10 22:41:36 +05:30
108 changed files with 3017 additions and 4609 deletions

View File

@@ -1 +0,0 @@
export POSTGRES_DATABASE_URL="postgres://postgres:password@localhost:5432/postgres"

View File

@@ -6,7 +6,6 @@ on:
push: push:
branches: branches:
- master - master
- db-abstract
jobs: jobs:
fmt: fmt:

View File

@@ -6,7 +6,6 @@ on:
push: push:
branches: branches:
- master - master
- db-abstract
jobs: jobs:
build_and_test: build_and_test:
@@ -52,12 +51,6 @@ jobs:
target target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: load env
run: |
source .env_sample \
&& echo "POSTGRES_DATABASE_URL=$POSTGRES_DATABASE_URL" >> $GITHUB_ENV
- uses: actions/setup-node@v2 - uses: actions/setup-node@v2
with: with:
node-version: "16.x" node-version: "16.x"
@@ -81,19 +74,18 @@ jobs:
- name: Run migrations - name: Run migrations
run: make migrate run: make migrate
env: env:
POSTGRES_DATABASE_URL: "${{ env.POSTGRES_DATABASE_URL }}" DATABASE_URL: postgres://postgres:password@localhost:5432/postgres
- name: build frontend - name: build frontend
run: make frontend run: make frontend
- name: Generate coverage file - name: Generate coverage file
if: github.event_name == 'pull_request' if: (github.ref == 'refs/heads/master' || github.event_name == 'pull_request')
#if: (github.ref == 'refs/heads/master' || github.event_name == 'pull_request')
uses: actions-rs/tarpaulin@v0.1 uses: actions-rs/tarpaulin@v0.1
with: with:
args: "-t 1200" args: "-t 1200"
env: env:
POSTGRES_DATABASE_URL: "${{ env.POSTGRES_DATABASE_URL }}" DATABASE_URL: postgres://postgres:password@localhost:5432/postgres
# GIT_HASH is dummy value. I guess build.rs is skipped in tarpaulin # GIT_HASH is dummy value. I guess build.rs is skipped in tarpaulin
# execution so this value is required for preventing meta tests from # execution so this value is required for preventing meta tests from
# panicking # panicking
@@ -102,5 +94,5 @@ jobs:
COMPILED_DATE: "2021-07-21" COMPILED_DATE: "2021-07-21"
- name: Upload to Codecov - name: Upload to Codecov
if: github.event_name == 'pull_request' if: (github.ref == 'refs/heads/master' || github.event_name == 'pull_request')
uses: codecov/codecov-action@v2 uses: codecov/codecov-action@v2

View File

@@ -8,7 +8,6 @@ on:
push: push:
branches: branches:
- master - master
- db-abstract
jobs: jobs:
build_and_test: build_and_test:
@@ -55,12 +54,6 @@ jobs:
target target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: load env
run: |
source .env_sample \
&& echo "POSTGRES_DATABASE_URL=$POSTGRES_DATABASE_URL" >> $GITHUB_ENV
- uses: actions/setup-node@v2 - uses: actions/setup-node@v2
with: with:
node-version: '16.x' node-version: '16.x'
@@ -78,12 +71,12 @@ jobs:
- name: Run migrations - name: Run migrations
run: make migrate run: make migrate
env: env:
POSTGRES_DATABASE_URL: "${{ env.POSTGRES_DATABASE_URL }}" DATABASE_URL: postgres://postgres:password@localhost:5432/postgres
- name: build - name: build
run: make run: make
env: env:
POSTGRES_DATABASE_URL: "${{ env.POSTGRES_DATABASE_URL }}" DATABASE_URL: postgres://postgres:password@localhost:5432/postgres
# - name: build frontend # - name: build frontend
# run: make frontend # run: make frontend
@@ -94,13 +87,13 @@ jobs:
- name: run tests - name: run tests
run: make test run: make test
env: env:
POSTGRES_DATABASE_URL: "${{ env.POSTGRES_DATABASE_URL }}" DATABASE_URL: postgres://postgres:password@localhost:5432/postgres
- name: generate documentation - name: generate documentation
if: matrix.version == 'stable' && (github.repository == 'mCaptcha/mCaptcha') if: matrix.version == 'stable' && (github.repository == 'mCaptcha/mCaptcha')
run: make doc run: make doc
env: env:
POSTGRES_DATABASE_URL: "${{ env.POSTGRES_DATABASE_URL }}" DATABASE_URL: postgres://postgres:password@localhost:5432/postgres
GIT_HASH: 8e77345f1597e40c2e266cb4e6dee74888918a61 # dummy value GIT_HASH: 8e77345f1597e40c2e266cb4e6dee74888918a61 # dummy value
COMPILED_DATE: "2021-07-21" COMPILED_DATE: "2021-07-21"

70
Cargo.lock generated
View File

@@ -835,30 +835,6 @@ version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57" checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57"
[[package]]
name = "db-core"
version = "0.1.0"
dependencies = [
"async-trait",
"libmcaptcha",
"serde 1.0.137",
"serde_json",
"thiserror",
"url",
]
[[package]]
name = "db-sqlx-postgres"
version = "0.1.0"
dependencies = [
"actix-rt",
"async-trait",
"db-core",
"futures",
"sqlx",
"url",
]
[[package]] [[package]]
name = "derive_builder" name = "derive_builder"
version = "0.10.2" version = "0.10.2"
@@ -1650,12 +1626,9 @@ dependencies = [
"actix-web", "actix-web",
"actix-web-codegen 4.0.0 (git+https://github.com/realaravinth/actix-web)", "actix-web-codegen 4.0.0 (git+https://github.com/realaravinth/actix-web)",
"argon2-creds", "argon2-creds",
"async-trait",
"awc", "awc",
"cache-buster", "cache-buster",
"config", "config",
"db-core",
"db-sqlx-postgres",
"derive_builder 0.11.1", "derive_builder 0.11.1",
"derive_more", "derive_more",
"futures", "futures",
@@ -1728,14 +1701,25 @@ dependencies = [
[[package]] [[package]]
name = "mio" name = "mio"
version = "0.8.3" version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "713d550d9b44d89174e066b7a6217ae06234c10cb47819a88290d2b353c31799" checksum = "52da4364ffb0e4fe33a9841a98a3f3014fb964045ce4f7a45a398243c8d6b0c9"
dependencies = [ dependencies = [
"libc", "libc",
"log", "log",
"miow",
"ntapi",
"wasi 0.11.0+wasi-snapshot-preview1", "wasi 0.11.0+wasi-snapshot-preview1",
"windows-sys", "winapi",
]
[[package]]
name = "miow"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9f1c5b025cda876f66ef43a113f91ebc9f4ccef34843000e0adf6ebbab84e21"
dependencies = [
"winapi",
] ]
[[package]] [[package]]
@@ -1783,6 +1767,15 @@ dependencies = [
"minimal-lexical", "minimal-lexical",
] ]
[[package]]
name = "ntapi"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c28774a7fd2fbb4f0babd8237ce554b73af68021b5f695a3cebd6c59bac0980f"
dependencies = [
"winapi",
]
[[package]] [[package]]
name = "num" name = "num"
version = "0.4.0" version = "0.4.0"
@@ -2873,9 +2866,9 @@ checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.94" version = "1.0.92"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a07e33e919ebcd69113d5be0e4d70c5707004ff45188910106854f38b960df4a" checksum = "7ff7c592601f11445996a06f8ad0c27f094a58857c2f89e97974ab9235b92c52"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -3009,9 +3002,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.18.2" version = "1.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4903bf0427cf68dddd5aa6a93220756f8be0c34fcfa9f5e6191e103e15a31395" checksum = "dce653fb475565de9f6fb0614b28bca8df2c430c0cf84bcd9c843f15de5414cc"
dependencies = [ dependencies = [
"bytes", "bytes",
"libc", "libc",
@@ -3202,7 +3195,6 @@ dependencies = [
"idna", "idna",
"matches", "matches",
"percent-encoding", "percent-encoding",
"serde 1.0.137",
] ]
[[package]] [[package]]
@@ -3472,18 +3464,18 @@ dependencies = [
[[package]] [[package]]
name = "zstd" name = "zstd"
version = "0.10.2+zstd.1.5.2" version = "0.10.0+zstd.1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f4a6bd64f22b5e3e94b4e238669ff9f10815c27a5180108b849d24174a83847" checksum = "3b1365becbe415f3f0fcd024e2f7b45bacfb5bdd055f0dc113571394114e7bdd"
dependencies = [ dependencies = [
"zstd-safe", "zstd-safe",
] ]
[[package]] [[package]]
name = "zstd-safe" name = "zstd-safe"
version = "4.1.6+zstd.1.5.2" version = "4.1.4+zstd.1.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94b61c51bb270702d6167b8ce67340d2754b088d0c091b06e593aa772c3ee9bb" checksum = "2f7cd17c9af1a4d6c24beb1cc54b17e2ef7b593dc92f19e9d9acad8b182bbaee"
dependencies = [ dependencies = [
"libc", "libc",
"zstd-sys", "zstd-sys",

View File

@@ -13,15 +13,14 @@ build = "build.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[workspace]
exclude = ["db/db-migrations", "utils/cache-bust"]
memebers = [".", "db/db-core", "db/db-sqlx-postgres"]
[[bin]] [[bin]]
name = "mcaptcha" name = "mcaptcha"
path = "./src/main.rs" path = "./src/main.rs"
[[bin]]
name = "tests-migrate"
path = "./src/tests-migrate.rs"
[dependencies] [dependencies]
actix-web = "4.0.1" actix-web = "4.0.1"
actix = "0.13" actix = "0.13"
@@ -30,7 +29,7 @@ actix-http = "3.0.4"
actix-rt = "2" actix-rt = "2"
actix-cors = "0.6.1" actix-cors = "0.6.1"
actix-service = "2.0.0" actix-service = "2.0.0"
async-trait = "0.1.51" #my-codegen = {version="0.5.0-beta.5", package = "actix-web-codegen", git ="https://github.com/realaravinth/actix-web"}
mime_guess = "2.0.3" mime_guess = "2.0.3"
rust-embed = "6.4.0" rust-embed = "6.4.0"
cache-buster = { git = "https://github.com/realaravinth/cache-buster" } cache-buster = { git = "https://github.com/realaravinth/cache-buster" }
@@ -77,13 +76,6 @@ lettre = { version = "0.10.0-rc.3", features = [
openssl = { version = "0.10.29", features = ["vendored"] } openssl = { version = "0.10.29", features = ["vendored"] }
[dependencies.db-core]
path = "./db/db-core"
[dependencies.db-sqlx-postgres]
path = "./db/db-sqlx-postgres"
[dependencies.my-codegen] [dependencies.my-codegen]
git = "https://github.com/realaravinth/actix-web" git = "https://github.com/realaravinth/actix-web"
package = "actix-web-codegen" package = "actix-web-codegen"
@@ -95,6 +87,8 @@ features = ["actix_identity_backend"]
[build-dependencies] [build-dependencies]
serde_json = "1" serde_json = "1"
cache-buster = { version = "0.2.0", git = "https://github.com/realaravinth/cache-buster" }
mime = "0.3.16"
sqlx = { version = "0.5.13", features = [ "runtime-actix-rustls", "postgres", "time", "offline" ] } sqlx = { version = "0.5.13", features = [ "runtime-actix-rustls", "postgres", "time", "offline" ] }
[dev-dependencies] [dev-dependencies]

View File

@@ -7,33 +7,15 @@ define frontend_env ## install frontend deps
cd docs/openapi && yarn install cd docs/openapi && yarn install
endef endef
define cache_bust ## run cache_busting program
cd utils/cache-bust && cargo run
endef
default: frontend ## Build app in debug mode default: frontend ## Build app in debug mode
$(call cache_bust)
cargo build cargo build
check: ## Check for syntax errors on all workspaces
cargo check --workspace --tests --all-features
cd utils/cache-bust && cargo check --tests --all-features
cd db/db-migrations && cargo check --tests --all-features
cd db/db-sqlx-postgres &&\
DATABASE_URL=${POSTGRES_DATABASE_URL}\
cargo check
cd db/db-core/ && cargo check
cache-bust: ## Run cache buster on static assets
$(call cache_bust)
clean: ## Delete build artifacts clean: ## Delete build artifacts
@cargo clean @cargo clean
@yarn cache clean @yarn cache clean
@-rm $(CLEAN_UP) @-rm $(CLEAN_UP)
coverage: migrate ## Generate code coverage report in HTML format coverage: migrate ## Generate code coverage report in HTML format
$(call cache_bust)
cargo tarpaulin -t 1200 --out Html cargo tarpaulin -t 1200 --out Html
doc: ## Generate documentation doc: ## Generate documentation
@@ -84,43 +66,19 @@ lint: ## Lint codebase
cd $(OPENAPI)&& yarn test cd $(OPENAPI)&& yarn test
migrate: ## Run database migrations migrate: ## Run database migrations
cd db/db-migrations/ && \ cargo run --bin tests-migrate
DATABASE_URL=${POSTGRES_DATABASE_URL} cargo run
release: frontend ## Build app with release optimizations release: frontend ## Build app with release optimizations
$(call cache_bust)
cargo build --release cargo build --release
run: frontend ## Run app in debug mode run: frontend ## Run app in debug mode
cargo run cargo run
sqlx-offline-data: ## prepare sqlx offline data
cargo sqlx prepare --database-url=${POSTGRES_DATABASE_URL} -- --bin mcaptcha \
--all-features
cd db/db-migrations && cargo sqlx prepare \
--database-url=${POSTGRES_DATABASE_URL} -- --bin db-migrations \
--all-features
cd db/db-sqlx-postgres && cargo sqlx prepare \
--database-url=${POSTGRES_DATABASE_URL} -- \
--all-features
# cd db/db-sqlx-sqlite/ \
# && DATABASE_URL=${SQLITE_DATABASE_URL} cargo sqlx prepare
test-db: ## run tests on database
cd db/db-sqlx-postgres &&\
DATABASE_URL=${POSTGRES_DATABASE_URL}\
cargo test --no-fail-fast
test: frontend-test frontend ## Run all available tests test: frontend-test frontend ## Run all available tests
$(call cache_bust) ./scripts/tests.sh
cd db/db-sqlx-postgres &&\ # cargo test --all-features --no-fail-fast
DATABASE_URL=${POSTGRES_DATABASE_URL}\
cargo test --no-fail-fast
cargo test --all-features --no-fail-fast
# ./scripts/tests.sh
xml-test-coverage: migrate ## Generate code coverage report in XML format xml-test-coverage: migrate ## Generate code coverage report in XML format
$(call cache_bust)
cargo tarpaulin -t 1200 --out Xml cargo tarpaulin -t 1200 --out Xml
help: ## Prints help for targets with comments help: ## Prints help for targets with comments

View File

@@ -103,16 +103,9 @@ development, database frequently wiped).
Clone the repo and run the following from the root of the repo: Clone the repo and run the following from the root of the repo:
```bash ```bash
git clone https://github.com/mCaptcha/mCaptcha.git $ docker-compose -d up
docker-compose -d up
``` ```
After the containers are up, visit [http://localhost:7000](http://localhost:7000) and login with the default credentials:
- username: aaronsw
- password: password
It takes a while to build the image so please be patient :) It takes a while to build the image so please be patient :)
See [DEPLOYMENT.md](./docs/DEPLOYMET.md) detailed alternate deployment See [DEPLOYMENT.md](./docs/DEPLOYMET.md) detailed alternate deployment

View File

@@ -16,6 +16,7 @@
*/ */
use std::process::Command; use std::process::Command;
use cache_buster::{BusterBuilder, NoHashCategory};
use sqlx::types::time::OffsetDateTime; use sqlx::types::time::OffsetDateTime;
fn main() { fn main() {
@@ -29,4 +30,32 @@ fn main() {
let now = OffsetDateTime::now_utc().format("%y-%m-%d"); let now = OffsetDateTime::now_utc().format("%y-%m-%d");
println!("cargo:rustc-env=COMPILED_DATE={}", &now); println!("cargo:rustc-env=COMPILED_DATE={}", &now);
cache_bust();
}
fn cache_bust() {
// until APPLICATION_WASM gets added to mime crate
// PR: https://github.com/hyperium/mime/pull/138
// let types = vec![
// mime::IMAGE_PNG,
// mime::IMAGE_SVG,
// mime::IMAGE_JPEG,
// mime::IMAGE_GIF,
// mime::APPLICATION_JAVASCRIPT,
// mime::TEXT_CSS,
// ];
println!("cargo:rerun-if-changed=static/cache");
let no_hash = vec![NoHashCategory::FileExtentions(vec!["wasm"])];
let config = BusterBuilder::default()
.source("./static/cache/")
.result("./assets")
.no_hash(no_hash)
.follow_links(true)
.build()
.unwrap();
config.process().unwrap();
} }

View File

@@ -28,7 +28,6 @@ salt = "asdl;kjfhjawehfpa;osdkjasdvjaksndfpoanjdfainsdfaijdsfajlkjdsaf;ajsdfwero
# garbage collection period to manage mCaptcha system # garbage collection period to manage mCaptcha system
# leave untouched if you don't know what you are doing # leave untouched if you don't know what you are doing
gc = 30 gc = 30
enable_stats = true
[captcha.default_difficulty_strategy] [captcha.default_difficulty_strategy]
avg_traffic_difficulty = 50000 # almost instant solution avg_traffic_difficulty = 50000 # almost instant solution

View File

@@ -1,2 +0,0 @@
/target
/Cargo.lock

View File

@@ -1,23 +0,0 @@
[package]
name = "db-core"
version = "0.1.0"
edition = "2021"
homepage = "https://mcaptcha.org"
repository = "https://github.com/mCaptcha/mCaptcha"
documentation = "https://mcaptcha.org/docs/"
license = "AGPLv3 or later version"
authors = ["realaravinth <realaravinth@batsense.net>"]
[dependencies]
async-trait = "0.1.51"
thiserror = "1.0.30"
serde = { version = "1", features = ["derive"]}
url = { version = "2.2.2", features = ["serde"] }
libmcaptcha = { branch = "master", git = "https://github.com/mCaptcha/libmcaptcha", features = ["minimal"], default-features = false }
[features]
default = []
test = []
[dev-dependencies]
serde_json = "1"

View File

@@ -1,61 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
//! represents all the ways a trait can fail using this crate
use std::error::Error as StdError;
//use derive_more::{error, Error as DeriveError};
use thiserror::Error;
/// Error data structure grouping various error subtypes
#[derive(Debug, Error)]
pub enum DBError {
/// errors that are specific to a database implementation
#[error("{0}")]
DBError(#[source] BoxDynError),
/// Username is taken
#[error("Username is taken")]
UsernameTaken,
/// Email is taken
#[error("Email is taken")]
EmailTaken,
/// Secret is taken
#[error("Secret is taken")]
SecretTaken,
/// Captcha key is taken
#[error("Captcha key is taken")]
CaptchaKeyTaken,
/// Account not found
#[error("Account not found")]
AccountNotFound,
/// Captcha not found
#[error("Captcha not found")]
CaptchaNotFound,
/// Traffic pattern not found
#[error("Traffic pattern not found")]
TrafficPatternNotFound,
/// Notification not found
#[error("Notification not found")]
NotificationNotFound,
}
/// Convenience type alias for grouping driver-specific errors
pub type BoxDynError = Box<dyn StdError + 'static + Send + Sync>;
/// Generic result data structure
pub type DBResult<V> = std::result::Result<V, DBError>;

View File

@@ -1,352 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#![warn(missing_docs)]
//! # `mCaptcha` database operations
//!
//! Traits and datastructures used in mCaptcha to interact with database.
//!
//! To use an unsupported database with mCaptcha, traits present within this crate should be
//! implemented.
//!
//!
//! ## Organisation
//!
//! Database functionallity is divided accross various modules:
//!
//! - [errors](crate::auth): error data structures used in this crate
//! - [ops](crate::ops): meta operations like connection pool creation, migrations and getting
//! connection from pool
use serde::{Deserialize, Serialize};
pub use libmcaptcha::defense::Level;
pub mod errors;
pub mod ops;
#[cfg(feature = "test")]
pub mod tests;
use dev::*;
pub use ops::GetConnection;
pub mod prelude {
//! useful imports for users working with a supported database
pub use super::errors::*;
pub use super::ops::*;
pub use super::*;
}
pub mod dev {
//! useful imports for supporting a new database
pub use super::prelude::*;
pub use async_trait::async_trait;
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
/// Data required to register a new user
pub struct Register<'a> {
/// username of new user
pub username: &'a str,
/// secret of new user
pub secret: &'a str,
/// hashed password of new use
pub hash: &'a str,
/// Optionally, email of new use
pub email: Option<&'a str>,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
/// data required to update them email of a user
pub struct UpdateEmail<'a> {
/// username of the user
pub username: &'a str,
/// new email address of the user
pub new_email: &'a str,
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
/// types of credentials used as identifiers during login
pub enum Login<'a> {
/// username as login
Username(&'a str),
/// email as login
Email(&'a str),
}
#[derive(Clone, Debug, Deserialize, Serialize, PartialEq)]
/// type encapsulating username and hashed password of a user
pub struct NameHash {
/// username
pub username: String,
/// hashed password
pub hash: String,
}
#[async_trait]
/// mCaptcha's database requirements. To implement support for $Database, kindly implement this
/// trait.
pub trait MCDatabase: std::marker::Send + std::marker::Sync + CloneSPDatabase {
/// ping DB
async fn ping(&self) -> bool;
/// register a new user
async fn register(&self, p: &Register) -> DBResult<()>;
/// delete a user
async fn delete_user(&self, username: &str) -> DBResult<()>;
/// check if username exists
async fn username_exists(&self, username: &str) -> DBResult<bool>;
/// get user email
async fn get_email(&self, username: &str) -> DBResult<Option<String>>;
/// check if email exists
async fn email_exists(&self, email: &str) -> DBResult<bool>;
/// update a user's email
async fn update_email(&self, p: &UpdateEmail) -> DBResult<()>;
/// get a user's password
async fn get_password(&self, l: &Login) -> DBResult<NameHash>;
/// update user's password
async fn update_password(&self, p: &NameHash) -> DBResult<()>;
/// update username
async fn update_username(&self, current: &str, new: &str) -> DBResult<()>;
/// get a user's secret
async fn get_secret(&self, username: &str) -> DBResult<Secret>;
/// update a user's secret
async fn update_secret(&self, username: &str, secret: &str) -> DBResult<()>;
/// create new captcha
async fn create_captcha(&self, username: &str, p: &CreateCaptcha) -> DBResult<()>;
/// Get captcha config
async fn get_captcha_config(&self, username: &str, key: &str) -> DBResult<Captcha>;
/// Get all captchas belonging to user
async fn get_all_user_captchas(&self, username: &str) -> DBResult<Vec<Captcha>>;
/// update captcha metadata; doesn't change captcha key
async fn update_captcha_metadata(
&self,
username: &str,
p: &CreateCaptcha,
) -> DBResult<()>;
/// update captcha key; doesn't change metadata
async fn update_captcha_key(
&self,
username: &str,
old_key: &str,
new_key: &str,
) -> DBResult<()>;
/// Add levels to captcha
async fn add_captcha_levels(
&self,
username: &str,
captcha_key: &str,
levels: &[Level],
) -> DBResult<()>;
/// check if captcha exists
async fn captcha_exists(
&self,
username: Option<&str>,
captcha_key: &str,
) -> DBResult<bool>;
/// Delete all levels of a captcha
async fn delete_captcha_levels(
&self,
username: &str,
captcha_key: &str,
) -> DBResult<()>;
/// Delete captcha
async fn delete_captcha(&self, username: &str, captcha_key: &str) -> DBResult<()>;
/// Get captcha levels
async fn get_captcha_levels(
&self,
username: Option<&str>,
captcha_key: &str,
) -> DBResult<Vec<Level>>;
/// Get captcha's cooldown period
async fn get_captcha_cooldown(&self, captcha_key: &str) -> DBResult<i32>;
/// Add traffic configuration
async fn add_traffic_pattern(
&self,
username: &str,
captcha_key: &str,
pattern: &TrafficPattern,
) -> DBResult<()>;
/// Get traffic configuration
async fn get_traffic_pattern(
&self,
username: &str,
captcha_key: &str,
) -> DBResult<TrafficPattern>;
/// Delete traffic configuration
async fn delete_traffic_pattern(
&self,
username: &str,
captcha_key: &str,
) -> DBResult<()>;
/// create new notification
async fn create_notification(&self, p: &AddNotification) -> DBResult<()>;
/// get all unread notifications
async fn get_all_unread_notifications(
&self,
username: &str,
) -> DBResult<Vec<Notification>>;
/// mark a notification read
async fn mark_notification_read(&self, username: &str, id: i32) -> DBResult<()>;
/// record PoWConfig fetches
async fn record_fetch(&self, key: &str) -> DBResult<()>;
/// record PoWConfig solves
async fn record_solve(&self, key: &str) -> DBResult<()>;
/// record PoWConfig confirms
async fn record_confirm(&self, key: &str) -> DBResult<()>;
/// featch PoWConfig fetches
async fn fetch_config_fetched(&self, user: &str, key: &str) -> DBResult<Vec<i64>>;
/// featch PoWConfig solves
async fn fetch_solve(&self, user: &str, key: &str) -> DBResult<Vec<i64>>;
/// featch PoWConfig confirms
async fn fetch_confirm(&self, user: &str, key: &str) -> DBResult<Vec<i64>>;
}
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
/// Captcha statistics with time recorded in UNIX epoch formats
pub struct StatsUnixTimestamp {
/// times at which the configuration were fetched
pub config_fetches: Vec<i64>,
/// times at which the PoW was solved
pub solves: Vec<i64>,
/// times at which the PoW token was verified
pub confirms: Vec<i64>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize, PartialEq)]
/// Represents notification
pub struct Notification {
/// receiver name of the notification
pub name: Option<String>,
/// heading of the notification
pub heading: Option<String>,
/// message of the notification
pub message: Option<String>,
/// when notification was received
pub received: Option<i64>,
/// db assigned ID of the notification
pub id: Option<i32>,
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
/// Data required to add notification
pub struct AddNotification<'a> {
/// who is the notification addressed to?
pub to: &'a str,
/// notification sender
pub from: &'a str,
/// heading of the notification
pub heading: &'a str,
/// mesage of the notification
pub message: &'a str,
}
#[derive(Default, PartialEq, Serialize, Deserialize, Clone, Debug)]
/// User's traffic pattern; used in generating a captcha configuration
pub struct TrafficPattern {
/// average traffic of user's website
pub avg_traffic: u32,
/// the peak traffic that the user's website can handle
pub peak_sustainable_traffic: u32,
/// trafic that bought the user's website down; optional
pub broke_my_site_traffic: Option<u32>,
}
#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
/// data requried to create new captcha
pub struct CreateCaptcha<'a> {
/// cool down duration
pub duration: i32,
/// description of the captcha
pub description: &'a str,
/// secret key of the captcha
pub key: &'a str,
}
#[derive(Clone, Debug, Default, PartialEq, Deserialize, Serialize)]
/// Data representing a captcha
pub struct Captcha {
/// Database assigned ID
pub config_id: i32,
/// cool down duration
pub duration: i32,
/// description of the captcha
pub description: String,
/// secret key of the captcha
pub key: String,
}
#[derive(Clone, Debug, Deserialize, PartialEq, Default, Serialize)]
/// datastructure representing a user's secret
pub struct Secret {
/// user's secret
pub secret: String,
}
/// Trait to clone MCDatabase
pub trait CloneSPDatabase {
/// clone DB
fn clone_db(&self) -> Box<dyn MCDatabase>;
}
impl<T> CloneSPDatabase for T
where
T: MCDatabase + Clone + 'static,
{
fn clone_db(&self) -> Box<dyn MCDatabase> {
Box::new(self.clone())
}
}
impl Clone for Box<dyn MCDatabase> {
fn clone(&self) -> Self {
(**self).clone_db()
}
}

View File

@@ -1,49 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
//! meta operations like migration and connecting to a database
use crate::dev::*;
/// Database operations trait(migrations, pool creation and fetching connection from pool)
pub trait DBOps: GetConnection + Migrate {}
/// Get database connection
#[async_trait]
pub trait GetConnection {
/// database connection type
type Conn;
/// database specific error-type
/// get connection from connection pool
async fn get_conn(&self) -> DBResult<Self::Conn>;
}
/// Create databse connection
#[async_trait]
pub trait Connect {
/// database specific pool-type
type Pool: MCDatabase;
/// database specific error-type
/// create connection pool
async fn connect(self) -> DBResult<Self::Pool>;
}
/// database migrations
#[async_trait]
pub trait Migrate: MCDatabase {
/// database specific error-type
/// run migrations
async fn migrate(&self) -> DBResult<()>;
}

View File

@@ -1,284 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
//! Test utilities
use crate::errors::*;
use crate::prelude::*;
/// test all database functions
pub async fn database_works<'a, T: MCDatabase>(
db: &T,
p: &Register<'a>,
c: &CreateCaptcha<'a>,
l: &[Level],
tp: &TrafficPattern,
an: &AddNotification<'a>,
) {
assert!(db.ping().await, "ping test");
if db.username_exists(p.username).await.unwrap() {
db.delete_user(p.username).await.unwrap();
assert!(
!db.username_exists(p.username).await.unwrap(),
"user is deleted so username shouldn't exsit"
);
}
db.register(p).await.unwrap();
// testing get secret
let secret = db.get_secret(p.username).await.unwrap();
assert_eq!(secret.secret, p.secret, "user secret matches");
// testing update secret: setting secret = username
db.update_secret(p.username, p.username).await.unwrap();
let secret = db.get_secret(p.username).await.unwrap();
assert_eq!(
secret.secret, p.username,
"user secret matches username; as set by previous step"
);
// testing get_password
// with username
let name_hash = db.get_password(&Login::Username(p.username)).await.unwrap();
assert_eq!(name_hash.hash, p.hash, "user password matches");
assert_eq!(name_hash.username, p.username, "username matches");
// with email
let mut name_hash = db
.get_password(&Login::Email(p.email.as_ref().unwrap()))
.await
.unwrap();
assert_eq!(name_hash.hash, p.hash, "user password matches");
assert_eq!(name_hash.username, p.username, "username matches");
// testing get_email
assert_eq!(
db.get_email(p.username)
.await
.unwrap()
.as_ref()
.unwrap()
.as_str(),
p.email.unwrap()
);
// testing email exists
assert!(
db.email_exists(p.email.as_ref().unwrap()).await.unwrap(),
"user is registered so email should exsit"
);
assert!(
db.username_exists(p.username).await.unwrap(),
"user is registered so username should exsit"
);
// update password test. setting password = username
name_hash.hash = name_hash.username.clone();
db.update_password(&name_hash).await.unwrap();
let name_hash = db.get_password(&Login::Username(p.username)).await.unwrap();
assert_eq!(
name_hash.hash, p.username,
"user password matches with changed value"
);
assert_eq!(name_hash.username, p.username, "username matches");
// update username to p.email
assert!(
!db.username_exists(p.email.as_ref().unwrap()).await.unwrap(),
"user with p.email doesn't exist. pre-check to update username to p.email"
);
db.update_username(p.username, p.email.as_ref().unwrap())
.await
.unwrap();
assert!(
db.username_exists(p.email.as_ref().unwrap()).await.unwrap(),
"user with p.email exist post-update"
);
// deleting user for re-registration with email = None
db.delete_user(p.email.as_ref().unwrap()).await.unwrap();
assert!(
!db.username_exists(p.email.as_ref().unwrap()).await.unwrap(),
"user is deleted so username shouldn't exsit"
);
// register with email = None
let mut p2 = p.clone();
p2.email = None;
db.register(&p2).await.unwrap();
assert!(
db.username_exists(p2.username).await.unwrap(),
"user is registered so username should exsit"
);
assert!(
!db.email_exists(p.email.as_ref().unwrap()).await.unwrap(),
"user registration with email is deleted; so email shouldn't exsit"
);
// testing get_email = None
assert_eq!(db.get_email(p.username).await.unwrap(), None);
// testing update email
let update_email = UpdateEmail {
username: p.username,
new_email: p.email.as_ref().unwrap(),
};
db.update_email(&update_email).await.unwrap();
println!(
"null user email: {}",
db.email_exists(p.email.as_ref().unwrap()).await.unwrap()
);
assert!(
db.email_exists(p.email.as_ref().unwrap()).await.unwrap(),
"user was with empty email but email is set; so email should exsit"
);
/*
* test notification workflows
* 1. Add notifications: a minimum of two, to mark as read and test if it has affected it
* 2. Get unread notifications
* 3. Mark a notification read, check if it has affected Step #2
*/
// 1. add notification
db.create_notification(an).await.unwrap();
db.create_notification(an).await.unwrap();
// 2. Get notifications
let notifications = db.get_all_unread_notifications(an.to).await.unwrap();
assert_eq!(notifications.len(), 2);
assert_eq!(notifications[0].heading.as_ref().unwrap(), an.heading);
// 3. mark a notification read
db.mark_notification_read(an.to, notifications[0].id.unwrap())
.await
.unwrap();
let new_notifications = db.get_all_unread_notifications(an.to).await.unwrap();
assert_eq!(new_notifications.len(), 1);
// create captcha
db.create_captcha(p.username, c).await.unwrap();
assert!(db.captcha_exists(None, c.key).await.unwrap());
assert!(db.captcha_exists(Some(p.username), c.key).await.unwrap());
// get captcha configuration
let captcha = db.get_captcha_config(p.username, c.key).await.unwrap();
assert_eq!(captcha.key, c.key);
assert_eq!(captcha.duration, c.duration);
assert_eq!(captcha.description, c.description);
// get all captchas that belong to user
let all_user_captchas = db.get_all_user_captchas(p.username).await.unwrap();
assert_eq!(all_user_captchas.len(), 1);
assert_eq!(all_user_captchas[0], captcha);
// get captcha cooldown duration
assert_eq!(db.get_captcha_cooldown(c.key).await.unwrap(), c.duration);
// add traffic pattern
db.add_traffic_pattern(p.username, c.key, tp).await.unwrap();
assert_eq!(
&db.get_traffic_pattern(p.username, c.key).await.unwrap(),
tp
);
// delete traffic pattern
db.delete_traffic_pattern(p.username, c.key).await.unwrap();
assert!(
matches!(
db.get_traffic_pattern(p.username, c.key).await,
Err(DBError::TrafficPatternNotFound)
),
"deletion successful; traffic pattern no longer exists"
);
// add captcha levels
db.add_captcha_levels(p.username, c.key, l).await.unwrap();
// get captcha levels with username
let levels = db
.get_captcha_levels(Some(p.username), c.key)
.await
.unwrap();
assert_eq!(levels, l);
// get captcha levels without username
let levels = db.get_captcha_levels(None, c.key).await.unwrap();
assert_eq!(levels, l);
/*
* Test stats
* 1. record fetch config
* 2. record solve
* 3. record token verify
* 4. fetch config fetches
* 5. fetch solves
* 6. fetch token verify
*/
assert!(db
.fetch_config_fetched(p.username, c.key)
.await
.unwrap()
.is_empty());
assert!(db.fetch_solve(p.username, c.key).await.unwrap().is_empty());
assert!(db
.fetch_confirm(p.username, c.key)
.await
.unwrap()
.is_empty());
db.record_fetch(c.key).await.unwrap();
db.record_solve(c.key).await.unwrap();
db.record_confirm(c.key).await.unwrap();
assert_eq!(db.fetch_solve(p.username, c.key).await.unwrap().len(), 1);
assert_eq!(
db.fetch_config_fetched(p.username, c.key)
.await
.unwrap()
.len(),
1
);
assert_eq!(db.fetch_solve(p.username, c.key).await.unwrap().len(), 1);
assert_eq!(db.fetch_confirm(p.username, c.key).await.unwrap().len(), 1);
// update captcha key; set key = username;
db.update_captcha_key(p.username, c.key, p.username)
.await
.unwrap();
// checking for captcha with old key; shouldn't exist
assert!(!db.captcha_exists(Some(p.username), c.key).await.unwrap());
// checking for captcha with new key; shouldn exist
assert!(db
.captcha_exists(Some(p.username), p.username)
.await
.unwrap());
// delete captcha levels
db.delete_captcha_levels(p.username, c.key).await.unwrap();
// update captcha; set description = username and duration *= duration;
let mut c2 = c.clone();
c2.duration *= c2.duration;
c2.description = p.username;
db.update_captcha_metadata(p.username, &c2).await.unwrap();
// delete captcha; updated key = p.username so invoke delete with it
db.delete_captcha(p.username, p.username).await.unwrap();
assert!(!db.captcha_exists(Some(p.username), c.key).await.unwrap());
}

View File

@@ -1,2 +0,0 @@
/target
/Cargo.lock

View File

@@ -1,13 +0,0 @@
[package]
name = "db-migrations"
version = "0.1.0"
edition = "2021"
homepage = "https://mcaptcha.org"
repository = "https://github.com/mCaptcha/mCaptcha"
documentation = "https://mcaptcha.org/docs/"
license = "AGPLv3 or later version"
authors = ["realaravinth <realaravinth@batsense.net>"]
[dependencies]
actix-rt = "2"
sqlx = { version = "0.5.13", features = [ "runtime-actix-rustls", "postgres", "time", "offline" ] }

View File

@@ -1,3 +0,0 @@
{
"db": "PostgreSQL"
}

View File

@@ -1,2 +0,0 @@
/target
/Cargo.lock

View File

@@ -1,21 +0,0 @@
[package]
name = "db-sqlx-postgres"
version = "0.1.0"
edition = "2021"
homepage = "https://mcaptcha.org"
repository = "https://github.com/mCaptcha/mCaptcha"
documentation = "https://mcaptcha.org/docs/"
license = "AGPLv3 or later version"
authors = ["realaravinth <realaravinth@batsense.net>"]
[dependencies]
async-trait = "0.1.51"
db-core = {path = "../db-core"}
futures = "0.3.15"
sqlx = { version = "0.5.13", features = [ "runtime-actix-rustls", "postgres", "time", "offline" ] }
[dev-dependencies]
actix-rt = "2"
sqlx = { version = "0.5.13", features = [ "runtime-actix-rustls", "postgres", "time", "offline" ] }
db-core = {path = "../db-core", features = ["test"]}
url = { version = "2.2.2", features = ["serde"] }

View File

@@ -1,3 +0,0 @@
-- Add migration script here
ALTER TABLE mcaptcha_notifications ALTER COLUMN heading TYPE varchar(100),
ALTER COLUMN heading SET NOT NULL;

View File

@@ -1,741 +0,0 @@
{
"db": "PostgreSQL",
"02deb524bb12632af9b7883975f75fdc30d6775d836aff647add1dffd1a4bc00": {
"describe": {
"columns": [
{
"name": "config_id",
"ordinal": 0,
"type_info": "Int4"
},
{
"name": "duration",
"ordinal": 1,
"type_info": "Int4"
},
{
"name": "name",
"ordinal": 2,
"type_info": "Varchar"
},
{
"name": "key",
"ordinal": 3,
"type_info": "Varchar"
}
],
"nullable": [
false,
false,
false,
false
],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "SELECT config_id, duration, name, key from mcaptcha_config WHERE\n key = $1 AND\n user_id = (SELECT ID FROM mcaptcha_users WHERE name = $2) "
},
"044e2036a518de2ccac9318ccba07f7ce10e4a1c1d51d0128ea5e8cb94358ac5": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text",
"Timestamptz"
]
}
},
"query": "INSERT INTO mcaptcha_pow_confirmed_stats \n (config_id, time) VALUES ((SELECT config_id FROM mcaptcha_config WHERE key = $1), $2)"
},
"0840af95cc17c8ea6fc994e53696d4dec39ef9b4b6dd6c58c21cc44ccbb4bd09": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text",
"Text",
"Int4",
"Int4",
"Int4"
]
}
},
"query": "INSERT INTO mcaptcha_sitekey_user_provided_avg_traffic (\n config_id,\n avg_traffic,\n peak_sustainable_traffic,\n broke_my_site_traffic\n ) VALUES ( \n (SELECT config_id FROM mcaptcha_config WHERE key = ($1)\n AND user_id = (SELECT ID FROM mcaptcha_users WHERE name = $2)\n ), $3, $4, $5)"
},
"0e7a1a38019c5e88ebd096fc5f6031aaa7f337fe735aa44c4e31bd6e51163749": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "DELETE FROM mcaptcha_sitekey_user_provided_avg_traffic\n WHERE config_id = (\n SELECT config_id \n FROM \n mcaptcha_config \n WHERE\n key = ($1) \n AND \n user_id = (SELECT ID FROM mcaptcha_users WHERE name = $2)\n );"
},
"16864df9cf9a69c299d9ab68bac559c48f4fc433541a10f7c1b60717df2b820e": {
"describe": {
"columns": [
{
"name": "key",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "name",
"ordinal": 1,
"type_info": "Varchar"
},
{
"name": "config_id",
"ordinal": 2,
"type_info": "Int4"
},
{
"name": "duration",
"ordinal": 3,
"type_info": "Int4"
}
],
"nullable": [
false,
false,
false,
false
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT key, name, config_id, duration FROM mcaptcha_config WHERE\n user_id = (SELECT ID FROM mcaptcha_users WHERE name = $1) "
},
"1e9fe69b23e4bfa7bb369455753100307e334e8dbaf02ff37cda08992fe95910": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
}
},
"query": "UPDATE mcaptcha_users set name = $1\n WHERE name = $2"
},
"2b319a202bb983d5f28979d1e371f399125da1122fbda36a5a55b75b9c743451": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Int4",
"Text"
]
}
},
"query": "-- mark a notification as read\nUPDATE mcaptcha_notifications\n SET read = TRUE\nWHERE \n mcaptcha_notifications.id = $1\nAND\n mcaptcha_notifications.rx = (\n SELECT\n id\n FROM\n mcaptcha_users\n WHERE\n name = $2\n );\n"
},
"307245aaf5b0d692448b80358d6916aa50c507b35e724d66c9b16a16b60e1b38": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Int4",
"Varchar"
]
}
},
"query": "INSERT INTO mcaptcha_config\n (key, user_id, duration, name)\n VALUES ($1, (SELECT ID FROM mcaptcha_users WHERE name = $2), $3, $4)"
},
"30ba202b601dd07f41798775c7c59fde7deeae759ec959df46734a66ffd78df7": {
"describe": {
"columns": [
{
"name": "email",
"ordinal": 0,
"type_info": "Varchar"
}
],
"nullable": [
true
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT email FROM mcaptcha_users WHERE name = $1"
},
"3b1c8128fc48b16d8e8ea6957dd4fbc0eb19ae64748fd7824e9f5e1901dd1726": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
}
},
"query": "UPDATE mcaptcha_users set secret = $1\n WHERE name = $2"
},
"3eb1c43ffd2378c4dd59975568c3a180b72d13008f294a91f3e76b785dba295b": {
"describe": {
"columns": [
{
"name": "exists",
"ordinal": 0,
"type_info": "Bool"
}
],
"nullable": [
null
],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "SELECT EXISTS (\n SELECT 1 from mcaptcha_config WHERE key = $1 \n AND user_id = (SELECT ID FROM mcaptcha_users WHERE name = $2)\n )"
},
"4303f5c6ef98e0de9d8d3c2d781d3ffaa3dee5f7d27db831d327b26f03ba9d68": {
"describe": {
"columns": [
{
"name": "time",
"ordinal": 0,
"type_info": "Timestamptz"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "SELECT time FROM mcaptcha_pow_confirmed_stats \n WHERE \n config_id = (\n SELECT config_id FROM mcaptcha_config \n WHERE \n key = $1\n AND\n user_id = (\n SELECT \n ID FROM mcaptcha_users WHERE name = $2))\n ORDER BY time DESC"
},
"45d9e9fb6344fe3a18c2529d50c935d3837bfe25c96595beb6970d6067720578": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Varchar",
"Varchar"
]
}
},
"query": "insert into mcaptcha_users \n (name , password, email, secret) values ($1, $2, $3, $4)"
},
"47fa50aecfb1499b0a18fa9299643017a1a8d69d4e9980032e0d8f745465d14f": {
"describe": {
"columns": [
{
"name": "exists",
"ordinal": 0,
"type_info": "Bool"
}
],
"nullable": [
null
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT EXISTS (SELECT 1 from mcaptcha_users WHERE email = $1)"
},
"4a5dfbc5aeb2bab290a09640cc25223d484fbc7549e5bc54f33bab8616725031": {
"describe": {
"columns": [
{
"name": "exists",
"ordinal": 0,
"type_info": "Bool"
}
],
"nullable": [
null
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT EXISTS (SELECT 1 from mcaptcha_config WHERE key = $1)"
},
"507bea10c7f8417c5b1430211d0137299cd561333bf47f7b4887d0ef801d1ea4": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text"
]
}
},
"query": "UPDATE mcaptcha_config SET key = $1 \n WHERE key = $2 AND user_id = (SELECT ID FROM mcaptcha_users WHERE name = $3)"
},
"570c22f19fe0b97d78086038c8ef82509dce0bae704d80f9f031c1c47e6a6572": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Int4",
"Text",
"Text"
]
}
},
"query": "UPDATE mcaptcha_config SET name = $1, duration = $2\n WHERE user_id = (SELECT ID FROM mcaptcha_users WHERE name = $3)\n AND key = $4"
},
"717771c42737feb3f4ca13f2ab11361073ea17b55562a103f660149bf049c5c6": {
"describe": {
"columns": [
{
"name": "difficulty_factor",
"ordinal": 0,
"type_info": "Int4"
},
{
"name": "visitor_threshold",
"ordinal": 1,
"type_info": "Int4"
}
],
"nullable": [
false,
false
],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "SELECT difficulty_factor, visitor_threshold FROM mcaptcha_levels WHERE\n config_id = (\n SELECT config_id FROM mcaptcha_config WHERE key = ($1)\n AND user_id = (SELECT ID from mcaptcha_users WHERE name = $2)\n )\n ORDER BY difficulty_factor ASC;"
},
"726a794f7599b78ab749d9f887f5c28db38f072b41f691bde35d23ba0dd72409": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text",
"Timestamptz"
]
}
},
"query": "INSERT INTO mcaptcha_pow_fetched_stats \n (config_id, time) VALUES ((SELECT config_id FROM mcaptcha_config WHERE key = $1), $2)"
},
"7c96ae73dd73c1b0e073e3ac78f87f4cba23fdb2cdbed9ba9b0d55f33655582e": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "DELETE FROM mcaptcha_levels \n WHERE config_id = (\n SELECT config_id FROM mcaptcha_config where key = ($1) \n AND user_id = (\n SELECT ID from mcaptcha_users WHERE name = $2\n )\n )"
},
"81c779ed4bb59f8b94dea730cbda31f7733ef16d509a3ed607388b5ddef74638": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Varchar"
]
}
},
"query": "INSERT INTO mcaptcha_users \n (name , password, secret) VALUES ($1, $2, $3)"
},
"84484cb6892db29121816bc5bff5702b9e857e20aa14e79d080d78ae7593153b": {
"describe": {
"columns": [
{
"name": "time",
"ordinal": 0,
"type_info": "Timestamptz"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "SELECT time FROM mcaptcha_pow_solved_stats \n WHERE config_id = (\n SELECT config_id FROM mcaptcha_config \n WHERE \n key = $1\n AND\n user_id = (\n SELECT \n ID FROM mcaptcha_users WHERE name = $2)) \n ORDER BY time DESC"
},
"9753721856a47438c5e72f28fd9d149db10c48e677b4613bf3f1e8487908aac8": {
"describe": {
"columns": [
{
"name": "difficulty_factor",
"ordinal": 0,
"type_info": "Int4"
},
{
"name": "visitor_threshold",
"ordinal": 1,
"type_info": "Int4"
}
],
"nullable": [
false,
false
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT difficulty_factor, visitor_threshold FROM mcaptcha_levels WHERE\n config_id = (\n SELECT config_id FROM mcaptcha_config WHERE key = ($1)\n ) ORDER BY difficulty_factor ASC;"
},
"ad196ab3ef9dc32f6de2313577ccd6c26eae9ab19df5f71ce182651983efb99a": {
"describe": {
"columns": [
{
"name": "duration",
"ordinal": 0,
"type_info": "Int4"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT duration FROM mcaptcha_config \n WHERE key = $1"
},
"ad23588ee4bcbb13e208460ce21e2fa9f1373893934b530b339fea10360b34a8": {
"describe": {
"columns": [
{
"name": "exists",
"ordinal": 0,
"type_info": "Bool"
}
],
"nullable": [
null
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT EXISTS (SELECT 1 from mcaptcha_users WHERE name = $1)"
},
"b97d810814fbeb2df19f47bcfa381bc6fb7ac6832d040b377cf4fca2ca896cfb": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
}
},
"query": "UPDATE mcaptcha_users set email = $1\n WHERE name = $2"
},
"bb6443e1df704294abbbdb563f1bf46660d0f3462c0c35c10a533446fc7c53e8": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "DELETE FROM mcaptcha_config WHERE key = ($1)\n AND\n user_id = (SELECT ID FROM mcaptcha_users WHERE name = $2)"
},
"bdf2e2781bfa2e9c81c18ef8df7230809d3b20274685a35b1c544804f2a58241": {
"describe": {
"columns": [
{
"name": "name",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "password",
"ordinal": 1,
"type_info": "Text"
}
],
"nullable": [
false,
false
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT name, password FROM mcaptcha_users WHERE email = ($1)"
},
"c2e167e56242de7e0a835e25004b15ca8340545fa0ca7ac8f3293157d2d03d98": {
"describe": {
"columns": [
{
"name": "avg_traffic",
"ordinal": 0,
"type_info": "Int4"
},
{
"name": "peak_sustainable_traffic",
"ordinal": 1,
"type_info": "Int4"
},
{
"name": "broke_my_site_traffic",
"ordinal": 2,
"type_info": "Int4"
}
],
"nullable": [
false,
false,
true
],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "SELECT \n avg_traffic, \n peak_sustainable_traffic, \n broke_my_site_traffic \n FROM \n mcaptcha_sitekey_user_provided_avg_traffic \n WHERE \n config_id = (\n SELECT \n config_id \n FROM \n mcaptcha_config \n WHERE \n KEY = $1 \n AND user_id = (\n SELECT \n id \n FROM \n mcaptcha_users \n WHERE \n NAME = $2\n )\n )\n "
},
"c399efd5db1284dcb470c40f9b076851f77498c75a63a3b151d4a111bd3e2957": {
"describe": {
"columns": [
{
"name": "time",
"ordinal": 0,
"type_info": "Timestamptz"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "SELECT time FROM mcaptcha_pow_fetched_stats\n WHERE \n config_id = (\n SELECT \n config_id FROM mcaptcha_config \n WHERE \n key = $1\n AND\n user_id = (\n SELECT \n ID FROM mcaptcha_users WHERE name = $2))\n ORDER BY time DESC"
},
"ca9d5241f1234d1825f7ead391ebe9099fca776e7101ac6e1761881606def5fa": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "DELETE FROM mcaptcha_users WHERE name = ($1)"
},
"d7dd6cd6a7626e79c62377b2d59115067c5851ec044911ff8833779a08bbb8f7": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Text",
"Text",
"Timestamptz"
]
}
},
"query": "INSERT INTO mcaptcha_notifications (\n heading, message, tx, rx, received)\n VALUES (\n $1, $2,\n (SELECT ID FROM mcaptcha_users WHERE name = $3),\n (SELECT ID FROM mcaptcha_users WHERE name = $4),\n $5\n );"
},
"dbe4307651d94bc6db4f1d8b2c6d076fde6280983d59593216d7765cbbdd669c": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text",
"Timestamptz"
]
}
},
"query": "INSERT INTO mcaptcha_pow_solved_stats \n (config_id, time) VALUES ((SELECT config_id FROM mcaptcha_config WHERE key = $1), $2)"
},
"dcf0d4f9d803dcb1d6f775899f79595f9c78d46633e0ec822303284430df7a3d": {
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Int4"
},
{
"name": "heading",
"ordinal": 1,
"type_info": "Varchar"
},
{
"name": "message",
"ordinal": 2,
"type_info": "Varchar"
},
{
"name": "received",
"ordinal": 3,
"type_info": "Timestamptz"
},
{
"name": "name",
"ordinal": 4,
"type_info": "Varchar"
}
],
"nullable": [
true,
true,
true,
true,
true
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "-- gets all unread notifications a user has\nSELECT \n mcaptcha_notifications.id,\n mcaptcha_notifications.heading,\n mcaptcha_notifications.message,\n mcaptcha_notifications.received,\n mcaptcha_users.name\nFROM\n mcaptcha_notifications \nINNER JOIN \n mcaptcha_users \nON \n mcaptcha_notifications.tx = mcaptcha_users.id\nWHERE \n mcaptcha_notifications.rx = (\n SELECT \n id \n FROM \n mcaptcha_users\n WHERE\n name = $1\n )\nAND \n mcaptcha_notifications.read IS NULL;\n"
},
"e4c710d33b709aee262fa0704372ac216d98851447ef4fbe221740b7ae4ea422": {
"describe": {
"columns": [
{
"name": "secret",
"ordinal": 0,
"type_info": "Varchar"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT secret FROM mcaptcha_users WHERE name = ($1)"
},
"e9ed973dfd2bfef36d5a4724aef4993328e1d8d3ca397fe6d5408a780efc775a": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "UPDATE mcaptcha_users set password = $1\n WHERE name = $2"
},
"f330cb94c53d33495df94aacec7e4e91d8a920742b89a63d1c59a8ea8937c5c8": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Int4",
"Int4",
"Text",
"Text"
]
}
},
"query": "INSERT INTO mcaptcha_levels (\n difficulty_factor, \n visitor_threshold,\n config_id) VALUES (\n $1, $2, (\n SELECT config_id FROM mcaptcha_config WHERE\n key = ($3) AND user_id = (\n SELECT ID FROM mcaptcha_users WHERE name = $4\n )));"
},
"f3dee60b85be2ae861b6695286e387529dabf3d11202fb2eeb7e75a7bb3bd0a4": {
"describe": {
"columns": [
{
"name": "name",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "password",
"ordinal": 1,
"type_info": "Text"
}
],
"nullable": [
false,
false
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT name, password FROM mcaptcha_users WHERE name = ($1)"
}
}

View File

@@ -1,56 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
//! Error-handling utilities
use std::borrow::Cow;
use db_core::dev::*;
use sqlx::Error;
/// map custom row not found error to DB error
pub fn map_row_not_found_err(e: Error, row_not_found: DBError) -> DBError {
if let Error::RowNotFound = e {
row_not_found
} else {
map_register_err(e)
}
}
/// map postgres errors to [DBError](DBError) types
pub fn map_register_err(e: Error) -> DBError {
if let Error::Database(err) = e {
if err.code() == Some(Cow::from("23505")) {
let msg = err.message();
println!("{}", msg);
if msg.contains("mcaptcha_users_name_key") {
DBError::UsernameTaken
} else if msg.contains("mcaptcha_users_email_key") {
DBError::EmailTaken
} else if msg.contains("mcaptcha_users_secret_key") {
DBError::SecretTaken
} else if msg.contains("mcaptcha_config_key_key") {
DBError::CaptchaKeyTaken
} else {
DBError::DBError(Box::new(Error::Database(err)))
}
} else {
DBError::DBError(Box::new(Error::Database(err)))
}
} else {
DBError::DBError(Box::new(e))
}
}

View File

@@ -1,940 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use db_core::dev::*;
use sqlx::postgres::PgPoolOptions;
use sqlx::types::time::OffsetDateTime;
use sqlx::PgPool;
pub mod errors;
#[cfg(test)]
pub mod tests;
#[derive(Clone)]
pub struct Database {
pub pool: PgPool,
}
/// Use an existing database pool
pub struct Conn(pub PgPool);
/// Connect to databse
pub enum ConnectionOptions {
/// fresh connection
Fresh(Fresh),
/// existing connection
Existing(Conn),
}
pub struct Fresh {
pub pool_options: PgPoolOptions,
pub url: String,
}
pub mod dev {
pub use super::errors::*;
pub use super::Database;
pub use db_core::dev::*;
pub use prelude::*;
pub use sqlx::Error;
}
pub mod prelude {
pub use super::*;
pub use db_core::prelude::*;
}
#[async_trait]
impl Connect for ConnectionOptions {
type Pool = Database;
async fn connect(self) -> DBResult<Self::Pool> {
let pool = match self {
Self::Fresh(fresh) => fresh
.pool_options
.connect(&fresh.url)
.await
.map_err(|e| DBError::DBError(Box::new(e)))?,
Self::Existing(conn) => conn.0,
};
Ok(Database { pool })
}
}
use dev::*;
#[async_trait]
impl Migrate for Database {
async fn migrate(&self) -> DBResult<()> {
sqlx::migrate!("./migrations/")
.run(&self.pool)
.await
.map_err(|e| DBError::DBError(Box::new(e)))?;
Ok(())
}
}
#[async_trait]
impl MCDatabase for Database {
/// ping DB
async fn ping(&self) -> bool {
use sqlx::Connection;
if let Ok(mut con) = self.pool.acquire().await {
con.ping().await.is_ok()
} else {
false
}
}
/// register a new user
async fn register(&self, p: &Register) -> DBResult<()> {
let res = if let Some(email) = &p.email {
sqlx::query!(
"insert into mcaptcha_users
(name , password, email, secret) values ($1, $2, $3, $4)",
&p.username,
&p.hash,
&email,
&p.secret,
)
.execute(&self.pool)
.await
} else {
sqlx::query!(
"INSERT INTO mcaptcha_users
(name , password, secret) VALUES ($1, $2, $3)",
&p.username,
&p.hash,
&p.secret,
)
.execute(&self.pool)
.await
};
res.map_err(map_register_err)?;
Ok(())
}
/// delete a user
async fn delete_user(&self, username: &str) -> DBResult<()> {
sqlx::query!("DELETE FROM mcaptcha_users WHERE name = ($1)", username)
.execute(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::AccountNotFound))?;
Ok(())
}
/// check if username exists
async fn username_exists(&self, username: &str) -> DBResult<bool> {
let res = sqlx::query!(
"SELECT EXISTS (SELECT 1 from mcaptcha_users WHERE name = $1)",
username,
)
.fetch_one(&self.pool)
.await
.map_err(map_register_err)?;
let mut resp = false;
if let Some(x) = res.exists {
resp = x;
}
Ok(resp)
}
/// get user email
async fn get_email(&self, username: &str) -> DBResult<Option<String>> {
struct Email {
email: Option<String>,
}
let res = sqlx::query_as!(
Email,
"SELECT email FROM mcaptcha_users WHERE name = $1",
username
)
.fetch_one(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::AccountNotFound))?;
Ok(res.email)
}
/// check if email exists
async fn email_exists(&self, email: &str) -> DBResult<bool> {
let res = sqlx::query!(
"SELECT EXISTS (SELECT 1 from mcaptcha_users WHERE email = $1)",
email
)
.fetch_one(&self.pool)
.await
.map_err(map_register_err)?;
let mut resp = false;
if let Some(x) = res.exists {
resp = x;
}
Ok(resp)
}
/// update a user's email
async fn update_email(&self, p: &UpdateEmail) -> DBResult<()> {
sqlx::query!(
"UPDATE mcaptcha_users set email = $1
WHERE name = $2",
&p.new_email,
&p.username,
)
.execute(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::AccountNotFound))?;
Ok(())
}
/// get a user's password
async fn get_password(&self, l: &Login) -> DBResult<NameHash> {
struct Password {
name: String,
password: String,
}
let rec = match l {
Login::Username(u) => sqlx::query_as!(
Password,
r#"SELECT name, password FROM mcaptcha_users WHERE name = ($1)"#,
u,
)
.fetch_one(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::AccountNotFound))?,
Login::Email(e) => sqlx::query_as!(
Password,
r#"SELECT name, password FROM mcaptcha_users WHERE email = ($1)"#,
e,
)
.fetch_one(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::AccountNotFound))?,
};
let res = NameHash {
hash: rec.password,
username: rec.name,
};
Ok(res)
}
/// update user's password
async fn update_password(&self, p: &NameHash) -> DBResult<()> {
sqlx::query!(
"UPDATE mcaptcha_users set password = $1
WHERE name = $2",
&p.hash,
&p.username,
)
.execute(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::AccountNotFound))?;
Ok(())
}
/// update username
async fn update_username(&self, current: &str, new: &str) -> DBResult<()> {
sqlx::query!(
"UPDATE mcaptcha_users set name = $1
WHERE name = $2",
new,
current,
)
.execute(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::AccountNotFound))?;
Ok(())
}
/// get a user's secret
async fn get_secret(&self, username: &str) -> DBResult<Secret> {
let secret = sqlx::query_as!(
Secret,
r#"SELECT secret FROM mcaptcha_users WHERE name = ($1)"#,
username,
)
.fetch_one(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::AccountNotFound))?;
Ok(secret)
}
/// update a user's secret
async fn update_secret(&self, username: &str, secret: &str) -> DBResult<()> {
sqlx::query!(
"UPDATE mcaptcha_users set secret = $1
WHERE name = $2",
&secret,
&username,
)
.execute(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::AccountNotFound))?;
Ok(())
}
/// create new captcha
async fn create_captcha(&self, username: &str, p: &CreateCaptcha) -> DBResult<()> {
sqlx::query!(
"INSERT INTO mcaptcha_config
(key, user_id, duration, name)
VALUES ($1, (SELECT ID FROM mcaptcha_users WHERE name = $2), $3, $4)",
p.key,
username,
p.duration as i32,
p.description,
)
.execute(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::AccountNotFound))?;
Ok(())
}
/// Get captcha config
async fn get_captcha_config(&self, username: &str, key: &str) -> DBResult<Captcha> {
let captcha = sqlx::query_as!(
InternaleCaptchaConfig,
"SELECT config_id, duration, name, key from mcaptcha_config WHERE
key = $1 AND
user_id = (SELECT ID FROM mcaptcha_users WHERE name = $2) ",
&key,
&username,
)
.fetch_one(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
Ok(captcha.into())
}
/// Get all captchas belonging to user
async fn get_all_user_captchas(&self, username: &str) -> DBResult<Vec<Captcha>> {
let mut res = sqlx::query_as!(
InternaleCaptchaConfig,
"SELECT key, name, config_id, duration FROM mcaptcha_config WHERE
user_id = (SELECT ID FROM mcaptcha_users WHERE name = $1) ",
&username,
)
.fetch_all(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::AccountNotFound))?;
let mut captchas = Vec::with_capacity(res.len());
res.drain(0..).for_each(|r| captchas.push(r.into()));
Ok(captchas)
}
/// update captcha metadata; doesn't change captcha key
async fn update_captcha_metadata(
&self,
username: &str,
p: &CreateCaptcha,
) -> DBResult<()> {
sqlx::query!(
"UPDATE mcaptcha_config SET name = $1, duration = $2
WHERE user_id = (SELECT ID FROM mcaptcha_users WHERE name = $3)
AND key = $4",
p.description,
p.duration,
username,
p.key,
)
.execute(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
Ok(())
}
/// update captcha key; doesn't change metadata
async fn update_captcha_key(
&self,
username: &str,
old_key: &str,
new_key: &str,
) -> DBResult<()> {
sqlx::query!(
"UPDATE mcaptcha_config SET key = $1
WHERE key = $2 AND user_id = (SELECT ID FROM mcaptcha_users WHERE name = $3)",
new_key,
old_key,
username,
)
.execute(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
Ok(())
}
/// Add levels to captcha
async fn add_captcha_levels(
&self,
username: &str,
captcha_key: &str,
levels: &[Level],
) -> DBResult<()> {
use futures::future::try_join_all;
let mut futs = Vec::with_capacity(levels.len());
for level in levels.iter() {
let difficulty_factor = level.difficulty_factor as i32;
let visitor_threshold = level.visitor_threshold as i32;
let fut = sqlx::query!(
"INSERT INTO mcaptcha_levels (
difficulty_factor,
visitor_threshold,
config_id) VALUES (
$1, $2, (
SELECT config_id FROM mcaptcha_config WHERE
key = ($3) AND user_id = (
SELECT ID FROM mcaptcha_users WHERE name = $4
)));",
difficulty_factor,
visitor_threshold,
&captcha_key,
username,
)
.execute(&self.pool);
futs.push(fut);
}
try_join_all(futs)
.await
.map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
Ok(())
}
/// check if captcha exists
async fn captcha_exists(
&self,
username: Option<&str>,
captcha_key: &str,
) -> DBResult<bool> {
let mut exists = false;
match username {
Some(username) => {
let x = sqlx::query!(
"SELECT EXISTS (
SELECT 1 from mcaptcha_config WHERE key = $1
AND user_id = (SELECT ID FROM mcaptcha_users WHERE name = $2)
)",
captcha_key,
username
)
.fetch_one(&self.pool)
.await
.map_err(map_register_err)?;
if let Some(x) = x.exists {
exists = x;
};
}
None => {
let x = sqlx::query!(
"SELECT EXISTS (SELECT 1 from mcaptcha_config WHERE key = $1)",
&captcha_key,
)
.fetch_one(&self.pool)
.await
.map_err(map_register_err)?;
if let Some(x) = x.exists {
exists = x;
};
}
};
Ok(exists)
}
/// Delete all levels of a captcha
async fn delete_captcha_levels(
&self,
username: &str,
captcha_key: &str,
) -> DBResult<()> {
sqlx::query!(
"DELETE FROM mcaptcha_levels
WHERE config_id = (
SELECT config_id FROM mcaptcha_config where key = ($1)
AND user_id = (
SELECT ID from mcaptcha_users WHERE name = $2
)
)",
captcha_key,
username
)
.execute(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
Ok(())
}
/// Delete captcha
async fn delete_captcha(&self, username: &str, captcha_key: &str) -> DBResult<()> {
sqlx::query!(
"DELETE FROM mcaptcha_config WHERE key = ($1)
AND
user_id = (SELECT ID FROM mcaptcha_users WHERE name = $2)",
captcha_key,
username,
)
.execute(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
Ok(())
}
/// Get captcha levels
async fn get_captcha_levels(
&self,
username: Option<&str>,
captcha_key: &str,
) -> DBResult<Vec<Level>> {
struct I32Levels {
difficulty_factor: i32,
visitor_threshold: i32,
}
let levels = match username {
None => sqlx::query_as!(
I32Levels,
"SELECT difficulty_factor, visitor_threshold FROM mcaptcha_levels WHERE
config_id = (
SELECT config_id FROM mcaptcha_config WHERE key = ($1)
) ORDER BY difficulty_factor ASC;",
captcha_key,
)
.fetch_all(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?,
Some(username) => sqlx::query_as!(
I32Levels,
"SELECT difficulty_factor, visitor_threshold FROM mcaptcha_levels WHERE
config_id = (
SELECT config_id FROM mcaptcha_config WHERE key = ($1)
AND user_id = (SELECT ID from mcaptcha_users WHERE name = $2)
)
ORDER BY difficulty_factor ASC;",
captcha_key,
username
)
.fetch_all(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?,
};
let mut new_levels = Vec::with_capacity(levels.len());
for l in levels.iter() {
new_levels.push(Level {
difficulty_factor: l.difficulty_factor as u32,
visitor_threshold: l.visitor_threshold as u32,
});
}
Ok(new_levels)
}
/// Get captcha's cooldown period
async fn get_captcha_cooldown(&self, captcha_key: &str) -> DBResult<i32> {
struct DurationResp {
duration: i32,
}
let resp = sqlx::query_as!(
DurationResp,
"SELECT duration FROM mcaptcha_config
WHERE key = $1",
captcha_key,
)
.fetch_one(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
Ok(resp.duration)
}
/// Add traffic configuration
async fn add_traffic_pattern(
&self,
username: &str,
captcha_key: &str,
pattern: &TrafficPattern,
) -> DBResult<()> {
sqlx::query!(
"INSERT INTO mcaptcha_sitekey_user_provided_avg_traffic (
config_id,
avg_traffic,
peak_sustainable_traffic,
broke_my_site_traffic
) VALUES (
(SELECT config_id FROM mcaptcha_config WHERE key = ($1)
AND user_id = (SELECT ID FROM mcaptcha_users WHERE name = $2)
), $3, $4, $5)",
//payload.avg_traffic,
captcha_key,
username,
pattern.avg_traffic as i32,
pattern.peak_sustainable_traffic as i32,
pattern.broke_my_site_traffic.as_ref().map(|v| *v as i32),
)
.execute(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
Ok(())
}
/// Get traffic configuration
async fn get_traffic_pattern(
&self,
username: &str,
captcha_key: &str,
) -> DBResult<TrafficPattern> {
struct Traffic {
peak_sustainable_traffic: i32,
avg_traffic: i32,
broke_my_site_traffic: Option<i32>,
}
let res = sqlx::query_as!(
Traffic,
"SELECT
avg_traffic,
peak_sustainable_traffic,
broke_my_site_traffic
FROM
mcaptcha_sitekey_user_provided_avg_traffic
WHERE
config_id = (
SELECT
config_id
FROM
mcaptcha_config
WHERE
KEY = $1
AND user_id = (
SELECT
id
FROM
mcaptcha_users
WHERE
NAME = $2
)
)
",
captcha_key,
username
)
.fetch_one(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::TrafficPatternNotFound))?;
Ok(TrafficPattern {
broke_my_site_traffic: res.broke_my_site_traffic.as_ref().map(|v| *v as u32),
avg_traffic: res.avg_traffic as u32,
peak_sustainable_traffic: res.peak_sustainable_traffic as u32,
})
}
/// Delete traffic configuration
async fn delete_traffic_pattern(
&self,
username: &str,
captcha_key: &str,
) -> DBResult<()> {
sqlx::query!(
"DELETE FROM mcaptcha_sitekey_user_provided_avg_traffic
WHERE config_id = (
SELECT config_id
FROM
mcaptcha_config
WHERE
key = ($1)
AND
user_id = (SELECT ID FROM mcaptcha_users WHERE name = $2)
);",
captcha_key,
username,
)
.execute(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::TrafficPatternNotFound))?;
Ok(())
}
/// create new notification
async fn create_notification(&self, p: &AddNotification) -> DBResult<()> {
let now = now_unix_time_stamp();
sqlx::query!(
"INSERT INTO mcaptcha_notifications (
heading, message, tx, rx, received)
VALUES (
$1, $2,
(SELECT ID FROM mcaptcha_users WHERE name = $3),
(SELECT ID FROM mcaptcha_users WHERE name = $4),
$5
);",
p.heading,
p.message,
p.from,
p.to,
now
)
.execute(&self.pool)
.await
.map_err(map_register_err)?;
Ok(())
}
/// get all unread notifications
async fn get_all_unread_notifications(
&self,
username: &str,
) -> DBResult<Vec<Notification>> {
let mut inner_notifications = sqlx::query_file_as!(
InnerNotification,
"./src/get_all_unread_notifications.sql",
&username
)
.fetch_all(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::AccountNotFound))?;
let mut notifications = Vec::with_capacity(inner_notifications.len());
inner_notifications
.drain(0..)
.for_each(|n| notifications.push(n.into()));
Ok(notifications)
}
/// mark a notification read
async fn mark_notification_read(&self, username: &str, id: i32) -> DBResult<()> {
sqlx::query_file_as!(
Notification,
"./src/mark_notification_read.sql",
id,
&username
)
.execute(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::NotificationNotFound))?;
Ok(())
}
/// record PoWConfig fetches
async fn record_fetch(&self, key: &str) -> DBResult<()> {
let now = now_unix_time_stamp();
let _ = sqlx::query!(
"INSERT INTO mcaptcha_pow_fetched_stats
(config_id, time) VALUES ((SELECT config_id FROM mcaptcha_config WHERE key = $1), $2)",
key,
&now,
)
.execute(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
Ok(())
}
/// record PoWConfig solves
async fn record_solve(&self, key: &str) -> DBResult<()> {
let now = OffsetDateTime::now_utc();
let _ = sqlx::query!(
"INSERT INTO mcaptcha_pow_solved_stats
(config_id, time) VALUES ((SELECT config_id FROM mcaptcha_config WHERE key = $1), $2)",
key,
&now,
)
.execute(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
Ok(())
}
/// record PoWConfig confirms
async fn record_confirm(&self, key: &str) -> DBResult<()> {
let now = now_unix_time_stamp();
let _ = sqlx::query!(
"INSERT INTO mcaptcha_pow_confirmed_stats
(config_id, time) VALUES ((SELECT config_id FROM mcaptcha_config WHERE key = $1), $2)",
key,
&now
)
.execute(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
Ok(())
}
/// featch PoWConfig fetches
async fn fetch_config_fetched(&self, user: &str, key: &str) -> DBResult<Vec<i64>> {
let records = sqlx::query_as!(
Date,
"SELECT time FROM mcaptcha_pow_fetched_stats
WHERE
config_id = (
SELECT
config_id FROM mcaptcha_config
WHERE
key = $1
AND
user_id = (
SELECT
ID FROM mcaptcha_users WHERE name = $2))
ORDER BY time DESC",
&key,
&user,
)
.fetch_all(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
Ok(Date::dates_to_unix(records))
}
/// featch PoWConfig solves
async fn fetch_solve(&self, user: &str, key: &str) -> DBResult<Vec<i64>> {
let records = sqlx::query_as!(
Date,
"SELECT time FROM mcaptcha_pow_solved_stats
WHERE config_id = (
SELECT config_id FROM mcaptcha_config
WHERE
key = $1
AND
user_id = (
SELECT
ID FROM mcaptcha_users WHERE name = $2))
ORDER BY time DESC",
&key,
&user
)
.fetch_all(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
Ok(Date::dates_to_unix(records))
}
/// featch PoWConfig confirms
async fn fetch_confirm(&self, user: &str, key: &str) -> DBResult<Vec<i64>> {
let records = sqlx::query_as!(
Date,
"SELECT time FROM mcaptcha_pow_confirmed_stats
WHERE
config_id = (
SELECT config_id FROM mcaptcha_config
WHERE
key = $1
AND
user_id = (
SELECT
ID FROM mcaptcha_users WHERE name = $2))
ORDER BY time DESC",
&key,
&user
)
.fetch_all(&self.pool)
.await
.map_err(|e| map_row_not_found_err(e, DBError::CaptchaNotFound))?;
Ok(Date::dates_to_unix(records))
}
}
#[derive(Clone)]
struct Date {
time: OffsetDateTime,
}
impl Date {
fn dates_to_unix(mut d: Vec<Self>) -> Vec<i64> {
let mut dates = Vec::with_capacity(d.len());
d.drain(0..)
.for_each(|x| dates.push(x.time.unix_timestamp()));
dates
}
}
fn now_unix_time_stamp() -> OffsetDateTime {
OffsetDateTime::now_utc()
}
#[derive(Debug, Clone, Default, PartialEq)]
/// Represents notification
pub struct InnerNotification {
/// receiver name of the notification
pub name: Option<String>,
/// heading of the notification
pub heading: Option<String>,
/// message of the notification
pub message: Option<String>,
/// when notification was received
pub received: Option<OffsetDateTime>,
/// db assigned ID of the notification
pub id: Option<i32>,
}
impl From<InnerNotification> for Notification {
fn from(n: InnerNotification) -> Self {
Notification {
name: n.name,
heading: n.heading,
message: n.message,
received: n.received.map(|t| t.unix_timestamp()),
id: n.id,
}
}
}
#[derive(Clone)]
struct InternaleCaptchaConfig {
config_id: i32,
duration: i32,
name: String,
key: String,
}
impl From<InternaleCaptchaConfig> for Captcha {
fn from(i: InternaleCaptchaConfig) -> Self {
Self {
config_id: i.config_id,
duration: i.duration,
description: i.name,
key: i.key,
}
}
}

View File

@@ -1,88 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#![cfg(test)]
use sqlx::postgres::PgPoolOptions;
use std::env;
use crate::*;
use db_core::tests::*;
#[actix_rt::test]
async fn everyting_works() {
const EMAIL: &str = "postgresuser@foo.com";
const NAME: &str = "postgresuser";
const PASSWORD: &str = "pasdfasdfasdfadf";
const SECRET1: &str = "postgressecret1";
// captcha config
const CAPTCHA_SECRET: &str = "postgrescaptchasecret";
const CAPTCHA_DESCRIPTION: &str = "postgrescaptchadescription";
const CAPTCHA_DURATION: i32 = 30;
// notification config
const HEADING: &str = "testing notifications get db postgres";
const MESSAGE: &str = "testing notifications get message db postgres";
// easy traffic pattern
const TRAFFIC_PATTERN: TrafficPattern = TrafficPattern {
avg_traffic: 500,
peak_sustainable_traffic: 5_000,
broke_my_site_traffic: Some(10_000),
};
const LEVELS: [Level; 3] = [
Level {
difficulty_factor: 1,
visitor_threshold: 1,
},
Level {
difficulty_factor: 2,
visitor_threshold: 2,
},
Level {
difficulty_factor: 3,
visitor_threshold: 3,
},
];
const ADD_NOTIFICATION: AddNotification = AddNotification {
from: NAME,
to: NAME,
message: MESSAGE,
heading: HEADING,
};
let url = env::var("POSTGRES_DATABASE_URL").unwrap();
let pool_options = PgPoolOptions::new().max_connections(2);
let connection_options = ConnectionOptions::Fresh(Fresh { pool_options, url });
let db = connection_options.connect().await.unwrap();
db.migrate().await.unwrap();
let p = Register {
username: NAME,
email: Some(EMAIL),
hash: PASSWORD,
secret: SECRET1,
};
let c = CreateCaptcha {
duration: CAPTCHA_DURATION,
key: CAPTCHA_SECRET,
description: CAPTCHA_DESCRIPTION,
};
database_works(&db, &p, &c, &LEVELS, &TRAFFIC_PATTERN, &ADD_NOTIFICATION).await;
}

View File

@@ -0,0 +1,25 @@
version: '3.9'
services:
mcaptcha:
build: .
ports:
- 7000:7000
environment:
DATABASE_URL: postgres://postgres:password@postgres:5432/postgres # set password at placeholder
MCAPTCHA_REDIS_URL: redis://mcaptcha-redis/
RUST_LOG: debug
postgres:
image: postgres:13.2
volumes:
- mcaptcha-data:/var/lib/postgresql/
environment:
POSTGRES_PASSWORD: password # change password
PGDATA: /var/lib/postgresql/data/mcaptcha/
mcaptcha-redis:
image: mcaptcha/cache:latest
volumes:
mcaptcha-data:

View File

@@ -4,7 +4,6 @@
# I tried running cargo test with the `--jobs` parameter set to 1 but that didn't # I tried running cargo test with the `--jobs` parameter set to 1 but that didn't
# seem to solve the issue. This scr will run the whole test suite but one test at a time. # seem to solve the issue. This scr will run the whole test suite but one test at a time.
set -Eeuo pipefail
for ut in \ for ut in \
api::v1::meta::tests::build_details_works \ api::v1::meta::tests::build_details_works \

View File

@@ -1,3 +1,802 @@
{ {
"db": "PostgreSQL" "db": "PostgreSQL",
"044e2036a518de2ccac9318ccba07f7ce10e4a1c1d51d0128ea5e8cb94358ac5": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text",
"Timestamptz"
]
}
},
"query": "INSERT INTO mcaptcha_pow_confirmed_stats \n (config_id, time) VALUES ((SELECT config_id FROM mcaptcha_config WHERE key = $1), $2)"
},
"06699fda6b1542bf4544c0bdece91531a3020c24c9c76bcf967980e71ee25b42": {
"describe": {
"columns": [
{
"name": "email",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "secret",
"ordinal": 1,
"type_info": "Varchar"
}
],
"nullable": [
true,
false
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT email, secret FROM mcaptcha_users WHERE name = ($1)"
},
"2021bc0eb03df51af06b59e2a1efdba231e8f35d9cfb5c5b55241c566b9055ce": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
}
},
"query": "UPDATE mcaptcha_users set name = $1\n WHERE name = $2"
},
"238569a64d7dbd252e3b27204f207e8a8548109717b89495ddf8f9a870c7c75d": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Int4",
"Text",
"Text"
]
}
},
"query": "UPDATE mcaptcha_config SET name = $1, duration = $2 \n WHERE user_id = (SELECT ID FROM mcaptcha_users WHERE name = $3)\n AND key = $4"
},
"2b319a202bb983d5f28979d1e371f399125da1122fbda36a5a55b75b9c743451": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Int4",
"Text"
]
}
},
"query": "-- mark a notification as read\nUPDATE mcaptcha_notifications\n SET read = TRUE\nWHERE \n mcaptcha_notifications.id = $1\nAND\n mcaptcha_notifications.rx = (\n SELECT\n id\n FROM\n mcaptcha_users\n WHERE\n name = $2\n );\n"
},
"307245aaf5b0d692448b80358d6916aa50c507b35e724d66c9b16a16b60e1b38": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Int4",
"Varchar"
]
}
},
"query": "INSERT INTO mcaptcha_config\n (key, user_id, duration, name)\n VALUES ($1, (SELECT ID FROM mcaptcha_users WHERE name = $2), $3, $4)"
},
"3b1c8128fc48b16d8e8ea6957dd4fbc0eb19ae64748fd7824e9f5e1901dd1726": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
}
},
"query": "UPDATE mcaptcha_users set secret = $1\n WHERE name = $2"
},
"3ebc2aab517b9a2db463b6ea64aee76da5d051817acba8d0fb55ad503acc6b63": {
"describe": {
"columns": [
{
"name": "duration",
"ordinal": 0,
"type_info": "Int4"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT duration FROM mcaptcha_config \n WHERE key = $1"
},
"41451ffdad4ebda63cd38b90ec5259b478157eaa395960c036548bc7629c8d34": {
"describe": {
"columns": [
{
"name": "password",
"ordinal": 0,
"type_info": "Text"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT password FROM mcaptcha_users WHERE name = ($1)"
},
"4303f5c6ef98e0de9d8d3c2d781d3ffaa3dee5f7d27db831d327b26f03ba9d68": {
"describe": {
"columns": [
{
"name": "time",
"ordinal": 0,
"type_info": "Timestamptz"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "SELECT time FROM mcaptcha_pow_confirmed_stats \n WHERE \n config_id = (\n SELECT config_id FROM mcaptcha_config \n WHERE \n key = $1\n AND\n user_id = (\n SELECT \n ID FROM mcaptcha_users WHERE name = $2))\n ORDER BY time DESC"
},
"45d9e9fb6344fe3a18c2529d50c935d3837bfe25c96595beb6970d6067720578": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Varchar",
"Varchar"
]
}
},
"query": "insert into mcaptcha_users \n (name , password, email, secret) values ($1, $2, $3, $4)"
},
"47fa50aecfb1499b0a18fa9299643017a1a8d69d4e9980032e0d8f745465d14f": {
"describe": {
"columns": [
{
"name": "exists",
"ordinal": 0,
"type_info": "Bool"
}
],
"nullable": [
null
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT EXISTS (SELECT 1 from mcaptcha_users WHERE email = $1)"
},
"4a5dfbc5aeb2bab290a09640cc25223d484fbc7549e5bc54f33bab8616725031": {
"describe": {
"columns": [
{
"name": "exists",
"ordinal": 0,
"type_info": "Bool"
}
],
"nullable": [
null
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT EXISTS (SELECT 1 from mcaptcha_config WHERE key = $1)"
},
"4c3a9fe30a4c6bd49ab1cb8883c4495993aa05f2991483b4f04913b2e5043a63": {
"describe": {
"columns": [
{
"name": "difficulty_factor",
"ordinal": 0,
"type_info": "Int4"
},
{
"name": "visitor_threshold",
"ordinal": 1,
"type_info": "Int4"
}
],
"nullable": [
false,
false
],
"parameters": {
"Left": [
"Int4"
]
}
},
"query": "SELECT \n difficulty_factor, visitor_threshold \n FROM \n mcaptcha_levels \n WHERE config_id = $1 ORDER BY difficulty_factor ASC"
},
"507bea10c7f8417c5b1430211d0137299cd561333bf47f7b4887d0ef801d1ea4": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Text"
]
}
},
"query": "UPDATE mcaptcha_config SET key = $1 \n WHERE key = $2 AND user_id = (SELECT ID FROM mcaptcha_users WHERE name = $3)"
},
"51758dd099e4eaafeab3b45cdc08a44eb19d72f2e5b23494cf3978d7fc134402": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
}
},
"query": "UPDATE mcaptcha_users set email = $1\n WHERE name = $2"
},
"60081afa71dca3d10b372aabfdbc809f0cf62b33994a3bb43ea444159c6544fe": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Text",
"Text"
]
}
},
"query": "INSERT INTO mcaptcha_notifications (\n heading, message, tx, rx)\n VALUES (\n $1, $2,\n (SELECT ID FROM mcaptcha_users WHERE name = $3),\n (SELECT ID FROM mcaptcha_users WHERE name = $4)\n );"
},
"61523f76efade451db9db38cf4c8092af7489a90cd4186e8d21eb1d8afafdf64": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text",
"Text",
"Int4",
"Int4",
"Int4"
]
}
},
"query": "INSERT INTO mcaptcha_sitekey_user_provided_avg_traffic (\n config_id,\n avg_traffic,\n peak_sustainable_traffic,\n broke_my_site_traffic\n ) VALUES ( \n (SELECT config_id FROM mcaptcha_config \n WHERE\n key = ($1)\n AND user_id = (SELECT ID FROM mcaptcha_users WHERE name = $2)\n ), $3, $4, $5)"
},
"717771c42737feb3f4ca13f2ab11361073ea17b55562a103f660149bf049c5c6": {
"describe": {
"columns": [
{
"name": "difficulty_factor",
"ordinal": 0,
"type_info": "Int4"
},
{
"name": "visitor_threshold",
"ordinal": 1,
"type_info": "Int4"
}
],
"nullable": [
false,
false
],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "SELECT difficulty_factor, visitor_threshold FROM mcaptcha_levels WHERE\n config_id = (\n SELECT config_id FROM mcaptcha_config WHERE key = ($1)\n AND user_id = (SELECT ID from mcaptcha_users WHERE name = $2)\n )\n ORDER BY difficulty_factor ASC;"
},
"726a794f7599b78ab749d9f887f5c28db38f072b41f691bde35d23ba0dd72409": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text",
"Timestamptz"
]
}
},
"query": "INSERT INTO mcaptcha_pow_fetched_stats \n (config_id, time) VALUES ((SELECT config_id FROM mcaptcha_config WHERE key = $1), $2)"
},
"76d1b62e0c70d09247691ca328d8674c8039fab922a40352b8ab5ed5b26a5293": {
"describe": {
"columns": [
{
"name": "key",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "name",
"ordinal": 1,
"type_info": "Varchar"
}
],
"nullable": [
false,
false
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT key, name from mcaptcha_config WHERE\n user_id = (SELECT ID FROM mcaptcha_users WHERE name = $1) "
},
"7c96ae73dd73c1b0e073e3ac78f87f4cba23fdb2cdbed9ba9b0d55f33655582e": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "DELETE FROM mcaptcha_levels \n WHERE config_id = (\n SELECT config_id FROM mcaptcha_config where key = ($1) \n AND user_id = (\n SELECT ID from mcaptcha_users WHERE name = $2\n )\n )"
},
"81c779ed4bb59f8b94dea730cbda31f7733ef16d509a3ed607388b5ddef74638": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Varchar"
]
}
},
"query": "INSERT INTO mcaptcha_users \n (name , password, secret) VALUES ($1, $2, $3)"
},
"84484cb6892db29121816bc5bff5702b9e857e20aa14e79d080d78ae7593153b": {
"describe": {
"columns": [
{
"name": "time",
"ordinal": 0,
"type_info": "Timestamptz"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "SELECT time FROM mcaptcha_pow_solved_stats \n WHERE config_id = (\n SELECT config_id FROM mcaptcha_config \n WHERE \n key = $1\n AND\n user_id = (\n SELECT \n ID FROM mcaptcha_users WHERE name = $2)) \n ORDER BY time DESC"
},
"90608e874ec931db397dc7b357b60bc794fffec5e2eb59c0556808ea8dfef9e9": {
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Int4"
},
{
"name": "password",
"ordinal": 1,
"type_info": "Text"
}
],
"nullable": [
false,
false
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT ID, password FROM mcaptcha_users WHERE name = ($1)"
},
"94901d49666b3097b1fed832966697c4a1e3937beb2bd0431df4857402a4de04": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Int4",
"Int4",
"Text",
"Text"
]
}
},
"query": "INSERT INTO mcaptcha_levels (\n difficulty_factor, \n visitor_threshold,\n config_id) VALUES (\n $1, $2, (\n SELECT config_id FROM mcaptcha_config WHERE key = ($3) AND\n user_id = (\n SELECT ID from mcaptcha_users WHERE name = $4\n )\n ));"
},
"9753721856a47438c5e72f28fd9d149db10c48e677b4613bf3f1e8487908aac8": {
"describe": {
"columns": [
{
"name": "difficulty_factor",
"ordinal": 0,
"type_info": "Int4"
},
{
"name": "visitor_threshold",
"ordinal": 1,
"type_info": "Int4"
}
],
"nullable": [
false,
false
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT difficulty_factor, visitor_threshold FROM mcaptcha_levels WHERE\n config_id = (\n SELECT config_id FROM mcaptcha_config WHERE key = ($1)\n ) ORDER BY difficulty_factor ASC;"
},
"9bfdbc25316c623f8f19bb24e636bf8d0c930a0604d84f576682d2fe60a631f6": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "DELETE FROM mcaptcha_sitekey_user_provided_avg_traffic \n WHERE config_id = (\n SELECT config_id \n FROM \n mcaptcha_config \n WHERE\n key = ($1) \n AND \n user_id = (SELECT ID FROM mcaptcha_users WHERE name = $2)\n );"
},
"9c7a654aefa0a1683d9b07ff00c8edb0ee292e003c13ec99a419e563591c15e4": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text",
"Int4"
]
}
},
"query": "DELETE FROM mcaptcha_config WHERE key = ($1) AND user_id = $2;"
},
"a1c49ee377d6ac57fb22c9eac0ef1927a97087abd58da092a91623d06fa7076e": {
"describe": {
"columns": [
{
"name": "name",
"ordinal": 0,
"type_info": "Varchar"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "SELECT name FROM mcaptcha_config \n WHERE key = $1 \n AND user_id = (\n SELECT user_id FROM mcaptcha_users WHERE NAME = $2)"
},
"ad23588ee4bcbb13e208460ce21e2fa9f1373893934b530b339fea10360b34a8": {
"describe": {
"columns": [
{
"name": "exists",
"ordinal": 0,
"type_info": "Bool"
}
],
"nullable": [
null
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT EXISTS (SELECT 1 from mcaptcha_users WHERE name = $1)"
},
"ada91fac02c7bba9b13deebccda6f6fc45773b5a6e786c37c27b4a71a5cd29f2": {
"describe": {
"columns": [
{
"name": "config_id",
"ordinal": 0,
"type_info": "Int4"
},
{
"name": "duration",
"ordinal": 1,
"type_info": "Int4"
},
{
"name": "name",
"ordinal": 2,
"type_info": "Varchar"
}
],
"nullable": [
false,
false,
false
],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "SELECT config_id, duration, name from mcaptcha_config WHERE\n key = $1 AND\n user_id = (SELECT ID FROM mcaptcha_users WHERE name = $2) "
},
"bdf2e2781bfa2e9c81c18ef8df7230809d3b20274685a35b1c544804f2a58241": {
"describe": {
"columns": [
{
"name": "name",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "password",
"ordinal": 1,
"type_info": "Text"
}
],
"nullable": [
false,
false
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT name, password FROM mcaptcha_users WHERE email = ($1)"
},
"c2e167e56242de7e0a835e25004b15ca8340545fa0ca7ac8f3293157d2d03d98": {
"describe": {
"columns": [
{
"name": "avg_traffic",
"ordinal": 0,
"type_info": "Int4"
},
{
"name": "peak_sustainable_traffic",
"ordinal": 1,
"type_info": "Int4"
},
{
"name": "broke_my_site_traffic",
"ordinal": 2,
"type_info": "Int4"
}
],
"nullable": [
false,
false,
true
],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "SELECT \n avg_traffic, \n peak_sustainable_traffic, \n broke_my_site_traffic \n FROM \n mcaptcha_sitekey_user_provided_avg_traffic \n WHERE \n config_id = (\n SELECT \n config_id \n FROM \n mcaptcha_config \n WHERE \n KEY = $1 \n AND user_id = (\n SELECT \n id \n FROM \n mcaptcha_users \n WHERE \n NAME = $2\n )\n )\n "
},
"c399efd5db1284dcb470c40f9b076851f77498c75a63a3b151d4a111bd3e2957": {
"describe": {
"columns": [
{
"name": "time",
"ordinal": 0,
"type_info": "Timestamptz"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "SELECT time FROM mcaptcha_pow_fetched_stats\n WHERE \n config_id = (\n SELECT \n config_id FROM mcaptcha_config \n WHERE \n key = $1\n AND\n user_id = (\n SELECT \n ID FROM mcaptcha_users WHERE name = $2))\n ORDER BY time DESC"
},
"ca9d5241f1234d1825f7ead391ebe9099fca776e7101ac6e1761881606def5fa": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "DELETE FROM mcaptcha_users WHERE name = ($1)"
},
"d85750d86bbafeaf6f52cec3d49d708bef1a9ef85bbd9c55d63c9c27cb93223c": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text",
"Int4"
]
}
},
"query": "DELETE FROM mcaptcha_levels \n WHERE config_id = (\n SELECT config_id FROM mcaptcha_config \n WHERE key = $1 AND user_id = $2\n );"
},
"dbe4307651d94bc6db4f1d8b2c6d076fde6280983d59593216d7765cbbdd669c": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text",
"Timestamptz"
]
}
},
"query": "INSERT INTO mcaptcha_pow_solved_stats \n (config_id, time) VALUES ((SELECT config_id FROM mcaptcha_config WHERE key = $1), $2)"
},
"dcf0d4f9d803dcb1d6f775899f79595f9c78d46633e0ec822303284430df7a3d": {
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Int4"
},
{
"name": "heading",
"ordinal": 1,
"type_info": "Varchar"
},
{
"name": "message",
"ordinal": 2,
"type_info": "Varchar"
},
{
"name": "received",
"ordinal": 3,
"type_info": "Timestamptz"
},
{
"name": "name",
"ordinal": 4,
"type_info": "Varchar"
}
],
"nullable": [
true,
true,
true,
true,
true
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "-- gets all unread notifications a user has\nSELECT \n mcaptcha_notifications.id,\n mcaptcha_notifications.heading,\n mcaptcha_notifications.message,\n mcaptcha_notifications.received,\n mcaptcha_users.name\nFROM\n mcaptcha_notifications \nINNER JOIN \n mcaptcha_users \nON \n mcaptcha_notifications.tx = mcaptcha_users.id\nWHERE \n mcaptcha_notifications.rx = (\n SELECT \n id \n FROM \n mcaptcha_users\n WHERE\n name = $1\n )\nAND \n mcaptcha_notifications.read IS NULL;\n"
},
"e4c710d33b709aee262fa0704372ac216d98851447ef4fbe221740b7ae4ea422": {
"describe": {
"columns": [
{
"name": "secret",
"ordinal": 0,
"type_info": "Varchar"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT secret FROM mcaptcha_users WHERE name = ($1)"
},
"e98d0614d982fe7c04d78d457c3ce79e8d4d0bcaac28c8a3edecdbc9def04ea2": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "UPDATE mcaptcha_users set password = $1\n WHERE name = $2"
},
"f330cb94c53d33495df94aacec7e4e91d8a920742b89a63d1c59a8ea8937c5c8": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Int4",
"Int4",
"Text",
"Text"
]
}
},
"query": "INSERT INTO mcaptcha_levels (\n difficulty_factor, \n visitor_threshold,\n config_id) VALUES (\n $1, $2, (\n SELECT config_id FROM mcaptcha_config WHERE\n key = ($3) AND user_id = (\n SELECT ID FROM mcaptcha_users WHERE name = $4\n )));"
}
} }

View File

@@ -32,15 +32,21 @@ pub async fn delete_account(
data: AppData, data: AppData,
) -> ServiceResult<impl Responder> { ) -> ServiceResult<impl Responder> {
use argon2_creds::Config; use argon2_creds::Config;
use sqlx::Error::RowNotFound;
let username = id.identity().unwrap(); let username = id.identity().unwrap();
let hash = data let rec = sqlx::query_as!(
.db Password,
.get_password(&db_core::Login::Username(&username)) r#"SELECT password FROM mcaptcha_users WHERE name = ($1)"#,
.await?; &username,
)
.fetch_one(&data.db)
.await;
if Config::verify(&hash.hash, &payload.password)? { match rec {
Ok(s) => {
if Config::verify(&s.password, &payload.password)? {
runners::delete_user(&username, &data).await?; runners::delete_user(&username, &data).await?;
id.forget(); id.forget();
Ok(HttpResponse::Ok()) Ok(HttpResponse::Ok())
@@ -48,13 +54,19 @@ pub async fn delete_account(
Err(ServiceError::WrongPassword) Err(ServiceError::WrongPassword)
} }
} }
Err(RowNotFound) => Err(ServiceError::AccountNotFound),
Err(_) => Err(ServiceError::InternalServerError),
}
}
pub mod runners { pub mod runners {
use super::*; use super::*;
pub async fn delete_user(name: &str, data: &AppData) -> ServiceResult<()> { pub async fn delete_user(name: &str, data: &AppData) -> ServiceResult<()> {
data.db.delete_user(name).await?; sqlx::query!("DELETE FROM mcaptcha_users WHERE name = ($1)", name,)
.execute(&data.db)
.await?;
Ok(()) Ok(())
} }
} }

View File

@@ -14,9 +14,10 @@
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::borrow::Cow;
use actix_identity::Identity; use actix_identity::Identity;
use actix_web::{web, HttpResponse, Responder}; use actix_web::{web, HttpResponse, Responder};
use db_core::UpdateEmail;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::{AccountCheckPayload, AccountCheckResp}; use super::{AccountCheckPayload, AccountCheckResp};
@@ -33,9 +34,20 @@ pub async fn email_exists(
payload: web::Json<AccountCheckPayload>, payload: web::Json<AccountCheckPayload>,
data: AppData, data: AppData,
) -> ServiceResult<impl Responder> { ) -> ServiceResult<impl Responder> {
let exists = data.db.email_exists(&payload.val).await?; let res = sqlx::query!(
"SELECT EXISTS (SELECT 1 from mcaptcha_users WHERE email = $1)",
&payload.val,
)
.fetch_one(&data.db)
.await?;
let resp = AccountCheckResp { exists }; let mut resp = AccountCheckResp { exists: false };
if let Some(x) = res.exists {
if x {
resp.exists = true;
}
}
Ok(HttpResponse::Ok().json(resp)) Ok(HttpResponse::Ok().json(resp))
} }
@@ -54,13 +66,25 @@ async fn set_email(
data.creds.email(&payload.email)?; data.creds.email(&payload.email)?;
let update_email = UpdateEmail { let res = sqlx::query!(
username: &username, "UPDATE mcaptcha_users set email = $1
new_email: &payload.email, WHERE name = $2",
&payload.email,
&username,
)
.execute(&data.db)
.await;
if res.is_err() {
if let Err(sqlx::Error::Database(err)) = res {
if err.code() == Some(Cow::from("23505"))
&& err.message().contains("mcaptcha_users_email_key")
{
return Err(ServiceError::EmailTaken);
} else {
return Err(sqlx::Error::Database(err).into());
}
}; };
}
data.db.update_email(&update_email).await?;
Ok(HttpResponse::Ok()) Ok(HttpResponse::Ok())
} }

View File

@@ -17,9 +17,10 @@
use actix_identity::Identity; use actix_identity::Identity;
use actix_web::{web, HttpResponse, Responder}; use actix_web::{web, HttpResponse, Responder};
use argon2_creds::Config; use argon2_creds::Config;
use db_core::Login;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::Error::RowNotFound;
use crate::api::v1::auth::runners::Password;
use crate::errors::*; use crate::errors::*;
use crate::*; use crate::*;
@@ -55,12 +56,15 @@ async fn update_password_runner(
let new_hash = data.creds.password(&update.new_password)?; let new_hash = data.creds.password(&update.new_password)?;
let p = db_core::NameHash { sqlx::query!(
username: user.to_owned(), "UPDATE mcaptcha_users set password = $1
hash: new_hash, WHERE name = $2",
}; &new_hash,
&user,
)
.execute(&data.db)
.await?;
data.db.update_password(&p).await?;
Ok(()) Ok(())
} }
@@ -79,10 +83,17 @@ async fn update_user_password(
let username = id.identity().unwrap(); let username = id.identity().unwrap();
// TODO: verify behavior when account is not found let rec = sqlx::query_as!(
let res = data.db.get_password(&Login::Username(&username)).await?; Password,
r#"SELECT password FROM mcaptcha_users WHERE name = ($1)"#,
&username,
)
.fetch_one(&data.db)
.await;
if Config::verify(&res.hash, &payload.password)? { match rec {
Ok(s) => {
if Config::verify(&s.password, &payload.password)? {
let update: UpdatePassword = payload.into_inner().into(); let update: UpdatePassword = payload.into_inner().into();
update_password_runner(&username, update, &data).await?; update_password_runner(&username, update, &data).await?;
Ok(HttpResponse::Ok()) Ok(HttpResponse::Ok())
@@ -90,33 +101,38 @@ async fn update_user_password(
Err(ServiceError::WrongPassword) Err(ServiceError::WrongPassword)
} }
} }
Err(RowNotFound) => Err(ServiceError::AccountNotFound),
Err(_) => Err(ServiceError::InternalServerError),
}
}
pub fn services(cfg: &mut actix_web::web::ServiceConfig) { pub fn services(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(update_user_password); cfg.service(update_user_password);
} }
#[cfg(test)] #[cfg(test)]
pub mod tests { mod tests {
use super::*; use super::*;
use actix_web::http::StatusCode; use actix_web::http::StatusCode;
use actix_web::test; use actix_web::test;
use crate::api::v1::ROUTES; use crate::api::v1::ROUTES;
use crate::data::Data;
use crate::tests::*; use crate::tests::*;
#[actix_rt::test] #[actix_rt::test]
pub async fn update_password_works() { async fn update_password_works() {
const NAME: &str = "updatepassuser"; const NAME: &str = "updatepassuser";
const PASSWORD: &str = "longpassword2"; const PASSWORD: &str = "longpassword2";
const EMAIL: &str = "updatepassuser@a.com"; const EMAIL: &str = "updatepassuser@a.com";
let data = get_data().await; {
let data = &data; let data = Data::new().await;
delete_user(NAME, &data).await;
}
delete_user(data, NAME).await; let (data, _, signin_resp) = register_and_signin(NAME, EMAIL, PASSWORD).await;
let (_, signin_resp) = register_and_signin(data, NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp); let cookies = get_cookie!(signin_resp);
let app = get_app!(data).await; let app = get_app!(data).await;
@@ -128,7 +144,7 @@ pub mod tests {
confirm_new_password: PASSWORD.into(), confirm_new_password: PASSWORD.into(),
}; };
let res = update_password_runner(NAME, update_password.into(), data).await; let res = update_password_runner(NAME, update_password.into(), &data).await;
assert!(res.is_err()); assert!(res.is_err());
assert_eq!(res, Err(ServiceError::PasswordsDontMatch)); assert_eq!(res, Err(ServiceError::PasswordsDontMatch));
@@ -138,7 +154,7 @@ pub mod tests {
confirm_new_password: new_password.into(), confirm_new_password: new_password.into(),
}; };
assert!(update_password_runner(NAME, update_password.into(), data) assert!(update_password_runner(NAME, update_password.into(), &data)
.await .await
.is_ok()); .is_ok());
@@ -149,7 +165,6 @@ pub mod tests {
}; };
bad_post_req_test( bad_post_req_test(
data,
NAME, NAME,
new_password, new_password,
ROUTES.account.update_password, ROUTES.account.update_password,
@@ -165,7 +180,6 @@ pub mod tests {
}; };
bad_post_req_test( bad_post_req_test(
data,
NAME, NAME,
new_password, new_password,
ROUTES.account.update_password, ROUTES.account.update_password,

View File

@@ -14,21 +14,36 @@
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::borrow::Cow;
use actix_identity::Identity; use actix_identity::Identity;
use actix_web::{HttpResponse, Responder}; use actix_web::{HttpResponse, Responder};
use db_core::prelude::*; use serde::{Deserialize, Serialize};
use crate::api::v1::mcaptcha::get_random; use crate::api::v1::mcaptcha::get_random;
use crate::errors::*; use crate::errors::*;
use crate::AppData; use crate::AppData;
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Secret {
pub secret: String,
}
#[my_codegen::get( #[my_codegen::get(
path = "crate::V1_API_ROUTES.account.get_secret", path = "crate::V1_API_ROUTES.account.get_secret",
wrap = "crate::api::v1::get_middleware()" wrap = "crate::api::v1::get_middleware()"
)] )]
async fn get_secret(id: Identity, data: AppData) -> ServiceResult<impl Responder> { async fn get_secret(id: Identity, data: AppData) -> ServiceResult<impl Responder> {
let username = id.identity().unwrap(); let username = id.identity().unwrap();
let secret = data.db.get_secret(&username).await?;
let secret = sqlx::query_as!(
Secret,
r#"SELECT secret FROM mcaptcha_users WHERE name = ($1)"#,
&username,
)
.fetch_one(&data.db)
.await?;
Ok(HttpResponse::Ok().json(secret)) Ok(HttpResponse::Ok().json(secret))
} }
@@ -46,14 +61,26 @@ async fn update_user_secret(
loop { loop {
secret = get_random(32); secret = get_random(32);
let res = sqlx::query!(
match data.db.update_secret(&username, &secret).await { "UPDATE mcaptcha_users set secret = $1
Ok(_) => break, WHERE name = $2",
Err(DBError::SecretTaken) => continue, &secret,
Err(e) => return Err(e.into()), &username,
)
.execute(&data.db)
.await;
if res.is_ok() {
break;
} else if let Err(sqlx::Error::Database(err)) = res {
if err.code() == Some(Cow::from("23505"))
&& err.message().contains("mcaptcha_users_secret_key")
{
continue;
} else {
return Err(sqlx::Error::Database(err).into());
}
} }
} }
Ok(HttpResponse::Ok()) Ok(HttpResponse::Ok())
} }

View File

@@ -23,21 +23,24 @@ use super::username::Username;
use super::*; use super::*;
use crate::api::v1::auth::runners::Password; use crate::api::v1::auth::runners::Password;
use crate::api::v1::ROUTES; use crate::api::v1::ROUTES;
use crate::data::Data;
use crate::*; use crate::*;
use crate::errors::*; use crate::errors::*;
use crate::tests::*; use crate::tests::*;
#[actix_rt::test] #[actix_rt::test]
pub async fn uname_email_exists_works() { async fn uname_email_exists_works() {
const NAME: &str = "testuserexists"; const NAME: &str = "testuserexists";
const PASSWORD: &str = "longpassword2"; const PASSWORD: &str = "longpassword2";
const EMAIL: &str = "testuserexists@a.com2"; const EMAIL: &str = "testuserexists@a.com2";
let data = get_data().await;
let data = &data;
delete_user(data, NAME).await;
let (_, signin_resp) = register_and_signin(data, NAME, EMAIL, PASSWORD).await; {
let data = Data::new().await;
delete_user(NAME, &data).await;
}
let (data, _, signin_resp) = register_and_signin(NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp); let cookies = get_cookie!(signin_resp);
let app = get_app!(data).await; let app = get_app!(data).await;
@@ -115,20 +118,21 @@ pub async fn uname_email_exists_works() {
} }
#[actix_rt::test] #[actix_rt::test]
pub async fn email_udpate_password_validation_del_userworks() { async fn email_udpate_password_validation_del_userworks() {
const NAME: &str = "testuser2"; const NAME: &str = "testuser2";
const PASSWORD: &str = "longpassword2"; const PASSWORD: &str = "longpassword2";
const EMAIL: &str = "testuser1@a.com2"; const EMAIL: &str = "testuser1@a.com2";
const NAME2: &str = "eupdauser"; const NAME2: &str = "eupdauser";
const EMAIL2: &str = "eupdauser@a.com"; const EMAIL2: &str = "eupdauser@a.com";
let data = get_data().await; {
let data = &data; let data = Data::new().await;
delete_user(data, NAME).await; delete_user(NAME, &data).await;
delete_user(data, NAME2).await; delete_user(NAME2, &data).await;
}
let _ = register_and_signin(data, NAME2, EMAIL2, PASSWORD).await; let _ = register_and_signin(NAME2, EMAIL2, PASSWORD).await;
let (_creds, signin_resp) = register_and_signin(data, NAME, EMAIL, PASSWORD).await; let (data, _creds, signin_resp) = register_and_signin(NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp); let cookies = get_cookie!(signin_resp);
let app = get_app!(data).await; let app = get_app!(data).await;
@@ -149,7 +153,6 @@ pub async fn email_udpate_password_validation_del_userworks() {
// check duplicate email while duplicate email // check duplicate email while duplicate email
email_payload.email = EMAIL2.into(); email_payload.email = EMAIL2.into();
bad_post_req_test( bad_post_req_test(
data,
NAME, NAME,
PASSWORD, PASSWORD,
ROUTES.account.update_email, ROUTES.account.update_email,
@@ -163,7 +166,6 @@ pub async fn email_udpate_password_validation_del_userworks() {
password: NAME.into(), password: NAME.into(),
}; };
bad_post_req_test( bad_post_req_test(
data,
NAME, NAME,
PASSWORD, PASSWORD,
ROUTES.account.delete, ROUTES.account.delete,
@@ -198,7 +200,7 @@ pub async fn email_udpate_password_validation_del_userworks() {
} }
#[actix_rt::test] #[actix_rt::test]
pub async fn username_update_works() { async fn username_update_works() {
const NAME: &str = "testuserupda"; const NAME: &str = "testuserupda";
const EMAIL: &str = "testuserupda@sss.com"; const EMAIL: &str = "testuserupda@sss.com";
const EMAIL2: &str = "testuserupda2@sss.com"; const EMAIL2: &str = "testuserupda2@sss.com";
@@ -206,17 +208,18 @@ pub async fn username_update_works() {
const NAME2: &str = "terstusrtds"; const NAME2: &str = "terstusrtds";
const NAME_CHANGE: &str = "terstusrtdsxx"; const NAME_CHANGE: &str = "terstusrtdsxx";
let data = get_data().await; {
let data = &data; let data = Data::new().await;
futures::join!( futures::join!(
delete_user(data, NAME), delete_user(NAME, &data),
delete_user(data, NAME2), delete_user(NAME2, &data),
delete_user(data, NAME_CHANGE), delete_user(NAME_CHANGE, &data)
); );
}
let _ = register_and_signin(data, NAME2, EMAIL2, PASSWORD).await; let _ = register_and_signin(NAME2, EMAIL2, PASSWORD).await;
let (_creds, signin_resp) = register_and_signin(data, NAME, EMAIL, PASSWORD).await; let (data, _creds, signin_resp) = register_and_signin(NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp); let cookies = get_cookie!(signin_resp);
let app = get_app!(data).await; let app = get_app!(data).await;
@@ -236,7 +239,6 @@ pub async fn username_update_works() {
// check duplicate username with duplicate username // check duplicate username with duplicate username
username_udpate.username = NAME2.into(); username_udpate.username = NAME2.into();
bad_post_req_test( bad_post_req_test(
data,
NAME_CHANGE, NAME_CHANGE,
PASSWORD, PASSWORD,
ROUTES.account.update_username, ROUTES.account.update_username,

View File

@@ -14,6 +14,8 @@
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::borrow::Cow;
use actix_identity::Identity; use actix_identity::Identity;
use actix_web::{web, HttpResponse, Responder}; use actix_web::{web, HttpResponse, Responder};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@@ -38,9 +40,22 @@ pub mod runners {
payload: &AccountCheckPayload, payload: &AccountCheckPayload,
data: &AppData, data: &AppData,
) -> ServiceResult<AccountCheckResp> { ) -> ServiceResult<AccountCheckResp> {
let exists = data.db.username_exists(&payload.val).await?; let res = sqlx::query!(
"SELECT EXISTS (SELECT 1 from mcaptcha_users WHERE name = $1)",
&payload.val,
)
.fetch_one(&data.db)
.await?;
Ok(AccountCheckResp { exists }) let mut resp = AccountCheckResp { exists: false };
if let Some(x) = res.exists {
if x {
resp.exists = true;
}
}
Ok(resp)
} }
} }
@@ -63,8 +78,26 @@ async fn set_username(
let processed_uname = data.creds.username(&payload.username)?; let processed_uname = data.creds.username(&payload.username)?;
data.db.update_username(&username, &processed_uname).await?; let res = sqlx::query!(
"UPDATE mcaptcha_users set name = $1
WHERE name = $2",
&processed_uname,
&username,
)
.execute(&data.db)
.await;
if res.is_err() {
if let Err(sqlx::Error::Database(err)) = res {
if err.code() == Some(Cow::from("23505"))
&& err.message().contains("mcaptcha_users_name_key")
{
return Err(ServiceError::UsernameTaken);
} else {
return Err(sqlx::Error::Database(err).into());
}
};
}
id.forget(); id.forget();
id.remember(processed_uname); id.remember(processed_uname);

View File

@@ -18,7 +18,6 @@
use actix_identity::Identity; use actix_identity::Identity;
use actix_web::http::header; use actix_web::http::header;
use actix_web::{web, HttpResponse, Responder}; use actix_web::{web, HttpResponse, Responder};
use db_core::errors::DBError;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::mcaptcha::get_random; use super::mcaptcha::get_random;
@@ -63,6 +62,8 @@ pub mod routes {
} }
pub mod runners { pub mod runners {
use std::borrow::Cow;
use super::*; use super::*;
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
@@ -89,6 +90,7 @@ pub mod runners {
/// returns Ok(()) when everything checks out and the user is authenticated. Erros otherwise /// returns Ok(()) when everything checks out and the user is authenticated. Erros otherwise
pub async fn login_runner(payload: Login, data: &AppData) -> ServiceResult<String> { pub async fn login_runner(payload: Login, data: &AppData) -> ServiceResult<String> {
use argon2_creds::Config; use argon2_creds::Config;
use sqlx::Error::RowNotFound;
let verify = |stored: &str, received: &str| { let verify = |stored: &str, received: &str| {
if Config::verify(stored, received)? { if Config::verify(stored, received)? {
@@ -98,24 +100,55 @@ pub mod runners {
} }
}; };
let s = if payload.login.contains('@') { if payload.login.contains('@') {
data.db #[derive(Clone, Debug)]
.get_password(&db_core::Login::Email(&payload.login)) struct EmailLogin {
.await? name: String,
} else { password: String,
data.db
.get_password(&db_core::Login::Username(&payload.login))
.await?
};
verify(&s.hash, &payload.password)?;
Ok(s.username)
} }
let email_fut = sqlx::query_as!(
EmailLogin,
r#"SELECT name, password FROM mcaptcha_users WHERE email = ($1)"#,
&payload.login,
)
.fetch_one(&data.db)
.await;
match email_fut {
Ok(s) => {
verify(&s.password, &payload.password)?;
Ok(s.name)
}
Err(RowNotFound) => Err(ServiceError::AccountNotFound),
Err(_) => Err(ServiceError::InternalServerError),
}
} else {
let username_fut = sqlx::query_as!(
Password,
r#"SELECT password FROM mcaptcha_users WHERE name = ($1)"#,
&payload.login,
)
.fetch_one(&data.db)
.await;
match username_fut {
Ok(s) => {
verify(&s.password, &payload.password)?;
Ok(payload.login)
}
Err(RowNotFound) => Err(ServiceError::AccountNotFound),
Err(_) => Err(ServiceError::InternalServerError),
}
}
}
pub async fn register_runner( pub async fn register_runner(
payload: &Register, payload: &Register,
data: &AppData, data: &AppData,
) -> ServiceResult<()> { ) -> ServiceResult<()> {
if !data.settings.allow_registration { if !crate::SETTINGS.allow_registration {
return Err(ServiceError::ClosedForRegistration); return Err(ServiceError::ClosedForRegistration);
} }
@@ -133,21 +166,48 @@ pub mod runners {
loop { loop {
secret = get_random(32); secret = get_random(32);
let res;
let p = db_core::Register { if let Some(email) = &payload.email {
username: &username, res = sqlx::query!(
hash: &hash, "insert into mcaptcha_users
email: payload.email.as_deref(), (name , password, email, secret) values ($1, $2, $3, $4)",
secret: &secret, &username,
&hash,
&email,
&secret,
)
.execute(&data.db)
.await;
} else {
res = sqlx::query!(
"INSERT INTO mcaptcha_users
(name , password, secret) VALUES ($1, $2, $3)",
&username,
&hash,
&secret,
)
.execute(&data.db)
.await;
}
if res.is_ok() {
break;
} else if let Err(sqlx::Error::Database(err)) = res {
if err.code() == Some(Cow::from("23505")) {
let msg = err.message();
if msg.contains("mcaptcha_users_name_key") {
return Err(ServiceError::UsernameTaken);
} else if msg.contains("mcaptcha_users_email_key") {
return Err(ServiceError::EmailTaken);
} else if msg.contains("mcaptcha_users_secret_key") {
continue;
} else {
return Err(ServiceError::InternalServerError);
}
} else {
return Err(sqlx::Error::Database(err).into());
}
}; };
match data.db.register(&p).await {
Ok(_) => break,
Err(DBError::SecretTaken) => continue,
Err(e) => return Err(e.into()),
} }
}
Ok(()) Ok(())
} }
} }

View File

@@ -14,14 +14,13 @@
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::borrow::Cow;
use actix_identity::Identity; use actix_identity::Identity;
use actix_web::{web, HttpResponse, Responder}; use actix_web::{web, HttpResponse, Responder};
use libmcaptcha::defense::Level; use libmcaptcha::defense::Level;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use db_core::errors::DBError;
use db_core::CreateCaptcha as DBCreateCaptcha;
use super::get_random; use super::get_random;
use crate::errors::*; use crate::errors::*;
use crate::AppData; use crate::AppData;
@@ -56,8 +55,11 @@ pub async fn create(
} }
pub mod runner { pub mod runner {
use super::*; use futures::future::try_join_all;
use libmcaptcha::DefenseBuilder; use libmcaptcha::DefenseBuilder;
use log::debug;
use super::*;
pub async fn create( pub async fn create(
payload: &CreateCaptcha, payload: &CreateCaptcha,
@@ -71,29 +73,81 @@ pub mod runner {
defense.build()?; defense.build()?;
debug!("creating config");
let mcaptcha_config =
// add_mcaptcha_util(payload.duration, &payload.description, &data, username).await?;
{
let mut key; let mut key;
let duration = payload.duration as i32;
let resp;
loop { loop {
key = get_random(32); key = get_random(32);
let p = DBCreateCaptcha {
description: &payload.description, let res = sqlx::query!(
key: &key, "INSERT INTO mcaptcha_config
duration, (key, user_id, duration, name)
VALUES ($1, (SELECT ID FROM mcaptcha_users WHERE name = $2), $3, $4)",
&key,
&username,
payload.duration as i32,
&payload.description,
)
.execute(&data.db)
.await;
match res {
Err(sqlx::Error::Database(err)) => {
if err.code() == Some(Cow::from("23505"))
&& err.message().contains("mcaptcha_config_key_key")
{
continue;
} else {
return Err(sqlx::Error::Database(err).into());
}
}
Err(e) => return Err(e.into()),
Ok(_) => {
resp = MCaptchaDetails {
key,
name: payload.description.to_owned(),
};
break;
}
}
}
resp
}; };
match data.db.create_captcha(username, &p).await { debug!("config created");
Ok(_) => break,
Err(DBError::SecretTaken) => continue, let mut futs = Vec::with_capacity(payload.levels.len());
Err(e) => return Err(e.into()),
for level in payload.levels.iter() {
let difficulty_factor = level.difficulty_factor as i32;
let visitor_threshold = level.visitor_threshold as i32;
let fut = sqlx::query!(
"INSERT INTO mcaptcha_levels (
difficulty_factor,
visitor_threshold,
config_id) VALUES (
$1, $2, (
SELECT config_id FROM mcaptcha_config WHERE
key = ($3) AND user_id = (
SELECT ID FROM mcaptcha_users WHERE name = $4
)));",
difficulty_factor,
visitor_threshold,
&mcaptcha_config.key,
&username,
)
.execute(&data.db);
futs.push(fut);
} }
}
data.db try_join_all(futs).await?;
.add_captcha_levels(username, &key, &payload.levels)
.await?;
let mcaptcha_config = MCaptchaDetails {
name: payload.description.clone(),
key,
};
Ok(mcaptcha_config) Ok(mcaptcha_config)
} }
} }

View File

@@ -19,8 +19,6 @@ use actix_web::{web, HttpResponse, Responder};
use libmcaptcha::master::messages::RemoveCaptcha; use libmcaptcha::master::messages::RemoveCaptcha;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use db_core::Login;
use crate::errors::*; use crate::errors::*;
use crate::AppData; use crate::AppData;
@@ -40,19 +38,58 @@ async fn delete(
id: Identity, id: Identity,
) -> ServiceResult<impl Responder> { ) -> ServiceResult<impl Responder> {
use argon2_creds::Config; use argon2_creds::Config;
use sqlx::Error::RowNotFound;
let username = id.identity().unwrap(); let username = id.identity().unwrap();
let hash = data.db.get_password(&Login::Username(&username)).await?; struct PasswordID {
password: String,
if !Config::verify(&hash.hash, &payload.password)? { id: i32,
return Err(ServiceError::WrongPassword);
} }
let payload = payload.into_inner();
data.db.delete_captcha(&username, &payload.key).await?;
let rec = sqlx::query_as!(
PasswordID,
r#"SELECT ID, password FROM mcaptcha_users WHERE name = ($1)"#,
&username,
)
.fetch_one(&data.db)
.await;
match rec {
Ok(rec) => {
if Config::verify(&rec.password, &payload.password)? {
let payload = payload.into_inner();
sqlx::query!(
"DELETE FROM mcaptcha_levels
WHERE config_id = (
SELECT config_id FROM mcaptcha_config
WHERE key = $1 AND user_id = $2
);",
&payload.key,
&rec.id,
)
.execute(&data.db)
.await?;
sqlx::query!(
"DELETE FROM mcaptcha_config WHERE key = ($1) AND user_id = $2;",
&payload.key,
&rec.id,
)
.execute(&data.db)
.await?;
if let Err(err) = data.captcha.remove(RemoveCaptcha(payload.key)).await { if let Err(err) = data.captcha.remove(RemoveCaptcha(payload.key)).await {
log::error!("Error while trying to remove captcha from cache {}", err); log::error!(
"Error while trying to remove captcha from cache {}",
err
);
} }
Ok(HttpResponse::Ok()) Ok(HttpResponse::Ok())
} else {
Err(ServiceError::WrongPassword)
}
}
Err(RowNotFound) => Err(ServiceError::UsernameNotFound),
Err(_) => Err(ServiceError::InternalServerError),
}
} }

View File

@@ -19,8 +19,6 @@ use actix_web::{web, HttpResponse, Responder};
use libmcaptcha::{defense::Level, defense::LevelBuilder}; use libmcaptcha::{defense::Level, defense::LevelBuilder};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use db_core::TrafficPattern;
use super::create::{runner::create as create_runner, CreateCaptcha}; use super::create::{runner::create as create_runner, CreateCaptcha};
use super::update::{runner::update_captcha as update_captcha_runner, UpdateCaptcha}; use super::update::{runner::update_captcha as update_captcha_runner, UpdateCaptcha};
use crate::errors::*; use crate::errors::*;
@@ -49,53 +47,39 @@ pub fn services(cfg: &mut web::ServiceConfig) {
cfg.service(create); cfg.service(create);
} }
#[derive(Default, Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
/// User's traffic pattern; used in generating a captcha configuration pub struct TrafficPattern {
pub struct TrafficPatternRequest {
/// average traffic of user's website
pub avg_traffic: u32, pub avg_traffic: u32,
/// the peak traffic that the user's website can handle
pub peak_sustainable_traffic: u32, pub peak_sustainable_traffic: u32,
/// trafic that bought the user's website down; optional
pub broke_my_site_traffic: Option<u32>, pub broke_my_site_traffic: Option<u32>,
/// Captcha description
pub description: String, pub description: String,
} }
impl From<&TrafficPatternRequest> for TrafficPattern { impl TrafficPattern {
fn from(t: &TrafficPatternRequest) -> Self {
TrafficPattern {
avg_traffic: t.avg_traffic,
peak_sustainable_traffic: t.peak_sustainable_traffic,
broke_my_site_traffic: t.broke_my_site_traffic,
}
}
}
pub fn calculate( pub fn calculate(
tp: &TrafficPattern, &self,
strategy: &DefaultDifficultyStrategy, strategy: &DefaultDifficultyStrategy,
) -> ServiceResult<Vec<Level>> { ) -> ServiceResult<Vec<Level>> {
let mut levels = vec![ let mut levels = vec![
LevelBuilder::default() LevelBuilder::default()
.difficulty_factor(strategy.avg_traffic_difficulty)? .difficulty_factor(strategy.avg_traffic_difficulty)?
.visitor_threshold(tp.avg_traffic) .visitor_threshold(self.avg_traffic)
.build()?, .build()?,
LevelBuilder::default() LevelBuilder::default()
.difficulty_factor(strategy.peak_sustainable_traffic_difficulty)? .difficulty_factor(strategy.peak_sustainable_traffic_difficulty)?
.visitor_threshold(tp.peak_sustainable_traffic) .visitor_threshold(self.peak_sustainable_traffic)
.build()?, .build()?,
]; ];
let mut highest_level = LevelBuilder::default(); let mut highest_level = LevelBuilder::default();
highest_level.difficulty_factor(strategy.broke_my_site_traffic_difficulty)?; highest_level.difficulty_factor(strategy.broke_my_site_traffic_difficulty)?;
match tp.broke_my_site_traffic { match self.broke_my_site_traffic {
Some(broke_my_site_traffic) => { Some(broke_my_site_traffic) => {
highest_level.visitor_threshold(broke_my_site_traffic) highest_level.visitor_threshold(broke_my_site_traffic)
} }
None => match tp None => match self
.peak_sustainable_traffic .peak_sustainable_traffic
.checked_add(tp.peak_sustainable_traffic / 2) .checked_add(self.peak_sustainable_traffic / 2)
{ {
Some(num) => highest_level.visitor_threshold(num), Some(num) => highest_level.visitor_threshold(num),
// TODO check for overflow: database saves these values as i32, so this u32 is cast // TODO check for overflow: database saves these values as i32, so this u32 is cast
@@ -108,37 +92,58 @@ pub fn calculate(
Ok(levels) Ok(levels)
} }
}
#[my_codegen::post( #[my_codegen::post(
path = "crate::V1_API_ROUTES.captcha.easy.create", path = "crate::V1_API_ROUTES.captcha.easy.create",
wrap = "crate::api::v1::get_middleware()" wrap = "crate::api::v1::get_middleware()"
)] )]
async fn create( async fn create(
payload: web::Json<TrafficPatternRequest>, payload: web::Json<TrafficPattern>,
data: AppData, data: AppData,
id: Identity, id: Identity,
) -> ServiceResult<impl Responder> { ) -> ServiceResult<impl Responder> {
let username = id.identity().unwrap(); let username = id.identity().unwrap();
let payload = payload.into_inner(); let payload = payload.into_inner();
let pattern = (&payload).into();
let levels = let levels =
calculate(&pattern, &data.settings.captcha.default_difficulty_strategy)?; payload.calculate(&crate::SETTINGS.captcha.default_difficulty_strategy)?;
let msg = CreateCaptcha { let msg = CreateCaptcha {
levels, levels,
duration: data.settings.captcha.default_difficulty_strategy.duration, duration: crate::SETTINGS.captcha.default_difficulty_strategy.duration,
description: payload.description, description: payload.description,
}; };
let broke_my_site_traffic = payload.broke_my_site_traffic.map(|n| n as i32);
let mcaptcha_config = create_runner(&msg, &data, &username).await?; let mcaptcha_config = create_runner(&msg, &data, &username).await?;
data.db sqlx::query!(
.add_traffic_pattern(&username, &mcaptcha_config.key, &pattern) "INSERT INTO mcaptcha_sitekey_user_provided_avg_traffic (
config_id,
avg_traffic,
peak_sustainable_traffic,
broke_my_site_traffic
) VALUES (
(SELECT config_id FROM mcaptcha_config
WHERE
key = ($1)
AND user_id = (SELECT ID FROM mcaptcha_users WHERE name = $2)
), $3, $4, $5)",
//payload.avg_traffic,
&mcaptcha_config.key,
&username,
payload.avg_traffic as i32,
payload.peak_sustainable_traffic as i32,
broke_my_site_traffic,
)
.execute(&data.db)
.await?; .await?;
Ok(HttpResponse::Ok().json(mcaptcha_config)) Ok(HttpResponse::Ok().json(mcaptcha_config))
} }
#[derive(Serialize, Deserialize, Clone, Debug)] #[derive(Serialize, Deserialize, Clone, Debug)]
pub struct UpdateTrafficPattern { pub struct UpdateTrafficPattern {
pub pattern: TrafficPatternRequest, pub pattern: TrafficPattern,
pub key: String, pub key: String,
} }
@@ -153,30 +158,65 @@ async fn update(
) -> ServiceResult<impl Responder> { ) -> ServiceResult<impl Responder> {
let username = id.identity().unwrap(); let username = id.identity().unwrap();
let payload = payload.into_inner(); let payload = payload.into_inner();
let pattern = (&payload.pattern).into(); let levels = payload
let levels = .pattern
calculate(&pattern, &data.settings.captcha.default_difficulty_strategy)?; .calculate(&crate::SETTINGS.captcha.default_difficulty_strategy)?;
let msg = UpdateCaptcha { let msg = UpdateCaptcha {
levels, levels,
duration: data.settings.captcha.default_difficulty_strategy.duration, duration: crate::SETTINGS.captcha.default_difficulty_strategy.duration,
description: payload.pattern.description, description: payload.pattern.description,
key: payload.key, key: payload.key,
}; };
update_captcha_runner(&msg, &data, &username).await?; update_captcha_runner(&msg, &data, &username).await?;
data.db.delete_traffic_pattern(&username, &msg.key).await?; sqlx::query!(
"DELETE FROM mcaptcha_sitekey_user_provided_avg_traffic
WHERE config_id = (
SELECT config_id
FROM
mcaptcha_config
WHERE
key = ($1)
AND
user_id = (SELECT ID FROM mcaptcha_users WHERE name = $2)
);",
&msg.key,
&username,
)
.execute(&data.db)
.await?;
data.db let broke_my_site_traffic = payload.pattern.broke_my_site_traffic.map(|n| n as i32);
.add_traffic_pattern(&username, &msg.key, &pattern)
sqlx::query!(
"INSERT INTO mcaptcha_sitekey_user_provided_avg_traffic (
config_id,
avg_traffic,
peak_sustainable_traffic,
broke_my_site_traffic
) VALUES (
(SELECT config_id FROM mcaptcha_config
WHERE
key = ($1)
AND user_id = (SELECT ID FROM mcaptcha_users WHERE name = $2)
), $3, $4, $5)",
//payload.avg_traffic,
&msg.key,
&username,
payload.pattern.avg_traffic as i32,
payload.pattern.peak_sustainable_traffic as i32,
broke_my_site_traffic,
)
.execute(&data.db)
.await?; .await?;
Ok(HttpResponse::Ok()) Ok(HttpResponse::Ok())
} }
#[cfg(test)] #[cfg(test)]
pub mod tests { mod tests {
use actix_web::http::StatusCode; use actix_web::http::StatusCode;
use actix_web::test; use actix_web::test;
use actix_web::web::Bytes; use actix_web::web::Bytes;
@@ -187,22 +227,22 @@ pub mod tests {
use crate::tests::*; use crate::tests::*;
use crate::*; use crate::*;
#[cfg(test)]
mod isoloated_test { mod isoloated_test {
use super::{calculate, LevelBuilder}; use super::{LevelBuilder, TrafficPattern};
use db_core::TrafficPattern;
#[test] #[test]
fn easy_configuration_works() { fn easy_configuration_works() {
let settings = crate::tests::get_settings(); const NAME: &str = "defaultuserconfgworks";
let mut payload = TrafficPattern { let mut payload = TrafficPattern {
avg_traffic: 100_000, avg_traffic: 100_000,
peak_sustainable_traffic: 1_000_000, peak_sustainable_traffic: 1_000_000,
broke_my_site_traffic: Some(10_000_000), broke_my_site_traffic: Some(10_000_000),
description: NAME.into(),
}; };
let strategy = &settings.captcha.default_difficulty_strategy; let strategy = &crate::SETTINGS.captcha.default_difficulty_strategy;
let l1 = LevelBuilder::default() let l1 = LevelBuilder::default()
.difficulty_factor(strategy.avg_traffic_difficulty) .difficulty_factor(strategy.avg_traffic_difficulty)
.unwrap() .unwrap()
@@ -224,7 +264,7 @@ pub mod tests {
.unwrap(); .unwrap();
let levels = vec![l1, l2, l3]; let levels = vec![l1, l2, l3];
assert_eq!(calculate(&payload, strategy).unwrap(), levels); assert_eq!(payload.calculate(strategy).unwrap(), levels);
let estimated_lmax = LevelBuilder::default() let estimated_lmax = LevelBuilder::default()
.difficulty_factor(strategy.broke_my_site_traffic_difficulty) .difficulty_factor(strategy.broke_my_site_traffic_difficulty)
@@ -234,7 +274,7 @@ pub mod tests {
.unwrap(); .unwrap();
payload.broke_my_site_traffic = None; payload.broke_my_site_traffic = None;
assert_eq!( assert_eq!(
calculate(&payload, strategy).unwrap(), payload.calculate(strategy).unwrap(),
vec![l1, l2, estimated_lmax] vec![l1, l2, estimated_lmax]
); );
@@ -256,38 +296,37 @@ pub mod tests {
// payload.broke_my_site_traffic = Some(very_large_l2_peak_traffic); // payload.broke_my_site_traffic = Some(very_large_l2_peak_traffic);
payload.peak_sustainable_traffic = very_large_l2_peak_traffic; payload.peak_sustainable_traffic = very_large_l2_peak_traffic;
assert_eq!( assert_eq!(
calculate(&payload, strategy).unwrap(), payload.calculate(strategy).unwrap(),
vec![l1, very_large_l2, lmax] vec![l1, very_large_l2, lmax]
); );
} }
} }
#[actix_rt::test] #[actix_rt::test]
pub async fn easy_works() { async fn easy_works() {
const NAME: &str = "defaultuserconfgworks"; const NAME: &str = "defaultuserconfgworks";
const PASSWORD: &str = "longpassworddomain"; const PASSWORD: &str = "longpassworddomain";
const EMAIL: &str = "defaultuserconfgworks@a.com"; const EMAIL: &str = "defaultuserconfgworks@a.com";
let data = crate::tests::get_data().await;
let data = &data;
delete_user(data, NAME).await; {
let data = Data::new().await;
delete_user(NAME, &data).await;
}
let (_creds, signin_resp) = let (data, _creds, signin_resp) =
register_and_signin(data, NAME, EMAIL, PASSWORD).await; register_and_signin(NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp); let cookies = get_cookie!(signin_resp);
let app = get_app!(data).await; let app = get_app!(data).await;
let payload = TrafficPatternRequest { let payload = TrafficPattern {
avg_traffic: 100_000, avg_traffic: 100_000,
peak_sustainable_traffic: 1_000_000, peak_sustainable_traffic: 1_000_000,
broke_my_site_traffic: Some(10_000_000), broke_my_site_traffic: Some(10_000_000),
description: NAME.into(), description: NAME.into(),
}; };
let default_levels = calculate( let default_levels = payload
&(&payload).into(), .calculate(&crate::SETTINGS.captcha.default_difficulty_strategy)
&data.settings.captcha.default_difficulty_strategy,
)
.unwrap(); .unwrap();
// START create_easy // START create_easy
@@ -316,17 +355,15 @@ pub mod tests {
// END create_easy // END create_easy
// START update_easy // START update_easy
let update_pattern = TrafficPatternRequest { let update_pattern = TrafficPattern {
avg_traffic: 1_000, avg_traffic: 1_000,
peak_sustainable_traffic: 10_000, peak_sustainable_traffic: 10_000,
broke_my_site_traffic: Some(1_000_000), broke_my_site_traffic: Some(1_000_000),
description: NAME.into(), description: NAME.into(),
}; };
let updated_default_values = calculate( let updated_default_values = update_pattern
&(&update_pattern).into(), .calculate(&crate::SETTINGS.captcha.default_difficulty_strategy)
&data.settings.captcha.default_difficulty_strategy,
)
.unwrap(); .unwrap();
let payload = UpdateTrafficPattern { let payload = UpdateTrafficPattern {

View File

@@ -33,10 +33,7 @@ pub async fn get_captcha(
id: Identity, id: Identity,
) -> ServiceResult<impl Responder> { ) -> ServiceResult<impl Responder> {
let username = id.identity().unwrap(); let username = id.identity().unwrap();
let levels = data let levels = runner::get_captcha(&payload.key, &username, &data).await?;
.db
.get_captcha_levels(Some(&username), &payload.key)
.await?;
Ok(HttpResponse::Ok().json(levels)) Ok(HttpResponse::Ok().json(levels))
} }
@@ -50,3 +47,30 @@ pub struct I32Levels {
pub difficulty_factor: i32, pub difficulty_factor: i32,
pub visitor_threshold: i32, pub visitor_threshold: i32,
} }
pub mod runner {
use super::*;
// TODO get metadata from mcaptcha_config table
pub async fn get_captcha(
key: &str,
username: &str,
data: &AppData,
) -> ServiceResult<Vec<I32Levels>> {
let levels = sqlx::query_as!(
I32Levels,
"SELECT difficulty_factor, visitor_threshold FROM mcaptcha_levels WHERE
config_id = (
SELECT config_id FROM mcaptcha_config WHERE key = ($1)
AND user_id = (SELECT ID from mcaptcha_users WHERE name = $2)
)
ORDER BY difficulty_factor ASC;",
key,
&username
)
.fetch_all(&data.db)
.await?;
Ok(levels)
}
}

View File

@@ -19,6 +19,7 @@ use actix_web::{web, HttpResponse, Responder};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::errors::*; use crate::errors::*;
use crate::stats::fetch::{Stats, StatsUnixTimestamp};
use crate::AppData; use crate::AppData;
pub mod routes { pub mod routes {
@@ -49,6 +50,7 @@ pub async fn get(
id: Identity, id: Identity,
) -> ServiceResult<impl Responder> { ) -> ServiceResult<impl Responder> {
let username = id.identity().unwrap(); let username = id.identity().unwrap();
let stats = data.stats.fetch(&data, &username, &payload.key).await?; let stats = Stats::new(&username, &payload.key, &data.db).await?;
let stats = StatsUnixTimestamp::from_stats(&stats);
Ok(HttpResponse::Ok().json(&stats)) Ok(HttpResponse::Ok().json(&stats))
} }

View File

@@ -23,6 +23,7 @@ use libmcaptcha::defense::Level;
use crate::api::v1::mcaptcha::update::UpdateCaptcha; use crate::api::v1::mcaptcha::update::UpdateCaptcha;
use crate::api::v1::ROUTES; use crate::api::v1::ROUTES;
use crate::data::Data;
use crate::errors::*; use crate::errors::*;
use crate::tests::*; use crate::tests::*;
use crate::*; use crate::*;
@@ -37,18 +38,19 @@ const L2: Level = Level {
}; };
#[actix_rt::test] #[actix_rt::test]
pub async fn level_routes_work() { async fn level_routes_work() {
const NAME: &str = "testuserlevelroutes"; const NAME: &str = "testuserlevelroutes";
const PASSWORD: &str = "longpassworddomain"; const PASSWORD: &str = "longpassworddomain";
const EMAIL: &str = "testuserlevelrouts@a.com"; const EMAIL: &str = "testuserlevelrouts@a.com";
let data = get_data().await;
let data = &data;
delete_user(data, NAME).await; {
let data = Data::new().await;
delete_user(NAME, &data).await;
}
register_and_signin(data, NAME, EMAIL, PASSWORD).await; register_and_signin(NAME, EMAIL, PASSWORD).await;
// create captcha // create captcha
let (_, signin_resp, key) = add_levels_util(data, NAME, PASSWORD).await; let (data, _, signin_resp, key) = add_levels_util(NAME, PASSWORD).await;
let cookies = get_cookie!(signin_resp); let cookies = get_cookie!(signin_resp);
let app = get_app!(data).await; let app = get_app!(data).await;
@@ -101,7 +103,6 @@ pub async fn level_routes_work() {
}; };
bad_post_req_test( bad_post_req_test(
data,
NAME, NAME,
PASSWORD, PASSWORD,
ROUTES.captcha.delete, ROUTES.captcha.delete,

View File

@@ -14,15 +14,14 @@
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::borrow::Cow;
use actix_identity::Identity; use actix_identity::Identity;
use actix_web::{web, HttpResponse, Responder}; use actix_web::{web, HttpResponse, Responder};
use libmcaptcha::defense::Level; use libmcaptcha::defense::Level;
use libmcaptcha::master::messages::RenameBuilder; use libmcaptcha::master::messages::RenameBuilder;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use db_core::errors::DBError;
use db_core::CreateCaptcha;
use super::create::MCaptchaDetails; use super::create::MCaptchaDetails;
use super::get_random; use super::get_random;
use crate::errors::*; use crate::errors::*;
@@ -42,16 +41,16 @@ pub async fn update_key(
loop { loop {
key = get_random(32); key = get_random(32);
let res = runner::update_key(&key, &payload.key, &username, &data).await;
match data if res.is_ok() {
.db break;
.update_captcha_key(&username, &payload.key, &key) } else if let Err(sqlx::Error::Database(err)) = res {
.await if err.code() == Some(Cow::from("23505")) {
{ continue;
Ok(_) => break, } else {
Err(DBError::SecretTaken) => continue, return Err(sqlx::Error::Database(err).into());
Err(e) => return Err(e.into()),
} }
};
} }
let payload = payload.into_inner(); let payload = payload.into_inner();
@@ -93,10 +92,29 @@ pub async fn update_captcha(
} }
pub mod runner { pub mod runner {
use futures::future::try_join_all;
use libmcaptcha::{master::messages::RemoveCaptcha, DefenseBuilder}; use libmcaptcha::{master::messages::RemoveCaptcha, DefenseBuilder};
use super::*; use super::*;
pub async fn update_key(
key: &str,
old_key: &str,
username: &str,
data: &AppData,
) -> Result<(), sqlx::Error> {
sqlx::query!(
"UPDATE mcaptcha_config SET key = $1
WHERE key = $2 AND user_id = (SELECT ID FROM mcaptcha_users WHERE name = $3)",
&key,
&old_key,
&username,
)
.execute(&data.db)
.await?;
Ok(())
}
pub async fn update_captcha( pub async fn update_captcha(
payload: &UpdateCaptcha, payload: &UpdateCaptcha,
data: &AppData, data: &AppData,
@@ -113,21 +131,58 @@ pub mod runner {
// still, needs to be benchmarked // still, needs to be benchmarked
defense.build()?; defense.build()?;
data.db let mut futs = Vec::with_capacity(payload.levels.len() + 2);
.delete_captcha_levels(username, &payload.key) sqlx::query!(
"DELETE FROM mcaptcha_levels
WHERE config_id = (
SELECT config_id FROM mcaptcha_config where key = ($1)
AND user_id = (
SELECT ID from mcaptcha_users WHERE name = $2
)
)",
&payload.key,
&username
)
.execute(&data.db)
.await?; .await?;
let m = CreateCaptcha { let update_fut = sqlx::query!(
key: &payload.key, "UPDATE mcaptcha_config SET name = $1, duration = $2
duration: payload.duration as i32, WHERE user_id = (SELECT ID FROM mcaptcha_users WHERE name = $3)
description: &payload.description, AND key = $4",
}; &payload.description,
payload.duration as i32,
&username,
&payload.key,
)
.execute(&data.db); //.await?;
data.db.update_captcha_metadata(username, &m).await?; futs.push(update_fut);
data.db for level in payload.levels.iter() {
.add_captcha_levels(username, &payload.key, &payload.levels) let difficulty_factor = level.difficulty_factor as i32;
.await?; let visitor_threshold = level.visitor_threshold as i32;
let fut = sqlx::query!(
"INSERT INTO mcaptcha_levels (
difficulty_factor,
visitor_threshold,
config_id) VALUES (
$1, $2, (
SELECT config_id FROM mcaptcha_config WHERE key = ($3) AND
user_id = (
SELECT ID from mcaptcha_users WHERE name = $4
)
));",
difficulty_factor,
visitor_threshold,
&payload.key,
&username,
)
.execute(&data.db); //.await?;
futs.push(fut);
}
try_join_all(futs).await?;
if let Err(ServiceError::CaptchaError(e)) = data if let Err(ServiceError::CaptchaError(e)) = data
.captcha .captcha
.remove(RemoveCaptcha(payload.key.clone())) .remove(RemoveCaptcha(payload.key.clone()))
@@ -159,13 +214,15 @@ mod tests {
const NAME: &str = "updateusermcaptcha"; const NAME: &str = "updateusermcaptcha";
const PASSWORD: &str = "longpassworddomain"; const PASSWORD: &str = "longpassworddomain";
const EMAIL: &str = "testupdateusermcaptcha@a.com"; const EMAIL: &str = "testupdateusermcaptcha@a.com";
let data = get_data().await;
let data = &data; {
delete_user(data, NAME).await; let data = Data::new().await;
delete_user(NAME, &data).await;
}
// 1. add mcaptcha token // 1. add mcaptcha token
register_and_signin(data, NAME, EMAIL, PASSWORD).await; register_and_signin(NAME, EMAIL, PASSWORD).await;
let (_, signin_resp, token_key) = add_levels_util(data, NAME, PASSWORD).await; let (data, _, signin_resp, token_key) = add_levels_util(NAME, PASSWORD).await;
let cookies = get_cookie!(signin_resp); let cookies = get_cookie!(signin_resp);
let app = get_app!(data).await; let app = get_app!(data).await;

View File

@@ -73,13 +73,21 @@ impl Health {
/// checks all components of the system /// checks all components of the system
#[my_codegen::get(path = "crate::V1_API_ROUTES.meta.health")] #[my_codegen::get(path = "crate::V1_API_ROUTES.meta.health")]
async fn health(data: AppData) -> impl Responder { async fn health(data: AppData) -> impl Responder {
let mut resp_builder = HealthBuilder::default(); use sqlx::Connection;
resp_builder.db(data.db.ping().await); let mut resp_builder = HealthBuilder::default();
resp_builder.db(false);
resp_builder.redis = None;
if let Ok(mut con) = data.db.acquire().await {
if con.ping().await.is_ok() {
resp_builder.db(true);
}
};
if let SystemGroup::Redis(_) = data.captcha { if let SystemGroup::Redis(_) = data.captcha {
if let Ok(r) = Redis::new(RedisConfig::Single( if let Ok(r) = Redis::new(RedisConfig::Single(
data.settings.redis.as_ref().unwrap().url.clone(), crate::SETTINGS.redis.as_ref().unwrap().url.clone(),
)) ))
.await .await
{ {
@@ -99,7 +107,7 @@ pub fn services(cfg: &mut web::ServiceConfig) {
} }
#[cfg(test)] #[cfg(test)]
pub mod tests { mod tests {
use actix_web::{http::StatusCode, test, App}; use actix_web::{http::StatusCode, test, App};
use super::*; use super::*;
@@ -121,10 +129,9 @@ pub mod tests {
} }
#[actix_rt::test] #[actix_rt::test]
pub async fn health_works() { async fn health_works() {
println!("{}", V1_API_ROUTES.meta.health); println!("{}", V1_API_ROUTES.meta.health);
let data = crate::tests::get_data().await; let data = Data::new().await;
let data = &data;
let app = get_app!(data).await; let app = get_app!(data).await;
let resp = test::call_service( let resp = test::call_service(

View File

@@ -22,10 +22,8 @@ use serde::{Deserialize, Serialize};
use crate::errors::*; use crate::errors::*;
use crate::AppData; use crate::AppData;
use db_core::AddNotification; #[derive(Serialize, Deserialize)]
pub struct AddNotification {
#[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)]
pub struct AddNotificationRequest {
pub to: String, pub to: String,
pub heading: String, pub heading: String,
pub message: String, pub message: String,
@@ -37,27 +35,32 @@ pub struct AddNotificationRequest {
wrap = "crate::api::v1::get_middleware()" wrap = "crate::api::v1::get_middleware()"
)] )]
pub async fn add_notification( pub async fn add_notification(
payload: web::Json<AddNotificationRequest>, payload: web::Json<AddNotification>,
data: AppData, data: AppData,
id: Identity, id: Identity,
) -> ServiceResult<impl Responder> { ) -> ServiceResult<impl Responder> {
let sender = id.identity().unwrap(); let sender = id.identity().unwrap();
// TODO handle error where payload.to doesnt exist // TODO handle error where payload.to doesnt exist
sqlx::query!(
let p = AddNotification { "INSERT INTO mcaptcha_notifications (
from: &sender, heading, message, tx, rx)
to: &payload.to, VALUES (
message: &payload.message, $1, $2,
heading: &payload.heading, (SELECT ID FROM mcaptcha_users WHERE name = $3),
}; (SELECT ID FROM mcaptcha_users WHERE name = $4)
);",
data.db.create_notification(&p).await?; &payload.heading,
&payload.message,
&sender,
&payload.to,
)
.execute(&data.db)
.await?;
Ok(HttpResponse::Ok()) Ok(HttpResponse::Ok())
} }
#[cfg(test)] #[cfg(test)]
pub mod tests { mod tests {
use actix_web::http::StatusCode; use actix_web::http::StatusCode;
use actix_web::test; use actix_web::test;
@@ -66,26 +69,26 @@ pub mod tests {
use crate::*; use crate::*;
#[actix_rt::test] #[actix_rt::test]
pub async fn notification_works() { async fn notification_works() {
const NAME1: &str = "notifuser1"; const NAME1: &str = "notifuser1";
const NAME2: &str = "notiuser2"; const NAME2: &str = "notiuser2";
const PASSWORD: &str = "longpassworddomain"; const PASSWORD: &str = "longpassworddomain";
const EMAIL1: &str = "testnotification1@a.com"; const EMAIL1: &str = "testnotification1@a.com";
const EMAIL2: &str = "testnotification2@a.com"; const EMAIL2: &str = "testnotification2@a.com";
let data = get_data().await; {
let data = &data; let data = Data::new().await;
delete_user(NAME1, &data).await;
delete_user(NAME2, &data).await;
}
delete_user(data, NAME1).await; register_and_signin(NAME1, EMAIL1, PASSWORD).await;
delete_user(data, NAME2).await; register_and_signin(NAME2, EMAIL2, PASSWORD).await;
let (data, _creds, signin_resp) = signin(NAME1, PASSWORD).await;
register_and_signin(data, NAME1, EMAIL1, PASSWORD).await;
register_and_signin(data, NAME2, EMAIL2, PASSWORD).await;
let (_creds, signin_resp) = signin(data, NAME1, PASSWORD).await;
let cookies = get_cookie!(signin_resp); let cookies = get_cookie!(signin_resp);
let app = get_app!(data).await; let app = get_app!(data).await;
let msg = AddNotificationRequest { let msg = AddNotification {
to: NAME2.into(), to: NAME2.into(),
heading: "Test notification".into(), heading: "Test notification".into(),
message: "Testeing notifications with a dummy message".into(), message: "Testeing notifications with a dummy message".into(),

View File

@@ -18,13 +18,20 @@
use actix_identity::Identity; use actix_identity::Identity;
use actix_web::{HttpResponse, Responder}; use actix_web::{HttpResponse, Responder};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use sqlx::types::time::OffsetDateTime;
use crate::errors::*; use crate::errors::*;
use crate::AppData; use crate::AppData;
use db_core::Notification; pub struct Notification {
pub name: Option<String>,
pub heading: Option<String>,
pub message: Option<String>,
pub received: Option<OffsetDateTime>,
pub id: Option<i32>,
}
#[derive(Default, PartialEq, Clone, Deserialize, Serialize)] #[derive(Deserialize, Serialize)]
pub struct NotificationResp { pub struct NotificationResp {
pub name: String, pub name: String,
pub heading: String, pub heading: String,
@@ -38,26 +45,13 @@ impl From<Notification> for NotificationResp {
NotificationResp { NotificationResp {
name: n.name.unwrap(), name: n.name.unwrap(),
heading: n.heading.unwrap(), heading: n.heading.unwrap(),
received: n.received.unwrap(), received: n.received.unwrap().unix_timestamp(),
id: n.id.unwrap(), id: n.id.unwrap(),
message: n.message.unwrap(), message: n.message.unwrap(),
} }
} }
} }
impl NotificationResp {
pub fn from_notifications(mut n: Vec<Notification>) -> Vec<Self> {
let mut notifications = Vec::with_capacity(n.len());
n.drain(0..).for_each(|x| {
let y: NotificationResp = x.into();
notifications.push(y)
});
notifications
}
}
/// route handler that gets all unread notifications /// route handler that gets all unread notifications
#[my_codegen::get( #[my_codegen::get(
path = "crate::V1_API_ROUTES.notifications.get", path = "crate::V1_API_ROUTES.notifications.get",
@@ -70,23 +64,50 @@ pub async fn get_notification(
let receiver = id.identity().unwrap(); let receiver = id.identity().unwrap();
// TODO handle error where payload.to doesnt exist // TODO handle error where payload.to doesnt exist
let notifications = data.db.get_all_unread_notifications(&receiver).await?; let mut notifications = runner::get_notification(&data, &receiver).await?;
let notifications = NotificationResp::from_notifications(notifications); let resp: Vec<NotificationResp> = notifications
Ok(HttpResponse::Ok().json(notifications)) .drain(0..)
.map(|x| {
let y: NotificationResp = x.into();
y
})
.collect();
Ok(HttpResponse::Ok().json(resp))
}
pub mod runner {
use super::*;
pub async fn get_notification(
data: &AppData,
receiver: &str,
) -> ServiceResult<Vec<Notification>> {
// TODO handle error where payload.to doesnt exist
let notifications = sqlx::query_file_as!(
Notification,
"src/api/v1/notifications/get_all_unread.sql",
&receiver
)
.fetch_all(&data.db)
.await?;
Ok(notifications)
}
} }
#[cfg(test)] #[cfg(test)]
pub mod tests { mod tests {
use actix_web::http::StatusCode; use actix_web::http::StatusCode;
use actix_web::test; use actix_web::test;
use super::*; use super::*;
use crate::api::v1::notifications::add::AddNotificationRequest; use crate::api::v1::notifications::add::AddNotification;
use crate::tests::*; use crate::tests::*;
use crate::*; use crate::*;
#[actix_rt::test] #[actix_rt::test]
pub async fn notification_get_works() { async fn notification_get_works() {
const NAME1: &str = "notifuser12"; const NAME1: &str = "notifuser12";
const NAME2: &str = "notiuser22"; const NAME2: &str = "notiuser22";
const PASSWORD: &str = "longpassworddomain"; const PASSWORD: &str = "longpassworddomain";
@@ -95,21 +116,21 @@ pub mod tests {
const HEADING: &str = "testing notifications get"; const HEADING: &str = "testing notifications get";
const MESSAGE: &str = "testing notifications get message"; const MESSAGE: &str = "testing notifications get message";
let data = get_data().await; {
let data = &data; let data = Data::new().await;
delete_user(NAME1, &data).await;
delete_user(NAME2, &data).await;
}
delete_user(data, NAME1).await; register_and_signin(NAME1, EMAIL1, PASSWORD).await;
delete_user(data, NAME2).await; register_and_signin(NAME2, EMAIL2, PASSWORD).await;
let (data, _creds, signin_resp) = signin(NAME1, PASSWORD).await;
register_and_signin(data, NAME1, EMAIL1, PASSWORD).await; let (_data, _creds2, signin_resp2) = signin(NAME2, PASSWORD).await;
register_and_signin(data, NAME2, EMAIL2, PASSWORD).await;
let (_creds, signin_resp) = signin(data, NAME1, PASSWORD).await;
let (_creds2, signin_resp2) = signin(data, NAME2, PASSWORD).await;
let cookies = get_cookie!(signin_resp); let cookies = get_cookie!(signin_resp);
let cookies2 = get_cookie!(signin_resp2); let cookies2 = get_cookie!(signin_resp2);
let app = get_app!(data).await; let app = get_app!(data).await;
let msg = AddNotificationRequest { let msg = AddNotification {
to: NAME2.into(), to: NAME2.into(),
heading: HEADING.into(), heading: HEADING.into(),
message: MESSAGE.into(), message: MESSAGE.into(),

View File

@@ -27,6 +27,15 @@ pub struct MarkReadReq {
pub id: i32, pub id: i32,
} }
#[derive(Deserialize, Serialize)]
pub struct NotificationResp {
pub name: String,
pub heading: String,
pub message: String,
pub received: i64,
pub id: i32,
}
/// route handler that marks a notification read /// route handler that marks a notification read
#[my_codegen::post( #[my_codegen::post(
path = "crate::V1_API_ROUTES.notifications.mark_read", path = "crate::V1_API_ROUTES.notifications.mark_read",
@@ -40,27 +49,30 @@ pub async fn mark_read(
let receiver = id.identity().unwrap(); let receiver = id.identity().unwrap();
// TODO handle error where payload.to doesnt exist // TODO handle error where payload.to doesnt exist
// TODO get payload from path /api/v1/notifications/{id}/read" sqlx::query_file_as!(
data.db Notification,
.mark_notification_read(&receiver, payload.id) "src/api/v1/notifications/mark_read.sql",
payload.id,
&receiver
)
.execute(&data.db)
.await?; .await?;
Ok(HttpResponse::Ok()) Ok(HttpResponse::Ok())
} }
#[cfg(test)] #[cfg(test)]
pub mod tests { mod tests {
use actix_web::http::StatusCode; use actix_web::http::StatusCode;
use actix_web::test; use actix_web::test;
use super::*; use super::*;
use crate::api::v1::notifications::add::AddNotificationRequest; use crate::api::v1::notifications::add::AddNotification;
use crate::api::v1::notifications::get::NotificationResp;
use crate::tests::*; use crate::tests::*;
use crate::*; use crate::*;
#[actix_rt::test] #[actix_rt::test]
pub async fn notification_mark_read_works() { async fn notification_mark_read_works() {
const NAME1: &str = "notifuser122"; const NAME1: &str = "notifuser122";
const NAME2: &str = "notiuser222"; const NAME2: &str = "notiuser222";
const PASSWORD: &str = "longpassworddomain"; const PASSWORD: &str = "longpassworddomain";
@@ -68,21 +80,22 @@ pub mod tests {
const EMAIL2: &str = "testnotification222@a.com"; const EMAIL2: &str = "testnotification222@a.com";
const HEADING: &str = "testing notifications get"; const HEADING: &str = "testing notifications get";
const MESSAGE: &str = "testing notifications get message"; const MESSAGE: &str = "testing notifications get message";
let data = get_data().await;
let data = &data;
delete_user(data, NAME1).await; {
delete_user(data, NAME2).await; let data = Data::new().await;
delete_user(NAME1, &data).await;
delete_user(NAME2, &data).await;
}
register_and_signin(data, NAME1, EMAIL1, PASSWORD).await; register_and_signin(NAME1, EMAIL1, PASSWORD).await;
register_and_signin(data, NAME2, EMAIL2, PASSWORD).await; register_and_signin(NAME2, EMAIL2, PASSWORD).await;
let (_creds, signin_resp) = signin(data, NAME1, PASSWORD).await; let (data, _creds, signin_resp) = signin(NAME1, PASSWORD).await;
let (_creds2, signin_resp2) = signin(data, NAME2, PASSWORD).await; let (_data, _creds2, signin_resp2) = signin(NAME2, PASSWORD).await;
let cookies = get_cookie!(signin_resp); let cookies = get_cookie!(signin_resp);
let cookies2 = get_cookie!(signin_resp2); let cookies2 = get_cookie!(signin_resp2);
let app = get_app!(data).await; let app = get_app!(data).await;
let msg = AddNotificationRequest { let msg = AddNotification {
to: NAME2.into(), to: NAME2.into(),
heading: HEADING.into(), heading: HEADING.into(),
message: MESSAGE.into(), message: MESSAGE.into(),

View File

@@ -23,8 +23,9 @@ use libmcaptcha::{
}; };
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::I32Levels;
use crate::errors::*; use crate::errors::*;
//use crate::stats::record::record_fetch; use crate::stats::record::record_fetch;
use crate::AppData; use crate::AppData;
use crate::V1_API_ROUTES; use crate::V1_API_ROUTES;
@@ -41,15 +42,22 @@ pub async fn get_config(
payload: web::Json<GetConfigPayload>, payload: web::Json<GetConfigPayload>,
data: AppData, data: AppData,
) -> ServiceResult<impl Responder> { ) -> ServiceResult<impl Responder> {
//if res.exists.is_none() { let res = sqlx::query!(
if !data.db.captcha_exists(None, &payload.key).await? { "SELECT EXISTS (SELECT 1 from mcaptcha_config WHERE key = $1)",
&payload.key,
)
.fetch_one(&data.db)
.await?;
if res.exists.is_none() {
return Err(ServiceError::TokenNotFound); return Err(ServiceError::TokenNotFound);
} }
let payload = payload.into_inner(); let payload = payload.into_inner();
match res.exists {
Some(true) => {
match data.captcha.get_pow(payload.key.clone()).await { match data.captcha.get_pow(payload.key.clone()).await {
Ok(Some(config)) => { Ok(Some(config)) => {
data.stats.record_fetch(&data, &payload.key).await?; record_fetch(&payload.key, &data.db).await;
Ok(HttpResponse::Ok().json(config)) Ok(HttpResponse::Ok().json(config))
} }
Ok(None) => { Ok(None) => {
@@ -61,38 +69,16 @@ pub async fn get_config(
.expect("mcaptcha should be initialized and ready to go"); .expect("mcaptcha should be initialized and ready to go");
// background it. would require data::Data to be static // background it. would require data::Data to be static
// to satidfy lifetime // to satidfy lifetime
data.stats.record_fetch(&data, &payload.key).await?; record_fetch(&payload.key, &data.db).await;
Ok(HttpResponse::Ok().json(config)) Ok(HttpResponse::Ok().json(config))
} }
Err(e) => Err(e.into()), Err(e) => Err(e.into()),
} }
}
// match res.exists { Some(false) => Err(ServiceError::TokenNotFound),
// Some(true) => { None => Err(ServiceError::TokenNotFound),
// match data.captcha.get_pow(payload.key.clone()).await { }
// Ok(Some(config)) => {
// record_fetch(&payload.key, &data.db).await;
// Ok(HttpResponse::Ok().json(config))
// }
// Ok(None) => {
// init_mcaptcha(&data, &payload.key).await?;
// let config = data
// .captcha
// .get_pow(payload.key.clone())
// .await
// .expect("mcaptcha should be initialized and ready to go");
// // background it. would require data::Data to be static
// // to satidfy lifetime
// record_fetch(&payload.key, &data.db).await;
// Ok(HttpResponse::Ok().json(config))
// }
// Err(e) => Err(e.into()),
// }
// }
//
// Some(false) => Err(ServiceError::TokenNotFound),
// None => Err(ServiceError::TokenNotFound),
// }
} }
/// Call this when [MCaptcha][libmcaptcha::MCaptcha] is not in master. /// Call this when [MCaptcha][libmcaptcha::MCaptcha] is not in master.
/// ///
@@ -100,8 +86,29 @@ pub async fn get_config(
/// creates [MCaptcha][libmcaptcha::MCaptcha] and adds it to [Master][libmcaptcha::Defense] /// creates [MCaptcha][libmcaptcha::MCaptcha] and adds it to [Master][libmcaptcha::Defense]
async fn init_mcaptcha(data: &AppData, key: &str) -> ServiceResult<()> { async fn init_mcaptcha(data: &AppData, key: &str) -> ServiceResult<()> {
// get levels // get levels
let levels = data.db.get_captcha_levels(None, key).await?; let levels_fut = sqlx::query_as!(
let duration = data.db.get_captcha_cooldown(key).await?; I32Levels,
"SELECT difficulty_factor, visitor_threshold FROM mcaptcha_levels WHERE
config_id = (
SELECT config_id FROM mcaptcha_config WHERE key = ($1)
) ORDER BY difficulty_factor ASC;",
&key,
)
.fetch_all(&data.db);
struct DurationResp {
duration: i32,
}
// get duration
let duration_fut = sqlx::query_as!(
DurationResp,
"SELECT duration FROM mcaptcha_config
WHERE key = $1",
&key,
)
.fetch_one(&data.db);
//let (levels, duration) = futures::try_join!(levels_fut, duration_fut).await?;
let (levels, duration) = futures::try_join!(levels_fut, duration_fut)?;
// build defense // build defense
let mut defense = DefenseBuilder::default(); let mut defense = DefenseBuilder::default();
@@ -122,7 +129,7 @@ async fn init_mcaptcha(data: &AppData, key: &str) -> ServiceResult<()> {
let mcaptcha = MCaptchaBuilder::default() let mcaptcha = MCaptchaBuilder::default()
.defense(defense) .defense(defense)
// leaky bucket algorithm's emission interval // leaky bucket algorithm's emission interval
.duration(duration as u64) .duration(duration.duration as u64)
// .cache(cache) // .cache(cache)
.build() .build()
.unwrap(); .unwrap();
@@ -140,12 +147,11 @@ async fn init_mcaptcha(data: &AppData, key: &str) -> ServiceResult<()> {
} }
#[cfg(test)] #[cfg(test)]
pub mod tests { mod tests {
use crate::*;
use libmcaptcha::pow::PoWConfig; use libmcaptcha::pow::PoWConfig;
#[actix_rt::test] #[actix_rt::test]
pub async fn get_pow_config_works() { async fn get_pow_config_works() {
use super::*; use super::*;
use crate::tests::*; use crate::tests::*;
use crate::*; use crate::*;
@@ -155,13 +161,13 @@ pub mod tests {
const PASSWORD: &str = "testingpas"; const PASSWORD: &str = "testingpas";
const EMAIL: &str = "randomuser@a.com"; const EMAIL: &str = "randomuser@a.com";
let data = get_data().await; {
let data = &data; let data = Data::new().await;
delete_user(NAME, &data).await;
}
delete_user(data, NAME).await; register_and_signin(NAME, EMAIL, PASSWORD).await;
let (data, _, _signin_resp, token_key) = add_levels_util(NAME, PASSWORD).await;
register_and_signin(data, NAME, EMAIL, PASSWORD).await;
let (_, _signin_resp, token_key) = add_levels_util(data, NAME, PASSWORD).await;
let app = get_app!(data).await; let app = get_app!(data).await;
let get_config_payload = GetConfigPayload { let get_config_payload = GetConfigPayload {

View File

@@ -21,6 +21,7 @@ use libmcaptcha::pow::Work;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::errors::*; use crate::errors::*;
use crate::stats::record::record_solve;
use crate::AppData; use crate::AppData;
use crate::V1_API_ROUTES; use crate::V1_API_ROUTES;
@@ -42,13 +43,13 @@ pub async fn verify_pow(
) -> ServiceResult<impl Responder> { ) -> ServiceResult<impl Responder> {
let key = payload.key.clone(); let key = payload.key.clone();
let res = data.captcha.verify_pow(payload.into_inner()).await?; let res = data.captcha.verify_pow(payload.into_inner()).await?;
data.stats.record_solve(&data, &key).await?; record_solve(&key, &data.db).await;
let payload = ValidationToken { token: res }; let payload = ValidationToken { token: res };
Ok(HttpResponse::Ok().json(payload)) Ok(HttpResponse::Ok().json(payload))
} }
#[cfg(test)] #[cfg(test)]
pub mod tests { mod tests {
use actix_web::http::StatusCode; use actix_web::http::StatusCode;
use actix_web::test; use actix_web::test;
use libmcaptcha::pow::PoWConfig; use libmcaptcha::pow::PoWConfig;
@@ -59,17 +60,18 @@ pub mod tests {
use crate::*; use crate::*;
#[actix_rt::test] #[actix_rt::test]
pub async fn verify_pow_works() { async fn verify_pow_works() {
const NAME: &str = "powverifyusr"; const NAME: &str = "powverifyusr";
const PASSWORD: &str = "testingpas"; const PASSWORD: &str = "testingpas";
const EMAIL: &str = "verifyuser@a.com"; const EMAIL: &str = "verifyuser@a.com";
let data = get_data().await;
let data = &data;
delete_user(data, NAME).await; {
let data = Data::new().await;
delete_user(NAME, &data).await;
}
register_and_signin(data, NAME, EMAIL, PASSWORD).await; register_and_signin(NAME, EMAIL, PASSWORD).await;
let (_, _signin_resp, token_key) = add_levels_util(data, NAME, PASSWORD).await; let (data, _, _signin_resp, token_key) = add_levels_util(NAME, PASSWORD).await;
let app = get_app!(data).await; let app = get_app!(data).await;
let get_config_payload = GetConfigPayload { let get_config_payload = GetConfigPayload {

View File

@@ -21,6 +21,7 @@ use libmcaptcha::cache::messages::VerifyCaptchaResult;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use crate::errors::*; use crate::errors::*;
use crate::stats::record::record_confirm;
use crate::AppData; use crate::AppData;
use crate::V1_API_ROUTES; use crate::V1_API_ROUTES;
@@ -43,13 +44,13 @@ pub async fn validate_captcha_token(
.validate_verification_tokens(payload.into_inner()) .validate_verification_tokens(payload.into_inner())
.await?; .await?;
let payload = CaptchaValidateResp { valid: res }; let payload = CaptchaValidateResp { valid: res };
data.stats.record_confirm(&data, &key).await?; record_confirm(&key, &data.db).await;
//println!("{:?}", &payload); //println!("{:?}", &payload);
Ok(HttpResponse::Ok().json(payload)) Ok(HttpResponse::Ok().json(payload))
} }
#[cfg(test)] #[cfg(test)]
pub mod tests { mod tests {
use actix_web::http::StatusCode; use actix_web::http::StatusCode;
use actix_web::test; use actix_web::test;
use libmcaptcha::pow::PoWConfig; use libmcaptcha::pow::PoWConfig;
@@ -62,7 +63,7 @@ pub mod tests {
use crate::*; use crate::*;
#[actix_rt::test] #[actix_rt::test]
pub async fn validate_captcha_token_works() { async fn validate_captcha_token_works() {
const NAME: &str = "enterprisetken"; const NAME: &str = "enterprisetken";
const PASSWORD: &str = "testingpas"; const PASSWORD: &str = "testingpas";
const EMAIL: &str = "verifyuser@enter.com"; const EMAIL: &str = "verifyuser@enter.com";
@@ -71,12 +72,13 @@ pub mod tests {
const VERIFY_TOKEN_URL: &str = "/api/v1/pow/siteverify"; const VERIFY_TOKEN_URL: &str = "/api/v1/pow/siteverify";
// const UPDATE_URL: &str = "/api/v1/mcaptcha/domain/token/duration/update"; // const UPDATE_URL: &str = "/api/v1/mcaptcha/domain/token/duration/update";
let data = get_data().await; {
let data = &data; let data = Data::new().await;
delete_user(data, NAME).await; delete_user(NAME, &data).await;
}
register_and_signin(data, NAME, EMAIL, PASSWORD).await; register_and_signin(NAME, EMAIL, PASSWORD).await;
let (_, _signin_resp, token_key) = add_levels_util(data, NAME, PASSWORD).await; let (data, _, _signin_resp, token_key) = add_levels_util(NAME, PASSWORD).await;
let app = get_app!(data).await; let app = get_app!(data).await;
let get_config_payload = GetConfigPayload { let get_config_payload = GetConfigPayload {

View File

@@ -20,23 +20,22 @@ use actix_web::test;
use crate::api::v1::auth::runners::{Login, Register}; use crate::api::v1::auth::runners::{Login, Register};
use crate::api::v1::ROUTES; use crate::api::v1::ROUTES;
use crate::data::Data;
use crate::errors::*; use crate::errors::*;
use crate::*; use crate::*;
use crate::tests::*; use crate::tests::*;
#[actix_rt::test] #[actix_rt::test]
pub async fn auth_works() { async fn auth_works() {
let data = Data::new().await;
const NAME: &str = "testuser"; const NAME: &str = "testuser";
const PASSWORD: &str = "longpassword"; const PASSWORD: &str = "longpassword";
const EMAIL: &str = "testuser1@a.com"; const EMAIL: &str = "testuser1@a.com";
let data = get_data().await;
let data = &data;
let app = get_app!(data).await; let app = get_app!(data).await;
delete_user(data, NAME).await; delete_user(NAME, &data).await;
// 1. Register with email == None // 1. Register with email == None
let msg = Register { let msg = Register {
@@ -50,14 +49,14 @@ pub async fn auth_works() {
.await; .await;
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
// delete user // delete user
delete_user(data, NAME).await; delete_user(NAME, &data).await;
// 1. Register and signin // 1. Register and signin
let (_, signin_resp) = register_and_signin(data, NAME, EMAIL, PASSWORD).await; let (_, _, signin_resp) = register_and_signin(NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp); let cookies = get_cookie!(signin_resp);
// Sign in with email // Sign in with email
signin(data, EMAIL, PASSWORD).await; signin(EMAIL, PASSWORD).await;
// 2. check if duplicate username is allowed // 2. check if duplicate username is allowed
let mut msg = Register { let mut msg = Register {
@@ -67,7 +66,6 @@ pub async fn auth_works() {
email: Some(EMAIL.into()), email: Some(EMAIL.into()),
}; };
bad_post_req_test( bad_post_req_test(
data,
NAME, NAME,
PASSWORD, PASSWORD,
ROUTES.auth.register, ROUTES.auth.register,
@@ -79,7 +77,6 @@ pub async fn auth_works() {
let name = format!("{}dupemail", NAME); let name = format!("{}dupemail", NAME);
msg.username = name; msg.username = name;
bad_post_req_test( bad_post_req_test(
data,
NAME, NAME,
PASSWORD, PASSWORD,
ROUTES.auth.register, ROUTES.auth.register,
@@ -94,7 +91,6 @@ pub async fn auth_works() {
password: msg.password.clone(), password: msg.password.clone(),
}; };
bad_post_req_test( bad_post_req_test(
data,
NAME, NAME,
PASSWORD, PASSWORD,
ROUTES.auth.login, ROUTES.auth.login,
@@ -105,7 +101,6 @@ pub async fn auth_works() {
creds.login = "nonexistantuser@example.com".into(); creds.login = "nonexistantuser@example.com".into();
bad_post_req_test( bad_post_req_test(
data,
NAME, NAME,
PASSWORD, PASSWORD,
ROUTES.auth.login, ROUTES.auth.login,
@@ -119,7 +114,6 @@ pub async fn auth_works() {
creds.password = NAME.into(); creds.password = NAME.into();
bad_post_req_test( bad_post_req_test(
data,
NAME, NAME,
PASSWORD, PASSWORD,
ROUTES.auth.login, ROUTES.auth.login,
@@ -143,13 +137,12 @@ pub async fn auth_works() {
} }
#[actix_rt::test] #[actix_rt::test]
pub async fn serverside_password_validation_works() { async fn serverside_password_validation_works() {
const NAME: &str = "testuser542"; const NAME: &str = "testuser542";
const PASSWORD: &str = "longpassword2"; const PASSWORD: &str = "longpassword2";
let data = get_data().await; let data = Data::new().await;
let data = &data; delete_user(NAME, &data).await;
delete_user(data, NAME).await;
let app = get_app!(data).await; let app = get_app!(data).await;

View File

@@ -18,6 +18,7 @@
use actix_web::http::StatusCode; use actix_web::http::StatusCode;
use actix_web::test; use actix_web::test;
use crate::data::Data;
use crate::*; use crate::*;
use crate::tests::*; use crate::tests::*;
@@ -27,8 +28,6 @@ async fn protected_routes_work() {
const NAME: &str = "testuser619"; const NAME: &str = "testuser619";
const PASSWORD: &str = "longpassword2"; const PASSWORD: &str = "longpassword2";
const EMAIL: &str = "testuser119@a.com2"; const EMAIL: &str = "testuser119@a.com2";
let data = get_data().await;
let data = &data;
let _post_protected_urls = [ let _post_protected_urls = [
"/api/v1/account/secret/", "/api/v1/account/secret/",
@@ -48,9 +47,12 @@ async fn protected_routes_work() {
let get_protected_urls = ["/logout"]; let get_protected_urls = ["/logout"];
delete_user(data, NAME).await; {
let data = Data::new().await;
delete_user(NAME, &data).await;
}
let (_, signin_resp) = register_and_signin(data, NAME, EMAIL, PASSWORD).await; let (data, _, signin_resp) = register_and_signin(NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp); let cookies = get_cookie!(signin_resp);
let app = get_app!(data).await; let app = get_app!(data).await;

View File

@@ -19,8 +19,6 @@ use std::thread;
use actix::prelude::*; use actix::prelude::*;
use argon2_creds::{Config, ConfigBuilder, PasswordPolicy}; use argon2_creds::{Config, ConfigBuilder, PasswordPolicy};
use db_core::prelude::*;
use db_sqlx_postgres::{ConnectionOptions, Fresh};
use lettre::transport::smtp::authentication::Mechanism; use lettre::transport::smtp::authentication::Mechanism;
use lettre::{ use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, Tokio1Executor, transport::smtp::authentication::Credentials, AsyncSmtpTransport, Tokio1Executor,
@@ -41,12 +39,10 @@ use libmcaptcha::{
system::{System, SystemBuilder}, system::{System, SystemBuilder},
}; };
use sqlx::postgres::PgPoolOptions; use sqlx::postgres::PgPoolOptions;
use sqlx::PgPool;
use db_core::MCDatabase;
use crate::errors::ServiceResult; use crate::errors::ServiceResult;
use crate::settings::Settings; use crate::SETTINGS;
use crate::stats::{Dummy, Real, Stats};
macro_rules! enum_system_actor { macro_rules! enum_system_actor {
($name:ident, $type:ident) => { ($name:ident, $type:ident) => {
@@ -105,13 +101,9 @@ impl SystemGroup {
// utility function to remove captcha // utility function to remove captcha
enum_system_actor!(remove, RemoveCaptcha); enum_system_actor!(remove, RemoveCaptcha);
fn new_system<A: Save, B: MasterTrait>( fn new_system<A: Save, B: MasterTrait>(m: Addr<B>, c: Addr<A>) -> System<A, B> {
s: &Settings,
m: Addr<B>,
c: Addr<A>,
) -> System<A, B> {
let pow = PoWConfigBuilder::default() let pow = PoWConfigBuilder::default()
.salt(s.captcha.salt.clone()) .salt(SETTINGS.captcha.salt.clone())
.build() .build()
.unwrap(); .unwrap();
@@ -120,8 +112,8 @@ impl SystemGroup {
// read settings, if Redis is configured then produce a Redis mCaptcha cache // read settings, if Redis is configured then produce a Redis mCaptcha cache
// based SystemGroup // based SystemGroup
async fn new(s: &Settings) -> Self { async fn new() -> Self {
match &s.redis { match &SETTINGS.redis {
Some(val) => { Some(val) => {
let master = RedisMaster::new(RedisConfig::Single(val.url.clone())) let master = RedisMaster::new(RedisConfig::Single(val.url.clone()))
.await .await
@@ -131,14 +123,14 @@ impl SystemGroup {
.await .await
.unwrap() .unwrap()
.start(); .start();
let captcha = Self::new_system(s, master, cache); let captcha = Self::new_system(master, cache);
SystemGroup::Redis(captcha) SystemGroup::Redis(captcha)
} }
None => { None => {
let master = EmbeddedMaster::new(s.captcha.gc).start(); let master = EmbeddedMaster::new(SETTINGS.captcha.gc).start();
let cache = HashCache::default().start(); let cache = HashCache::default().start();
let captcha = Self::new_system(s, master, cache); let captcha = Self::new_system(master, cache);
SystemGroup::Embedded(captcha) SystemGroup::Embedded(captcha)
} }
@@ -148,18 +140,14 @@ impl SystemGroup {
/// App data /// App data
pub struct Data { pub struct Data {
/// database ops defined by db crates /// databse pool
pub db: Box<dyn MCDatabase>, pub db: PgPool,
/// credential management configuration /// credential management configuration
pub creds: Config, pub creds: Config,
/// mCaptcha system: Redis cache, etc. /// mCaptcha system: Redis cache, etc.
pub captcha: SystemGroup, pub captcha: SystemGroup,
/// email client /// email client
pub mailer: Option<Mailer>, pub mailer: Option<Mailer>,
/// app settings
pub settings: Settings,
/// stats recorder
pub stats: Box<dyn Stats>,
} }
impl Data { impl Data {
@@ -174,7 +162,7 @@ impl Data {
} }
#[cfg(not(tarpaulin_include))] #[cfg(not(tarpaulin_include))]
/// create new instance of app data /// create new instance of app data
pub async fn new(s: &Settings) -> Arc<Self> { pub async fn new() -> Arc<Self> {
let creds = Self::get_creds(); let creds = Self::get_creds();
let c = creds.clone(); let c = creds.clone();
@@ -185,28 +173,17 @@ impl Data {
log::info!("Initialized credential manager"); log::info!("Initialized credential manager");
}); });
let pool = s.database.pool; let db = PgPoolOptions::new()
let pool_options = PgPoolOptions::new().max_connections(pool); .max_connections(SETTINGS.database.pool)
let connection_options = ConnectionOptions::Fresh(Fresh { .connect(&SETTINGS.database.url)
pool_options, .await
url: s.database.url.clone(), .expect("Unable to form database pool");
});
let db = connection_options.connect().await.unwrap();
db.migrate().await.unwrap();
let stats: Box<dyn Stats> = if s.captcha.enable_stats {
Box::new(Real::default())
} else {
Box::new(Dummy::default())
};
let data = Data { let data = Data {
creds, creds,
db: Box::new(db), db,
captcha: SystemGroup::new(s).await, captcha: SystemGroup::new().await,
mailer: Self::get_mailer(s), mailer: Self::get_mailer(),
settings: s.clone(),
stats,
}; };
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
@@ -215,8 +192,8 @@ impl Data {
Arc::new(data) Arc::new(data)
} }
fn get_mailer(s: &Settings) -> Option<Mailer> { fn get_mailer() -> Option<Mailer> {
if let Some(smtp) = s.smtp.as_ref() { if let Some(smtp) = SETTINGS.smtp.as_ref() {
let creds = let creds =
Credentials::new(smtp.username.to_string(), smtp.password.to_string()); // "smtp_username".to_string(), "smtp_password".to_string()); Credentials::new(smtp.username.to_string(), smtp.password.to_string()); // "smtp_username".to_string(), "smtp_password".to_string());

View File

@@ -65,12 +65,6 @@ impl Date {
pub fn date(&self) -> String { pub fn date(&self) -> String {
self.time.format("%F %r %z") self.time.format("%F %r %z")
} }
pub fn new(unix: i64) -> Self {
Self {
time: OffsetDateTime::from_unix_timestamp(unix),
}
}
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -115,10 +115,11 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn demo_account_works() { async fn demo_account_works() {
let data_inner = get_data().await; {
let data_inner = &data_inner; let data = Data::new().await;
let data = AppData::new(data_inner.clone()); crate::tests::delete_user(DEMO_USER, &data).await;
crate::tests::delete_user(data_inner, DEMO_USER).await; }
let data = AppData::new(Data::new().await);
let duration = Duration::from_secs(DURATION); let duration = Duration::from_secs(DURATION);
// register works // register works
@@ -127,7 +128,7 @@ mod tests {
val: DEMO_USER.into(), val: DEMO_USER.into(),
}; };
assert!(username_exists(&payload, &data).await.unwrap().exists); assert!(username_exists(&payload, &data).await.unwrap().exists);
signin(data_inner, DEMO_USER, DEMO_PASSWORD).await; signin(DEMO_USER, DEMO_PASSWORD).await;
// deletion works // deletion works
assert!(DemoUser::delete_demo_user(&data).await.is_ok()); assert!(DemoUser::delete_demo_user(&data).await.is_ok());
@@ -135,8 +136,8 @@ mod tests {
// test the runner // test the runner
let user = DemoUser::spawn(data, duration).await.unwrap(); let user = DemoUser::spawn(data, duration).await.unwrap();
let (_, signin_resp, token_key) = let (data_inner, _, signin_resp, token_key) =
add_levels_util(data_inner, DEMO_USER, DEMO_PASSWORD).await; add_levels_util(DEMO_USER, DEMO_PASSWORD).await;
let cookies = get_cookie!(signin_resp); let cookies = get_cookie!(signin_resp);
let app = get_app!(data_inner).await; let app = get_app!(data_inner).await;

View File

@@ -72,6 +72,7 @@ pub fn handle_embedded_file(path: &str) -> HttpResponse {
} }
} }
#[my_codegen::get(path = "DOCS.assets")] #[my_codegen::get(path = "DOCS.assets")]
async fn dist(path: web::Path<String>) -> impl Responder { async fn dist(path: web::Path<String>) -> impl Responder {
handle_embedded_file(&path) handle_embedded_file(&path)

View File

@@ -23,6 +23,7 @@ use sailfish::TemplateOnce;
use crate::errors::*; use crate::errors::*;
use crate::Data; use crate::Data;
use crate::SETTINGS;
const PAGE: &str = "Login"; const PAGE: &str = "Login";
@@ -43,7 +44,7 @@ async fn verification(
to: &str, to: &str,
verification_link: &str, verification_link: &str,
) -> ServiceResult<()> { ) -> ServiceResult<()> {
if let Some(smtp) = data.settings.smtp.as_ref() { if let Some(smtp) = SETTINGS.smtp.as_ref() {
let from = format!("mCaptcha Admin <{}>", smtp.from); let from = format!("mCaptcha Admin <{}>", smtp.from);
let reply_to = format!("mCaptcha Admin <{}>", smtp.reply); let reply_to = format!("mCaptcha Admin <{}>", smtp.reply);
const SUBJECT: &str = "[mCaptcha] Please verify your email"; const SUBJECT: &str = "[mCaptcha] Please verify your email";
@@ -63,7 +64,7 @@ Admin
instance: {} instance: {}
project website: {}", project website: {}",
verification_link, verification_link,
&data.settings.server.domain, SETTINGS.server.domain,
crate::PKG_HOMEPAGE crate::PKG_HOMEPAGE
); );
@@ -104,8 +105,7 @@ mod tests {
async fn email_verification_works() { async fn email_verification_works() {
const TO_ADDR: &str = "Hello <realaravinth@localhost>"; const TO_ADDR: &str = "Hello <realaravinth@localhost>";
const VERIFICATION_LINK: &str = "https://localhost"; const VERIFICATION_LINK: &str = "https://localhost";
let data = crate::tests::get_data().await; let data = Data::new().await;
let settings = &data.settings;
verification(&data, TO_ADDR, VERIFICATION_LINK) verification(&data, TO_ADDR, VERIFICATION_LINK)
.await .await
.unwrap(); .unwrap();
@@ -118,7 +118,7 @@ mod tests {
.unwrap(); .unwrap();
let data: serde_json::Value = resp.json().await.unwrap(); let data: serde_json::Value = resp.json().await.unwrap();
let data = &data[0]; let data = &data[0];
let smtp = settings.smtp.as_ref().unwrap(); let smtp = SETTINGS.smtp.as_ref().unwrap();
let from_addr = &data["headers"]["from"]; let from_addr = &data["headers"]["from"];

View File

@@ -24,7 +24,6 @@ use actix_web::{
HttpResponse, HttpResponseBuilder, HttpResponse, HttpResponseBuilder,
}; };
use argon2_creds::errors::CredsError; use argon2_creds::errors::CredsError;
use db_core::errors::DBError;
use derive_more::{Display, Error}; use derive_more::{Display, Error};
use lettre::transport::smtp::Error as SmtpError; use lettre::transport::smtp::Error as SmtpError;
use libmcaptcha::errors::CaptchaError; use libmcaptcha::errors::CaptchaError;
@@ -36,15 +35,6 @@ use validator::ValidationErrors;
#[derive(Debug, Display, Error)] #[derive(Debug, Display, Error)]
pub struct SmtpErrorWrapper(SmtpError); pub struct SmtpErrorWrapper(SmtpError);
#[derive(Debug, Display, Error)]
pub struct DBErrorWrapper(DBError);
impl std::cmp::PartialEq for DBErrorWrapper {
fn eq(&self, other: &Self) -> bool {
format!("{}", self.0) == format!("{}", other.0)
}
}
impl std::cmp::PartialEq for SmtpErrorWrapper { impl std::cmp::PartialEq for SmtpErrorWrapper {
fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
self.0.status() == other.0.status() self.0.status() == other.0.status()
@@ -106,23 +96,13 @@ pub enum ServiceError {
#[display(fmt = "Unable to send email, contact admin")] #[display(fmt = "Unable to send email, contact admin")]
UnableToSendEmail(SmtpErrorWrapper), UnableToSendEmail(SmtpErrorWrapper),
/// when the a token name is already taken
/// token not found /// token not found
#[display(fmt = "Token not found. Is token registered?")] #[display(fmt = "Token not found. Is token registered?")]
TokenNotFound, TokenNotFound,
#[display(fmt = "{}", _0)] #[display(fmt = "{}", _0)]
CaptchaError(CaptchaError), CaptchaError(CaptchaError),
#[display(fmt = "{}", _0)]
DBError(DBErrorWrapper),
/// captcha not found
#[display(fmt = "Captcha not found.")]
CaptchaNotFound,
/// Traffic pattern not found
#[display(fmt = "Traffic pattern not found")]
TrafficPatternNotFound,
} }
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
@@ -180,10 +160,6 @@ impl ResponseError for ServiceError {
log::error!("{}", e.0); log::error!("{}", e.0);
StatusCode::INTERNAL_SERVER_ERROR StatusCode::INTERNAL_SERVER_ERROR
} }
ServiceError::DBError(_) => StatusCode::INTERNAL_SERVER_ERROR,
ServiceError::CaptchaNotFound => StatusCode::NOT_FOUND,
ServiceError::TrafficPatternNotFound => StatusCode::NOT_FOUND,
} }
} }
} }
@@ -203,22 +179,6 @@ impl From<CredsError> for ServiceError {
} }
} }
impl From<DBError> for ServiceError {
#[cfg(not(tarpaulin_include))]
fn from(e: DBError) -> ServiceError {
println!("from conversin: {}", e);
match e {
DBError::UsernameTaken => ServiceError::UsernameTaken,
DBError::SecretTaken => ServiceError::InternalServerError,
DBError::EmailTaken => ServiceError::EmailTaken,
DBError::AccountNotFound => ServiceError::AccountNotFound,
DBError::CaptchaNotFound => ServiceError::CaptchaNotFound,
DBError::TrafficPatternNotFound => ServiceError::TrafficPatternNotFound,
_ => ServiceError::DBError(DBErrorWrapper(e)),
}
}
}
impl From<ValidationErrors> for ServiceError { impl From<ValidationErrors> for ServiceError {
#[cfg(not(tarpaulin_include))] #[cfg(not(tarpaulin_include))]
fn from(_: ValidationErrors) -> ServiceError { fn from(_: ValidationErrors) -> ServiceError {
@@ -240,6 +200,21 @@ impl From<CaptchaError> for ServiceError {
} }
} }
#[cfg(not(tarpaulin_include))]
impl From<sqlx::Error> for ServiceError {
#[cfg(not(tarpaulin_include))]
fn from(e: sqlx::Error) -> Self {
use sqlx::error::Error;
use std::borrow::Cow;
if let Error::Database(err) = e {
if err.code() == Some(Cow::from("23505")) {
return ServiceError::UsernameTaken;
}
}
ServiceError::InternalServerError
}
}
#[cfg(not(tarpaulin_include))] #[cfg(not(tarpaulin_include))]
impl From<SmtpError> for ServiceError { impl From<SmtpError> for ServiceError {
#[cfg(not(tarpaulin_include))] #[cfg(not(tarpaulin_include))]
@@ -280,19 +255,18 @@ pub enum PageError {
} }
#[cfg(not(tarpaulin_include))] #[cfg(not(tarpaulin_include))]
impl From<ServiceError> for PageError { impl From<sqlx::Error> for PageError {
#[cfg(not(tarpaulin_include))] #[cfg(not(tarpaulin_include))]
fn from(e: ServiceError) -> Self { fn from(_: sqlx::Error) -> Self {
PageError::ServiceError(e) PageError::InternalServerError
} }
} }
#[cfg(not(tarpaulin_include))] #[cfg(not(tarpaulin_include))]
impl From<DBError> for PageError { impl From<ServiceError> for PageError {
#[cfg(not(tarpaulin_include))] #[cfg(not(tarpaulin_include))]
fn from(e: DBError) -> Self { fn from(e: ServiceError) -> Self {
let se: ServiceError = e.into(); PageError::ServiceError(e)
se.into()
} }
} }

View File

@@ -94,8 +94,7 @@ pub const PKG_HOMEPAGE: &str = env!("CARGO_PKG_HOMEPAGE");
pub const CACHE_AGE: u32 = 604800; pub const CACHE_AGE: u32 = 604800;
pub type ArcData = Arc<crate::data::Data>; pub type AppData = actix_web::web::Data<Arc<crate::data::Data>>;
pub type AppData = actix_web::web::Data<ArcData>;
#[cfg(not(tarpaulin_include))] #[cfg(not(tarpaulin_include))]
#[actix_web::main] #[actix_web::main]
@@ -110,13 +109,13 @@ async fn main() -> std::io::Result<()> {
PKG_NAME, PKG_DESCRIPTION, PKG_HOMEPAGE, VERSION, GIT_COMMIT_HASH PKG_NAME, PKG_DESCRIPTION, PKG_HOMEPAGE, VERSION, GIT_COMMIT_HASH
); );
let settings = Settings::new().unwrap(); let data = Data::new().await;
let data = Data::new(&settings).await; sqlx::migrate!("./migrations/").run(&data.db).await.unwrap();
let data = actix_web::web::Data::new(data); let data = actix_web::web::Data::new(data);
let mut demo_user: Option<DemoUser> = None; let mut demo_user: Option<DemoUser> = None;
if settings.allow_demo && settings.allow_registration { if SETTINGS.allow_demo && SETTINGS.allow_registration {
demo_user = Some( demo_user = Some(
DemoUser::spawn(data.clone(), Duration::from_secs(60 * 30)) DemoUser::spawn(data.clone(), Duration::from_secs(60 * 30))
.await .await
@@ -124,17 +123,16 @@ async fn main() -> std::io::Result<()> {
); );
} }
let ip = settings.server.get_ip(); println!("Starting server on: http://{}", SETTINGS.server.get_ip());
println!("Starting server on: http://{ip}");
HttpServer::new(move || { HttpServer::new(move || {
App::new() App::new()
.wrap(actix_middleware::Logger::default()) .wrap(actix_middleware::Logger::default())
.wrap( .wrap(
actix_middleware::DefaultHeaders::new() actix_middleware::DefaultHeaders::new()
.add(("Permissions-Policy", "interest-cohort=()")), .header("Permissions-Policy", "interest-cohort=()"),
) )
.wrap(get_identity_service(&settings)) .wrap(get_identity_service())
.wrap(actix_middleware::Compress::default()) .wrap(actix_middleware::Compress::default())
.app_data(data.clone()) .app_data(data.clone())
.wrap(actix_middleware::NormalizePath::new( .wrap(actix_middleware::NormalizePath::new(
@@ -143,7 +141,7 @@ async fn main() -> std::io::Result<()> {
.configure(routes::services) .configure(routes::services)
.app_data(get_json_err()) .app_data(get_json_err())
}) })
.bind(&ip) .bind(SETTINGS.server.get_ip())
.unwrap() .unwrap()
.run() .run()
.await?; .await?;
@@ -163,16 +161,14 @@ pub fn get_json_err() -> JsonConfig {
} }
#[cfg(not(tarpaulin_include))] #[cfg(not(tarpaulin_include))]
pub fn get_identity_service( pub fn get_identity_service() -> IdentityService<CookieIdentityPolicy> {
settings: &Settings, let cookie_secret = &SETTINGS.server.cookie_secret;
) -> IdentityService<CookieIdentityPolicy> {
let cookie_secret = &settings.server.cookie_secret;
IdentityService::new( IdentityService::new(
CookieIdentityPolicy::new(cookie_secret.as_bytes()) CookieIdentityPolicy::new(cookie_secret.as_bytes())
.name("Authorization") .name("Authorization")
//TODO change cookie age //TODO change cookie age
.max_age_secs(216000) .max_age_secs(216000)
.domain(&settings.server.domain) .domain(&SETTINGS.server.domain)
.secure(false), .secure(false),
) )
} }

View File

@@ -20,6 +20,7 @@ use lazy_static::lazy_static;
use my_codegen::get; use my_codegen::get;
use sailfish::TemplateOnce; use sailfish::TemplateOnce;
use crate::api::v1::RedirectQuery;
use crate::PAGES; use crate::PAGES;
#[derive(Clone, TemplateOnce)] #[derive(Clone, TemplateOnce)]

View File

@@ -11,9 +11,7 @@
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details. * GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>. */
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use actix_web::{web, HttpResponse, Responder}; use actix_web::{web, HttpResponse, Responder};
use lazy_static::lazy_static; use lazy_static::lazy_static;

View File

@@ -11,8 +11,7 @@
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details. * GNU Affero General Public License for more details.
* *
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License along with this program. If not, see <https://www.gnu.org/licenses/>.
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
use actix_auth_middleware::Authentication; use actix_auth_middleware::Authentication;
use actix_web::web::ServiceConfig; use actix_web::web::ServiceConfig;
@@ -52,12 +51,13 @@ mod tests {
const PASSWORD: &str = "longpassword"; const PASSWORD: &str = "longpassword";
const EMAIL: &str = "templateuser@a.com"; const EMAIL: &str = "templateuser@a.com";
let data = get_data().await; {
let data = &data; let data = Data::new().await;
delete_user(data, NAME).await; delete_user(NAME, &data).await;
}
register_and_signin(data, NAME, EMAIL, PASSWORD).await; register_and_signin(NAME, EMAIL, PASSWORD).await;
let (_, signin_resp, token_key) = add_levels_util(data, NAME, PASSWORD).await; let (data, _, signin_resp, token_key) = add_levels_util(NAME, PASSWORD).await;
let cookies = get_cookie!(signin_resp); let cookies = get_cookie!(signin_resp);
let app = get_app!(data).await; let app = get_app!(data).await;
@@ -95,7 +95,7 @@ mod tests {
assert_eq!(authenticated_resp.status(), StatusCode::OK); assert_eq!(authenticated_resp.status(), StatusCode::OK);
} }
delete_user(data, NAME).await; delete_user(NAME, &data).await;
} }
#[actix_rt::test] #[actix_rt::test]

View File

@@ -23,19 +23,18 @@ mod notifications;
mod settings; mod settings;
pub mod sitekey; pub mod sitekey;
use db_core::Captcha;
use crate::errors::PageResult; use crate::errors::PageResult;
use crate::AppData; use crate::AppData;
use sitekey::list::{get_list_sitekeys, SiteKeys};
#[derive(TemplateOnce, Clone)] #[derive(TemplateOnce, Clone)]
#[template(path = "panel/index.html")] #[template(path = "panel/index.html")]
pub struct IndexPage { pub struct IndexPage {
sitekeys: Vec<Captcha>, sitekeys: SiteKeys,
} }
impl IndexPage { impl IndexPage {
fn new(sitekeys: Vec<Captcha>) -> Self { fn new(sitekeys: SiteKeys) -> Self {
IndexPage { sitekeys } IndexPage { sitekeys }
} }
} }
@@ -47,8 +46,7 @@ const PAGE: &str = "Dashboard";
wrap = "crate::pages::get_middleware()" wrap = "crate::pages::get_middleware()"
)] )]
async fn panel(data: AppData, id: Identity) -> PageResult<impl Responder> { async fn panel(data: AppData, id: Identity) -> PageResult<impl Responder> {
let username = id.identity().unwrap(); let sitekeys = get_list_sitekeys(&data, &id).await?;
let sitekeys = data.db.get_all_user_captchas(&username).await?;
let body = IndexPage::new(sitekeys).render_once().unwrap(); let body = IndexPage::new(sitekeys).render_once().unwrap();
Ok(HttpResponse::Ok() Ok(HttpResponse::Ok()
.content_type("text/html; charset=utf-8") .content_type("text/html; charset=utf-8")

View File

@@ -20,6 +20,7 @@ use actix_web::{HttpResponse, Responder};
use sailfish::TemplateOnce; use sailfish::TemplateOnce;
use sqlx::types::time::OffsetDateTime; use sqlx::types::time::OffsetDateTime;
use crate::api::v1::notifications::get::{self, runner};
use crate::date::Date; use crate::date::Date;
use crate::errors::PageResult; use crate::errors::PageResult;
use crate::AppData; use crate::AppData;
@@ -45,12 +46,12 @@ pub struct Notification {
pub id: i32, pub id: i32,
} }
impl From<db_core::Notification> for Notification { impl From<get::Notification> for Notification {
fn from(n: db_core::Notification) -> Self { fn from(n: get::Notification) -> Self {
Notification { Notification {
name: n.name.unwrap(), name: n.name.unwrap(),
heading: n.heading.unwrap(), heading: n.heading.unwrap(),
received: OffsetDateTime::from_unix_timestamp(n.received.unwrap()), received: n.received.unwrap(),
id: n.id.unwrap(), id: n.id.unwrap(),
message: n.message.unwrap(), message: n.message.unwrap(),
} }
@@ -73,8 +74,7 @@ pub async fn notifications(data: AppData, id: Identity) -> PageResult<impl Respo
let receiver = id.identity().unwrap(); let receiver = id.identity().unwrap();
// TODO handle error where payload.to doesnt exist // TODO handle error where payload.to doesnt exist
// let mut notifications = runner::get_notification(&data, &receiver).await?; let mut notifications = runner::get_notification(&data, &receiver).await?;
let mut notifications = data.db.get_all_unread_notifications(&receiver).await?;
let notifications = notifications.drain(0..).map(|x| x.into()).collect(); let notifications = notifications.drain(0..).map(|x| x.into()).collect();
let body = IndexPage::new(notifications).render_once().unwrap(); let body = IndexPage::new(notifications).render_once().unwrap();

View File

@@ -69,13 +69,22 @@ pub struct IndexPage<'a> {
async fn settings(data: AppData, id: Identity) -> PageResult<impl Responder> { async fn settings(data: AppData, id: Identity) -> PageResult<impl Responder> {
let username = id.identity().unwrap(); let username = id.identity().unwrap();
let secret = data.db.get_secret(&username).await?; struct DBResult {
let secret = secret.secret; email: Option<String>,
let email = data.db.get_email(&username).await?; secret: String,
}
let details = sqlx::query_as!(
DBResult,
r#"SELECT email, secret FROM mcaptcha_users WHERE name = ($1)"#,
&username,
)
.fetch_one(&data.db)
.await?;
let data = IndexPage { let data = IndexPage {
email, email: details.email,
secret, secret: details.secret,
username: &username, username: &username,
}; };

View File

@@ -17,17 +17,27 @@
use actix_identity::Identity; use actix_identity::Identity;
use actix_web::{http, web, HttpResponse, Responder}; use actix_web::{http, web, HttpResponse, Responder};
use sailfish::TemplateOnce; use sailfish::TemplateOnce;
use sqlx::Error::RowNotFound;
use db_core::errors::DBError; use crate::api::v1::mcaptcha::easy::TrafficPattern;
use db_core::Captcha;
use libmcaptcha::defense::Level;
use crate::api::v1::mcaptcha::easy::TrafficPatternRequest;
use crate::errors::*; use crate::errors::*;
use crate::AppData; use crate::AppData;
const PAGE: &str = "Edit Sitekey"; const PAGE: &str = "Edit Sitekey";
#[derive(Clone)]
struct McaptchaConfig {
config_id: i32,
duration: i32,
name: String,
}
#[derive(Clone)]
struct Level {
difficulty_factor: i32,
visitor_threshold: i32,
}
#[derive(TemplateOnce, Clone)] #[derive(TemplateOnce, Clone)]
#[template(path = "panel/sitekey/edit/advance.html")] #[template(path = "panel/sitekey/edit/advance.html")]
struct AdvanceEditPage { struct AdvanceEditPage {
@@ -38,10 +48,10 @@ struct AdvanceEditPage {
} }
impl AdvanceEditPage { impl AdvanceEditPage {
fn new(config: Captcha, levels: Vec<Level>, key: String) -> Self { fn new(config: McaptchaConfig, levels: Vec<Level>, key: String) -> Self {
AdvanceEditPage { AdvanceEditPage {
duration: config.duration as u32, duration: config.duration as u32,
name: config.description, name: config.name,
levels, levels,
key, key,
} }
@@ -61,8 +71,28 @@ pub async fn advance(
let username = id.identity().unwrap(); let username = id.identity().unwrap();
let key = path.into_inner(); let key = path.into_inner();
let config = data.db.get_captcha_config(&username, &key).await?; let config = sqlx::query_as!(
let levels = data.db.get_captcha_levels(Some(&username), &key).await?; McaptchaConfig,
"SELECT config_id, duration, name from mcaptcha_config WHERE
key = $1 AND
user_id = (SELECT ID FROM mcaptcha_users WHERE name = $2) ",
&key,
&username,
)
.fetch_one(&data.db)
.await?;
let levels = sqlx::query_as!(
Level,
"SELECT
difficulty_factor, visitor_threshold
FROM
mcaptcha_levels
WHERE config_id = $1 ORDER BY difficulty_factor ASC",
&config.config_id
)
.fetch_all(&data.db)
.await?;
let body = AdvanceEditPage::new(config, levels, key) let body = AdvanceEditPage::new(config, levels, key)
.render_once() .render_once()
@@ -76,12 +106,12 @@ pub async fn advance(
#[template(path = "panel/sitekey/edit/easy/index.html")] #[template(path = "panel/sitekey/edit/easy/index.html")]
pub struct EasyEditPage<'a> { pub struct EasyEditPage<'a> {
pub form_title: &'a str, pub form_title: &'a str,
pub pattern: TrafficPatternRequest, pub pattern: TrafficPattern,
pub key: String, pub key: String,
} }
impl<'a> EasyEditPage<'a> { impl<'a> EasyEditPage<'a> {
pub fn new(key: String, pattern: TrafficPatternRequest) -> Self { pub fn new(key: String, pattern: TrafficPattern) -> Self {
Self { Self {
form_title: PAGE, form_title: PAGE,
pattern, pattern,
@@ -103,14 +133,65 @@ pub async fn easy(
let username = id.identity().unwrap(); let username = id.identity().unwrap();
let key = path.into_inner(); let key = path.into_inner();
match data.db.get_traffic_pattern(&username, &key).await { struct Traffic {
peak_sustainable_traffic: i32,
avg_traffic: i32,
broke_my_site_traffic: Option<i32>,
}
match sqlx::query_as!(
Traffic,
"SELECT
avg_traffic,
peak_sustainable_traffic,
broke_my_site_traffic
FROM
mcaptcha_sitekey_user_provided_avg_traffic
WHERE
config_id = (
SELECT
config_id
FROM
mcaptcha_config
WHERE
KEY = $1
AND user_id = (
SELECT
id
FROM
mcaptcha_users
WHERE
NAME = $2
)
)
",
&key,
&username
)
.fetch_one(&data.db)
.await
{
Ok(c) => { Ok(c) => {
let config = data.db.get_captcha_config(&username, &key).await?; struct Description {
let pattern = TrafficPatternRequest { name: String,
}
let description = sqlx::query_as!(
Description,
"SELECT name FROM mcaptcha_config
WHERE key = $1
AND user_id = (
SELECT user_id FROM mcaptcha_users WHERE NAME = $2)",
&key,
&username
)
.fetch_one(&data.db)
.await?;
let pattern = TrafficPattern {
peak_sustainable_traffic: c.peak_sustainable_traffic as u32, peak_sustainable_traffic: c.peak_sustainable_traffic as u32,
avg_traffic: c.avg_traffic as u32, avg_traffic: c.avg_traffic as u32,
broke_my_site_traffic: c.broke_my_site_traffic.map(|n| n as u32), broke_my_site_traffic: c.broke_my_site_traffic.map(|n| n as u32),
description: config.description, description: description.name,
}; };
let page = EasyEditPage::new(key, pattern).render_once().unwrap(); let page = EasyEditPage::new(key, pattern).render_once().unwrap();
@@ -118,7 +199,7 @@ pub async fn easy(
.content_type("text/html; charset=utf-8") .content_type("text/html; charset=utf-8")
.body(page)); .body(page));
} }
Err(DBError::TrafficPatternNotFound) => { Err(RowNotFound) => {
return Ok(HttpResponse::Found() return Ok(HttpResponse::Found()
.insert_header(( .insert_header((
http::header::LOCATION, http::header::LOCATION,
@@ -126,10 +207,7 @@ pub async fn easy(
)) ))
.finish()); .finish());
} }
Err(e) => { Err(e) => Err(e.into()),
let e: ServiceError = e.into();
Err(e.into())
}
} }
} }
@@ -147,12 +225,14 @@ mod test {
const NAME: &str = "editsitekeyuser"; const NAME: &str = "editsitekeyuser";
const PASSWORD: &str = "longpassworddomain"; const PASSWORD: &str = "longpassworddomain";
const EMAIL: &str = "editsitekeyuser@a.com"; const EMAIL: &str = "editsitekeyuser@a.com";
let data = get_data().await;
let data = &data;
delete_user(data, NAME).await;
register_and_signin(data, NAME, EMAIL, PASSWORD).await; {
let (_, signin_resp, key) = add_levels_util(data, NAME, PASSWORD).await; let data = Data::new().await;
delete_user(NAME, &data).await;
}
register_and_signin(NAME, EMAIL, PASSWORD).await;
let (data, _, signin_resp, key) = add_levels_util(NAME, PASSWORD).await;
let cookies = get_cookie!(signin_resp); let cookies = get_cookie!(signin_resp);
let app = get_app!(data).await; let app = get_app!(data).await;

View File

@@ -19,21 +19,20 @@ use actix_identity::Identity;
use actix_web::{HttpResponse, Responder}; use actix_web::{HttpResponse, Responder};
use sailfish::TemplateOnce; use sailfish::TemplateOnce;
use db_core::Captcha; use crate::api::v1::mcaptcha::create::MCaptchaDetails;
use crate::errors::*; use crate::errors::*;
use crate::AppData; use crate::AppData;
#[derive(TemplateOnce, Clone)] #[derive(TemplateOnce, Clone)]
#[template(path = "panel/sitekey/list/index.html")] #[template(path = "panel/sitekey/list/index.html")]
pub struct IndexPage { pub struct IndexPage {
sitekeys: Vec<Captcha>, sitekeys: SiteKeys,
} }
const PAGE: &str = "SiteKeys"; const PAGE: &str = "SiteKeys";
impl IndexPage { impl IndexPage {
fn new(sitekeys: Vec<Captcha>) -> Self { fn new(sitekeys: SiteKeys) -> Self {
IndexPage { sitekeys } IndexPage { sitekeys }
} }
} }
@@ -44,14 +43,29 @@ impl IndexPage {
wrap = "crate::pages::get_middleware()" wrap = "crate::pages::get_middleware()"
)] )]
pub async fn list_sitekeys(data: AppData, id: Identity) -> PageResult<impl Responder> { pub async fn list_sitekeys(data: AppData, id: Identity) -> PageResult<impl Responder> {
let username = id.identity().unwrap(); let res = get_list_sitekeys(&data, &id).await?;
let res = data.db.get_all_user_captchas(&username).await?;
let body = IndexPage::new(res).render_once().unwrap(); let body = IndexPage::new(res).render_once().unwrap();
Ok(HttpResponse::Ok() Ok(HttpResponse::Ok()
.content_type("text/html; charset=utf-8") .content_type("text/html; charset=utf-8")
.body(body)) .body(body))
} }
/// utility function to get a list of all sitekeys that a user owns
pub async fn get_list_sitekeys(data: &AppData, id: &Identity) -> PageResult<SiteKeys> {
let username = id.identity().unwrap();
let res = sqlx::query_as!(
MCaptchaDetails,
"SELECT key, name from mcaptcha_config WHERE
user_id = (SELECT ID FROM mcaptcha_users WHERE name = $1) ",
&username,
)
.fetch_all(&data.db)
.await?;
Ok(res)
}
pub type SiteKeys = Vec<MCaptchaDetails>;
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use actix_web::http::StatusCode; use actix_web::http::StatusCode;
@@ -67,12 +81,13 @@ mod test {
const PASSWORD: &str = "longpassworddomain"; const PASSWORD: &str = "longpassworddomain";
const EMAIL: &str = "listsitekeyuser@a.com"; const EMAIL: &str = "listsitekeyuser@a.com";
let data = get_data().await; {
let data = &data; let data = Data::new().await;
delete_user(data, NAME).await; delete_user(NAME, &data).await;
}
register_and_signin(data, NAME, EMAIL, PASSWORD).await; register_and_signin(NAME, EMAIL, PASSWORD).await;
let (_, signin_resp, key) = add_levels_util(data, NAME, PASSWORD).await; let (data, _, signin_resp, key) = add_levels_util(NAME, PASSWORD).await;
let cookies = get_cookie!(signin_resp); let cookies = get_cookie!(signin_resp);
let app = get_app!(data).await; let app = get_app!(data).await;

View File

@@ -17,17 +17,27 @@
use actix_identity::Identity; use actix_identity::Identity;
use actix_web::{web, HttpResponse, Responder}; use actix_web::{web, HttpResponse, Responder};
use futures::{future::TryFutureExt, try_join};
use sailfish::TemplateOnce; use sailfish::TemplateOnce;
use db_core::Captcha;
use libmcaptcha::defense::Level;
use crate::errors::*; use crate::errors::*;
use crate::stats::CaptchaStats; use crate::stats::fetch::Stats;
use crate::AppData; use crate::AppData;
const PAGE: &str = "SiteKeys"; const PAGE: &str = "SiteKeys";
#[derive(Clone)]
struct McaptchaConfig {
config_id: i32,
duration: i32,
name: String,
}
#[derive(Clone)]
struct Level {
difficulty_factor: i32,
visitor_threshold: i32,
}
#[derive(TemplateOnce, Clone)] #[derive(TemplateOnce, Clone)]
#[template(path = "panel/sitekey/view/index.html")] #[template(path = "panel/sitekey/view/index.html")]
struct IndexPage { struct IndexPage {
@@ -35,19 +45,19 @@ struct IndexPage {
name: String, name: String,
key: String, key: String,
levels: Vec<Level>, levels: Vec<Level>,
stats: CaptchaStats, stats: Stats,
} }
impl IndexPage { impl IndexPage {
fn new( fn new(
stats: CaptchaStats, stats: Stats,
config: Captcha, config: McaptchaConfig,
levels: Vec<Level>, levels: Vec<Level>,
key: String, key: String,
) -> Self { ) -> Self {
IndexPage { IndexPage {
duration: config.duration as u32, duration: config.duration as u32,
name: config.description, name: config.name,
levels, levels,
key, key,
stats, stats,
@@ -67,9 +77,31 @@ pub async fn view_sitekey(
) -> PageResult<impl Responder> { ) -> PageResult<impl Responder> {
let username = id.identity().unwrap(); let username = id.identity().unwrap();
let key = path.into_inner(); let key = path.into_inner();
let config = data.db.get_captcha_config(&username, &key).await?;
let levels = data.db.get_captcha_levels(Some(&username), &key).await?; let config = sqlx::query_as!(
let stats = data.stats.fetch(&data, &username, &key).await?; McaptchaConfig,
"SELECT config_id, duration, name from mcaptcha_config WHERE
key = $1 AND
user_id = (SELECT ID FROM mcaptcha_users WHERE name = $2) ",
&key,
&username,
)
.fetch_one(&data.db)
.await?;
let levels_fut = sqlx::query_as!(
Level,
"SELECT
difficulty_factor, visitor_threshold
FROM
mcaptcha_levels
WHERE config_id = $1 ORDER BY difficulty_factor ASC",
&config.config_id
)
.fetch_all(&data.db)
.err_into();
let (stats, levels) = try_join!(Stats::new(&username, &key, &data.db), levels_fut)?;
let body = IndexPage::new(stats, config, levels, key) let body = IndexPage::new(stats, config, levels, key)
.render_once() .render_once()
@@ -94,12 +126,13 @@ mod test {
const PASSWORD: &str = "longpassworddomain"; const PASSWORD: &str = "longpassworddomain";
const EMAIL: &str = "viewsitekeyuser@a.com"; const EMAIL: &str = "viewsitekeyuser@a.com";
let data = get_data().await; {
let data = &data; let data = Data::new().await;
delete_user(data, NAME).await; delete_user(NAME, &data).await;
}
register_and_signin(data, NAME, EMAIL, PASSWORD).await; register_and_signin(NAME, EMAIL, PASSWORD).await;
let (_, signin_resp, key) = add_levels_util(data, NAME, PASSWORD).await; let (data, _, signin_resp, key) = add_levels_util(NAME, PASSWORD).await;
let cookies = get_cookie!(signin_resp); let cookies = get_cookie!(signin_resp);
let app = get_app!(data).await; let app = get_app!(data).await;

View File

@@ -36,7 +36,6 @@ pub struct Server {
pub struct Captcha { pub struct Captcha {
pub salt: String, pub salt: String,
pub gc: u64, pub gc: u64,
pub enable_stats: bool,
pub default_difficulty_strategy: DefaultDifficultyStrategy, pub default_difficulty_strategy: DefaultDifficultyStrategy,
} }
@@ -122,12 +121,10 @@ impl Settings {
pub fn new() -> Result<Self, ConfigError> { pub fn new() -> Result<Self, ConfigError> {
let mut s = Config::new(); let mut s = Config::new();
const CURRENT_DIR: &str = "./config/default.toml"; const CURRENT_DIR: &str = "./config/default.toml";
const ETC: &str = "/etc/mcaptcha/config.toml"; const ETC: &str = "/etc/mcaptcha/config.toml";
s.set("capatcha.enable_stats", true.to_string())
.expect("unable to set capatcha.enable_stats default config");
if let Ok(path) = env::var("MCAPTCHA_CONFIG") { if let Ok(path) = env::var("MCAPTCHA_CONFIG") {
s.merge(File::with_name(&path))?; s.merge(File::with_name(&path))?;
} else if Path::new(CURRENT_DIR).exists() { } else if Path::new(CURRENT_DIR).exists() {
@@ -166,6 +163,8 @@ impl Settings {
s.set("database.pool", 2.to_string()) s.set("database.pool", 2.to_string())
.expect("Couldn't set database pool count"); .expect("Couldn't set database pool count");
match s.try_into() { match s.try_into() {
Ok(val) => Ok(val), Ok(val) => Ok(val),
Err(e) => Err(ConfigError::Message(format!("\n\nError: {}. If it says missing fields, then please refer to https://github.com/mCaptcha/mcaptcha#configuration to learn more about how mcaptcha reads configuration\n\n", e))), Err(e) => Err(ConfigError::Message(format!("\n\nError: {}. If it says missing fields, then please refer to https://github.com/mCaptcha/mcaptcha#configuration to learn more about how mcaptcha reads configuration\n\n", e))),

View File

@@ -100,6 +100,7 @@ fn handle_assets(path: &str) -> HttpResponse {
} }
} }
#[get("/assets/{_:.*}")] #[get("/assets/{_:.*}")]
pub async fn static_files(path: web::Path<String>) -> impl Responder { pub async fn static_files(path: web::Path<String>) -> impl Responder {
handle_assets(&path) handle_assets(&path)
@@ -130,6 +131,7 @@ fn handle_favicons(path: &str) -> HttpResponse {
} }
} }
#[get("/{file}")] #[get("/{file}")]
pub async fn favicons(path: web::Path<String>) -> impl Responder { pub async fn favicons(path: web::Path<String>) -> impl Responder {
debug!("searching favicons"); debug!("searching favicons");
@@ -141,28 +143,64 @@ mod tests {
use actix_web::http::StatusCode; use actix_web::http::StatusCode;
use actix_web::test; use actix_web::test;
use super::*;
use crate::*; use crate::*;
#[actix_rt::test] #[actix_rt::test]
async fn static_assets_work() { async fn static_assets_work() {
let app = get_app!().await; let app = get_app!().await;
let urls = [ let resp = test::call_service(
*crate::JS, &app,
*crate::VERIFICATIN_WIDGET_JS, test::TestRequest::get().uri(*crate::JS).to_request(),
*crate::VERIFICATIN_WIDGET_CSS, )
.await;
assert_eq!(resp.status(), StatusCode::OK);
let resp = test::call_service(
&app,
test::TestRequest::get()
.uri(*crate::VERIFICATIN_WIDGET_JS)
.to_request(),
)
.await;
assert_eq!(resp.status(), StatusCode::OK);
let resp = test::call_service(
&app,
test::TestRequest::get()
.uri(*crate::VERIFICATIN_WIDGET_CSS)
.to_request(),
)
.await;
assert_eq!(resp.status(), StatusCode::OK);
let resp = test::call_service(
&app,
test::TestRequest::get()
.uri(
crate::FILES crate::FILES
.get("./static/cache/img/icon-trans.png") .get("./static/cache/img/icon-trans.png")
.unwrap(), .unwrap(),
"/favicon.ico", )
]; .to_request(),
)
.await;
assert_eq!(resp.status(), StatusCode::OK);
}
for u in urls.iter() { #[actix_rt::test]
println!("[*] Testing static asset at URL: {u}"); async fn favicons_work() {
let resp = assert!(Favicons::get("favicon.ico").is_some());
test::call_service(&app, test::TestRequest::get().uri(u).to_request())
//let app = test::init_service(App::new().configure(services)).await;
let app = get_app!().await;
let resp = test::call_service(
&app,
test::TestRequest::get().uri("/favicon.ico").to_request(),
)
.await; .await;
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
} }
} }
}

View File

@@ -1,130 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use async_trait::async_trait;
use db_core::errors::DBResult;
use serde::{Deserialize, Serialize};
use crate::data::Data;
#[async_trait]
pub trait Stats: std::marker::Send + std::marker::Sync + CloneStats {
/// record PoWConfig fetches
async fn record_fetch(&self, d: &Data, key: &str) -> DBResult<()>;
/// record PoWConfig solves
async fn record_solve(&self, d: &Data, key: &str) -> DBResult<()>;
/// record PoWConfig confirms
async fn record_confirm(&self, d: &Data, key: &str) -> DBResult<()>;
/// fetch stats
async fn fetch(&self, d: &Data, user: &str, key: &str) -> DBResult<CaptchaStats>;
}
/// Trait to clone MCDatabase
pub trait CloneStats {
/// clone DB
fn clone_stats(&self) -> Box<dyn Stats>;
}
impl<T> CloneStats for T
where
T: Stats + Clone + 'static,
{
fn clone_stats(&self) -> Box<dyn Stats> {
Box::new(self.clone())
}
}
//impl Clone for Box<dyn CloneStats> {
// fn clone(&self) -> Self {
// Box::clone(self)
// //(*self).clone_stats()
// }
//}
#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)]
pub struct CaptchaStats {
pub config_fetches: Vec<i64>,
pub solves: Vec<i64>,
pub confirms: Vec<i64>,
}
#[derive(Clone, Default, PartialEq, Debug)]
pub struct Real;
#[async_trait]
impl Stats for Real {
/// record PoWConfig fetches
async fn record_fetch(&self, d: &Data, key: &str) -> DBResult<()> {
d.db.record_fetch(key).await
}
/// record PoWConfig solves
async fn record_solve(&self, d: &Data, key: &str) -> DBResult<()> {
d.db.record_solve(key).await
}
/// record PoWConfig confirms
async fn record_confirm(&self, d: &Data, key: &str) -> DBResult<()> {
d.db.record_confirm(key).await
}
/// fetch stats
async fn fetch(&self, d: &Data, user: &str, key: &str) -> DBResult<CaptchaStats> {
let config_fetches_fut = d.db.fetch_config_fetched(user, key);
let solves_fut = d.db.fetch_solve(user, key);
let confirms_fut = d.db.fetch_confirm(user, key);
let (config_fetches, solves, confirms) =
futures::try_join!(config_fetches_fut, solves_fut, confirms_fut)?;
let res = CaptchaStats {
config_fetches,
solves,
confirms,
};
Ok(res)
}
}
#[derive(Clone, Default, PartialEq, Debug)]
pub struct Dummy;
#[async_trait]
impl Stats for Dummy {
/// record PoWConfig fetches
async fn record_fetch(&self, _: &Data, _: &str) -> DBResult<()> {
Ok(())
}
/// record PoWConfig solves
async fn record_solve(&self, _: &Data, _: &str) -> DBResult<()> {
Ok(())
}
/// record PoWConfig confirms
async fn record_confirm(&self, _: &Data, _: &str) -> DBResult<()> {
Ok(())
}
/// fetch stats
async fn fetch(&self, _: &Data, _: &str, _: &str) -> DBResult<CaptchaStats> {
Ok(CaptchaStats::default())
}
}

219
src/stats/fetch.rs Normal file
View File

@@ -0,0 +1,219 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use serde::{Deserialize, Serialize};
use sqlx::PgPool;
use crate::date::Date;
use crate::errors::*;
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct StatsUnixTimestamp {
pub config_fetches: Vec<i64>,
pub solves: Vec<i64>,
pub confirms: Vec<i64>,
}
#[derive(Debug, Clone)]
pub struct Stats {
pub config_fetches: Vec<Date>,
pub solves: Vec<Date>,
pub confirms: Vec<Date>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct StatsPayload {
pub key: String,
}
impl Stats {
pub async fn new(user: &str, key: &str, db: &PgPool) -> ServiceResult<Self> {
let config_fetches_fut = runners::fetch_config_fetched(user, key, db);
let solves_fut = runners::fetch_solve(user, key, db);
let confirms_fut = runners::fetch_confirm(user, key, db);
let (config_fetches, solves, confirms) =
futures::try_join!(config_fetches_fut, solves_fut, confirms_fut)?;
let res = Self {
config_fetches,
solves,
confirms,
};
Ok(res)
}
}
impl StatsUnixTimestamp {
pub fn from_stats(stats: &Stats) -> Self {
let config_fetches = Self::unix_timestamp(&stats.config_fetches);
let solves = Self::unix_timestamp(&stats.solves);
let confirms = Self::unix_timestamp(&stats.confirms);
Self {
config_fetches,
solves,
confirms,
}
}
/// featch PoWConfig confirms
#[inline]
fn unix_timestamp(dates: &[Date]) -> Vec<i64> {
let mut res: Vec<i64> = Vec::with_capacity(dates.len());
dates
.iter()
.for_each(|record| res.push(record.time.unix_timestamp()));
res
}
}
pub mod runners {
use super::*;
/// featch PoWConfig fetches
#[inline]
pub async fn fetch_config_fetched(
user: &str,
key: &str,
db: &PgPool,
) -> ServiceResult<Vec<Date>> {
let records = sqlx::query_as!(
Date,
"SELECT time FROM mcaptcha_pow_fetched_stats
WHERE
config_id = (
SELECT
config_id FROM mcaptcha_config
WHERE
key = $1
AND
user_id = (
SELECT
ID FROM mcaptcha_users WHERE name = $2))
ORDER BY time DESC",
&key,
&user,
)
.fetch_all(db)
.await?;
Ok(records)
}
/// featch PoWConfig solves
#[inline]
pub async fn fetch_solve(
user: &str,
key: &str,
db: &PgPool,
) -> ServiceResult<Vec<Date>> {
let records = sqlx::query_as!(
Date,
"SELECT time FROM mcaptcha_pow_solved_stats
WHERE config_id = (
SELECT config_id FROM mcaptcha_config
WHERE
key = $1
AND
user_id = (
SELECT
ID FROM mcaptcha_users WHERE name = $2))
ORDER BY time DESC",
&key,
&user
)
.fetch_all(db)
.await?;
Ok(records)
}
/// featch PoWConfig confirms
#[inline]
pub async fn fetch_confirm(
user: &str,
key: &str,
db: &PgPool,
) -> ServiceResult<Vec<Date>> {
let records = sqlx::query_as!(
Date,
"SELECT time FROM mcaptcha_pow_confirmed_stats
WHERE
config_id = (
SELECT config_id FROM mcaptcha_config
WHERE
key = $1
AND
user_id = (
SELECT
ID FROM mcaptcha_users WHERE name = $2))
ORDER BY time DESC",
&key,
&user
)
.fetch_all(db)
.await?;
Ok(records)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::stats::record::*;
use crate::tests::*;
use crate::*;
#[actix_rt::test]
async fn stats_works() {
const NAME: &str = "statsuser";
const PASSWORD: &str = "testingpas";
const EMAIL: &str = "statsuser@a.com";
let data = Data::new().await;
delete_user(NAME, &data).await;
register_and_signin(NAME, EMAIL, PASSWORD).await;
let (_, _, _, token_key) = add_levels_util(NAME, PASSWORD).await;
let key = token_key.key.clone();
let stats = Stats::new(NAME, &key, &data.db).await.unwrap();
assert_eq!(stats.config_fetches.len(), 0);
assert_eq!(stats.solves.len(), 0);
assert_eq!(stats.confirms.len(), 0);
futures::join!(
record_fetch(&key, &data.db),
record_solve(&key, &data.db),
record_confirm(&key, &data.db)
);
let stats = Stats::new(NAME, &key, &data.db).await.unwrap();
assert_eq!(stats.config_fetches.len(), 1);
assert_eq!(stats.solves.len(), 1);
assert_eq!(stats.confirms.len(), 1);
let ustats = StatsUnixTimestamp::from_stats(&stats);
assert_eq!(ustats.config_fetches.len(), 1);
assert_eq!(ustats.solves.len(), 1);
assert_eq!(ustats.confirms.len(), 1);
}
}

View File

@@ -14,27 +14,6 @@
* You should have received a copy of the GNU Affero General Public License * You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>. * along with this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
use std::env;
use sqlx::postgres::PgPoolOptions; pub mod fetch;
pub mod record;
#[cfg(not(tarpaulin_include))]
#[actix_rt::main]
async fn main() {
//TODO featuregate sqlite and postgres
postgres_migrate().await;
}
async fn postgres_migrate() {
let db_url = env::var("POSTGRES_DATABASE_URL").expect("set POSTGRES_DATABASE_URL env var");
let db = PgPoolOptions::new()
.max_connections(2)
.connect(&db_url)
.await
.expect("Unable to form database pool");
sqlx::migrate!("../db-sqlx-postgres/migrations/")
.run(&db)
.await
.unwrap();
}

61
src/stats/record.rs Normal file
View File

@@ -0,0 +1,61 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use sqlx::types::time::OffsetDateTime;
use sqlx::PgPool;
/// record PoWConfig fetches
#[inline]
pub async fn record_fetch(key: &str, db: &PgPool) {
let now = OffsetDateTime::now_utc();
let _ = sqlx::query!(
"INSERT INTO mcaptcha_pow_fetched_stats
(config_id, time) VALUES ((SELECT config_id FROM mcaptcha_config WHERE key = $1), $2)",
&key,
&now,
)
.execute(db)
.await;
}
/// record PoWConfig solves
#[inline]
pub async fn record_solve(key: &str, db: &PgPool) {
let now = OffsetDateTime::now_utc();
let _ = sqlx::query!(
"INSERT INTO mcaptcha_pow_solved_stats
(config_id, time) VALUES ((SELECT config_id FROM mcaptcha_config WHERE key = $1), $2)",
&key,
&now,
)
.execute(db)
.await;
}
/// record PoWConfig confirms
#[inline]
pub async fn record_confirm(key: &str, db: &PgPool) {
let now = OffsetDateTime::now_utc();
let _ = sqlx::query!(
"INSERT INTO mcaptcha_pow_confirmed_stats
(config_id, time) VALUES ((SELECT config_id FROM mcaptcha_config WHERE key = $1), $2)",
&key,
&now
)
.execute(db)
.await;
}

85
src/tests-migrate.rs Normal file
View File

@@ -0,0 +1,85 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::env;
use lazy_static::lazy_static;
use sqlx::postgres::PgPoolOptions;
mod settings;
pub use settings::Settings;
#[cfg(not(tarpaulin_include))]
lazy_static! {
#[cfg(not(tarpaulin_include))]
pub static ref SETTINGS: Settings = Settings::new().unwrap();
}
#[cfg(not(tarpaulin_include))]
#[actix_rt::main]
async fn main() {
let db = PgPoolOptions::new()
.max_connections(SETTINGS.database.pool)
.connect(&SETTINGS.database.url)
.await
.expect("Unable to form database pool");
for arg in env::args() {
if arg == "--build" {
println!("Building cache buster config");
build();
}
}
sqlx::migrate!("./migrations/").run(&db).await.unwrap();
}
fn build() {
use std::process::Command;
// note: add error checking yourself.
let output = Command::new("git")
.args(&["rev-parse", "HEAD"])
.output()
.unwrap();
let git_hash = String::from_utf8(output.stdout).unwrap();
println!("cargo:rustc-env=GIT_HASH={}", git_hash);
cache_bust();
}
fn cache_bust() {
use cache_buster::BusterBuilder;
let types = vec![
mime::IMAGE_PNG,
mime::IMAGE_SVG,
mime::IMAGE_JPEG,
mime::IMAGE_GIF,
mime::APPLICATION_JAVASCRIPT,
mime::TEXT_CSS,
];
let config = BusterBuilder::default()
.source("./static/cache")
.result("./assets")
.mime_types(types)
.follow_links(true)
.build()
.unwrap();
config.process().unwrap();
}

View File

@@ -1,26 +1,9 @@
/* use std::sync::Arc;
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use actix_web::test; use actix_web::test;
use actix_web::{ use actix_web::{
body::{BoxBody, EitherBody}, dev::ServiceResponse, error::ResponseError, http::StatusCode,
dev::ServiceResponse, body::{EitherBody, BoxBody},
error::ResponseError,
http::StatusCode,
middleware as actix_middleware, middleware as actix_middleware,
}; };
use libmcaptcha::defense::Level; use libmcaptcha::defense::Level;
@@ -31,17 +14,8 @@ use crate::api::v1::auth::runners::{Login, Register};
use crate::api::v1::mcaptcha::create::CreateCaptcha; use crate::api::v1::mcaptcha::create::CreateCaptcha;
use crate::api::v1::mcaptcha::create::MCaptchaDetails; use crate::api::v1::mcaptcha::create::MCaptchaDetails;
use crate::api::v1::ROUTES; use crate::api::v1::ROUTES;
use crate::data::Data;
use crate::errors::*; use crate::errors::*;
use crate::ArcData;
pub fn get_settings() -> Settings {
Settings::new().unwrap()
}
pub async fn get_data() -> ArcData {
let settings = get_settings();
let data = Data::new(&settings).await;
data
}
#[macro_export] #[macro_export]
macro_rules! get_cookie { macro_rules! get_cookie {
@@ -50,6 +24,16 @@ macro_rules! get_cookie {
}; };
} }
pub async fn delete_user(name: &str, data: &Data) {
let r = sqlx::query!("DELETE FROM mcaptcha_users WHERE name = ($1)", name,)
.execute(&data.db)
.await;
println!();
println!();
println!();
println!("Deleting user: {:?}", &r);
}
#[macro_export] #[macro_export]
macro_rules! post_request { macro_rules! post_request {
($uri:expr) => { ($uri:expr) => {
@@ -76,7 +60,7 @@ macro_rules! get_app {
() => { () => {
test::init_service( test::init_service(
App::new() App::new()
// .wrap(get_identity_service(&$data.settings)) .wrap(get_identity_service())
.wrap(actix_middleware::NormalizePath::new( .wrap(actix_middleware::NormalizePath::new(
actix_middleware::TrailingSlash::Trim, actix_middleware::TrailingSlash::Trim,
)) ))
@@ -86,7 +70,7 @@ macro_rules! get_app {
($data:expr) => { ($data:expr) => {
test::init_service( test::init_service(
App::new() App::new()
.wrap(get_identity_service(&$data.settings)) .wrap(get_identity_service())
.wrap(actix_middleware::NormalizePath::new( .wrap(actix_middleware::NormalizePath::new(
actix_middleware::TrailingSlash::Trim, actix_middleware::TrailingSlash::Trim,
)) ))
@@ -97,27 +81,19 @@ macro_rules! get_app {
}; };
} }
pub async fn delete_user(data: &ArcData, name: &str) {
let x = data.db.delete_user(name).await;
println!();
println!();
println!();
println!("Deleting user: {:?}", &x);
}
/// register and signin utility /// register and signin utility
pub async fn register_and_signin( pub async fn register_and_signin(
data: &ArcData,
name: &str, name: &str,
email: &str, email: &str,
password: &str, password: &str,
) -> (Login, ServiceResponse<EitherBody<BoxBody>>) { ) -> (Arc<data::Data>, Login, ServiceResponse<EitherBody<BoxBody>>) {
register(data, name, email, password).await; register(name, email, password).await;
signin(data, name, password).await signin(name, password).await
} }
/// register utility /// register utility
pub async fn register(data: &ArcData, name: &str, email: &str, password: &str) { pub async fn register(name: &str, email: &str, password: &str) {
let data = Data::new().await;
let app = get_app!(data).await; let app = get_app!(data).await;
// 1. Register // 1. Register
@@ -134,11 +110,8 @@ pub async fn register(data: &ArcData, name: &str, email: &str, password: &str) {
} }
/// signin util /// signin util
pub async fn signin( pub async fn signin(name: &str, password: &str) -> (Arc<Data>, Login, ServiceResponse<EitherBody<BoxBody>>) {
data: &ArcData, let data = Data::new().await;
name: &str,
password: &str,
) -> (Login, ServiceResponse<EitherBody<BoxBody>>) {
let app = get_app!(data.clone()).await; let app = get_app!(data.clone()).await;
// 2. signin // 2. signin
@@ -150,19 +123,18 @@ pub async fn signin(
test::call_service(&app, post_request!(&creds, ROUTES.auth.login).to_request()) test::call_service(&app, post_request!(&creds, ROUTES.auth.login).to_request())
.await; .await;
assert_eq!(signin_resp.status(), StatusCode::OK); assert_eq!(signin_resp.status(), StatusCode::OK);
(creds, signin_resp) (data, creds, signin_resp)
} }
/// pub duplicate test /// pub duplicate test
pub async fn bad_post_req_test<T: Serialize>( pub async fn bad_post_req_test<T: Serialize>(
data: &ArcData,
name: &str, name: &str,
password: &str, password: &str,
url: &str, url: &str,
payload: &T, payload: &T,
err: ServiceError, err: ServiceError,
) { ) {
let (_, signin_resp) = signin(data, name, password).await; let (data, _, signin_resp) = signin(name, password).await;
let cookies = get_cookie!(signin_resp); let cookies = get_cookie!(signin_resp);
let app = get_app!(data).await; let app = get_app!(data).await;
@@ -173,41 +145,12 @@ pub async fn bad_post_req_test<T: Serialize>(
.to_request(), .to_request(),
) )
.await; .await;
if resp.status() != err.status_code() {
let resp_err: ErrorToResponse = test::read_body_json(resp).await;
panic!("error {}", resp_err.error);
}
assert_eq!(resp.status(), err.status_code()); assert_eq!(resp.status(), err.status_code());
let resp_err: ErrorToResponse = test::read_body_json(resp).await; let resp_err: ErrorToResponse = test::read_body_json(resp).await;
//println!("{}", txt.error); //println!("{}", txt.error);
assert_eq!(resp_err.error, format!("{}", err)); assert_eq!(resp_err.error, format!("{}", err));
} }
pub async fn add_levels_util(
data: &ArcData,
name: &str,
password: &str,
) -> (Login, ServiceResponse<EitherBody<BoxBody>>, MCaptchaDetails) {
let (creds, signin_resp) = signin(data, name, password).await;
let cookies = get_cookie!(signin_resp);
let app = get_app!(data).await;
let add_level = get_level_data();
// 1. add level
let add_token_resp = test::call_service(
&app,
post_request!(&add_level, ROUTES.captcha.create)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(add_token_resp.status(), StatusCode::OK);
let token_key: MCaptchaDetails = test::read_body_json(add_token_resp).await;
(creds, signin_resp, token_key)
}
pub const L1: Level = Level { pub const L1: Level = Level {
difficulty_factor: 50, difficulty_factor: 50,
visitor_threshold: 50, visitor_threshold: 50,
@@ -226,3 +169,27 @@ pub fn get_level_data() -> CreateCaptcha {
description: "dummy".into(), description: "dummy".into(),
} }
} }
pub async fn add_levels_util(
name: &str,
password: &str,
) -> (Arc<data::Data>, Login, ServiceResponse<EitherBody<BoxBody>>, MCaptchaDetails) {
let (data, creds, signin_resp) = signin(name, password).await;
let cookies = get_cookie!(signin_resp);
let app = get_app!(data).await;
let add_level = get_level_data();
// 1. add level
let add_token_resp = test::call_service(
&app,
post_request!(&add_level, ROUTES.captcha.create)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(add_token_resp.status(), StatusCode::OK);
let token_key: MCaptchaDetails = test::read_body_json(add_token_resp).await;
(data, creds, signin_resp, token_key)
}

View File

@@ -29,7 +29,7 @@ include!("./navbar/index.html"); .>
href="/sitekey/<.= sitekey.key .>/" href="/sitekey/<.= sitekey.key .>/"
class="sitekey-list__sitekey-link" class="sitekey-list__sitekey-link"
> >
<.= sitekey.description .> <.= sitekey.name .>
</a> </a>
</td> </td>
<td class="sitekey-list__key"> <td class="sitekey-list__key">

View File

@@ -31,7 +31,7 @@ include!("../../navbar/index.html"); .>
href="/sitekey/<.= sitekey.key .>/" href="/sitekey/<.= sitekey.key .>/"
class="sitekey-list__sitekey-link" class="sitekey-list__sitekey-link"
> >
<.= sitekey.description .> <.= sitekey.name .>
</a> </a>
</td> </td>
<td class="sitekey-list__key"> <td class="sitekey-list__key">

View File

@@ -16,7 +16,7 @@
</h3> </h3>
</td> </td>
<td> <td>
<p class="notification__item-text"><.= crate::date::Date::new(*val).date() .></p> <p class="notification__item-text"><.= val.date() .></p>
</td> </td>
</tr> </tr>
<. } .> <. } .>

View File

@@ -1,2 +0,0 @@
/target
src/cache_buster_data.json

View File

@@ -1,354 +0,0 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "block-buffer"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324"
dependencies = [
"generic-array",
]
[[package]]
name = "cache-bust"
version = "0.1.0"
dependencies = [
"cache-buster",
"serde",
"serde_json",
]
[[package]]
name = "cache-buster"
version = "0.2.0"
source = "git+https://github.com/realaravinth/cache-buster#7ca4545722fb99be30698a5e72c7d982a70fa11f"
dependencies = [
"data-encoding",
"derive_builder",
"mime",
"mime_guess",
"serde",
"serde_json",
"sha2",
"walkdir",
]
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "cpufeatures"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"
dependencies = [
"libc",
]
[[package]]
name = "crypto-common"
version = "0.1.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8"
dependencies = [
"generic-array",
"typenum",
]
[[package]]
name = "darling"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4529658bdda7fd6769b8614be250cdcfc3aeb0ee72fe66f9e41e5e5eb73eac02"
dependencies = [
"darling_core",
"darling_macro",
]
[[package]]
name = "darling_core"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "649c91bc01e8b1eac09fb91e8dbc7d517684ca6be8ebc75bb9cafc894f9fdb6f"
dependencies = [
"fnv",
"ident_case",
"proc-macro2",
"quote",
"strsim",
"syn",
]
[[package]]
name = "darling_macro"
version = "0.14.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ddfc69c5bfcbd2fc09a0f38451d2daf0e372e367986a83906d1b0dbc88134fb5"
dependencies = [
"darling_core",
"quote",
"syn",
]
[[package]]
name = "data-encoding"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57"
[[package]]
name = "derive_builder"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d07adf7be193b71cc36b193d0f5fe60b918a3a9db4dad0449f57bcfd519704a3"
dependencies = [
"derive_builder_macro",
]
[[package]]
name = "derive_builder_core"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f91d4cfa921f1c05904dc3c57b4a32c38aed3340cce209f3a6fd1478babafc4"
dependencies = [
"darling",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "derive_builder_macro"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f0314b72bed045f3a68671b3c86328386762c93f82d98c65c3cb5e5f573dd68"
dependencies = [
"derive_builder_core",
"syn",
]
[[package]]
name = "digest"
version = "0.10.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
dependencies = [
"block-buffer",
"crypto-common",
]
[[package]]
name = "fnv"
version = "1.0.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
[[package]]
name = "generic-array"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"
dependencies = [
"typenum",
"version_check",
]
[[package]]
name = "ident_case"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
[[package]]
name = "itoa"
version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
[[package]]
name = "libc"
version = "0.2.125"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b"
[[package]]
name = "mime"
version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
[[package]]
name = "mime_guess"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
dependencies = [
"mime",
"unicase",
]
[[package]]
name = "proc-macro2"
version = "1.0.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9027b48e9d4c9175fa2218adf3557f91c1137021739951d4932f5f8268ac48aa"
dependencies = [
"unicode-xid",
]
[[package]]
name = "quote"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1"
dependencies = [
"proc-macro2",
]
[[package]]
name = "ryu"
version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
[[package]]
name = "same-file"
version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
dependencies = [
"winapi-util",
]
[[package]]
name = "serde"
version = "1.0.137"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.137"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.81"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "sha2"
version = "0.10.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676"
dependencies = [
"cfg-if",
"cpufeatures",
"digest",
]
[[package]]
name = "strsim"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
[[package]]
name = "syn"
version = "1.0.94"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a07e33e919ebcd69113d5be0e4d70c5707004ff45188910106854f38b960df4a"
dependencies = [
"proc-macro2",
"quote",
"unicode-xid",
]
[[package]]
name = "typenum"
version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
[[package]]
name = "unicase"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
dependencies = [
"version_check",
]
[[package]]
name = "unicode-xid"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04"
[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "walkdir"
version = "2.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
dependencies = [
"same-file",
"winapi",
"winapi-util",
]
[[package]]
name = "winapi"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
dependencies = [
"winapi-i686-pc-windows-gnu",
"winapi-x86_64-pc-windows-gnu",
]
[[package]]
name = "winapi-i686-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
dependencies = [
"winapi",
]
[[package]]
name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"

View File

@@ -1,17 +0,0 @@
[package]
name = "cache-bust"
version = "0.1.0"
edition = "2021"
description = "mCaptcha - a PoW-based CAPTCHA system"
homepage = "https://mcaptcha.org"
repository = "https://github.com/mCaptcha/mCaptcha"
documentation = "https://mcaptcha.org/docs/"
license = "AGPLv3 or later version"
authors = ["realaravinth <realaravinth@batsense.net>"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
cache-buster = { version = "0.2.0", git = "https://github.com/realaravinth/cache-buster" }
serde = { version = "1", features = ["derive"] }
serde_json = "1"

View File

@@ -1,78 +0,0 @@
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::fs;
use std::path::Path;
use std::collections::HashMap;
use cache_buster::{BusterBuilder, CACHE_BUSTER_DATA_FILE, NoHashCategory};
use serde::{Serialize, Deserialize};
#[derive(Deserialize, Serialize)]
struct FileMap {
map: HashMap<String, String>,
base_dir: String,
}
fn main() {
cache_bust();
process_file_map();
}
fn cache_bust() {
// until APPLICATION_WASM gets added to mime crate
// PR: https://github.com/hyperium/mime/pull/138
// let types = vec![
// mime::IMAGE_PNG,
// mime::IMAGE_SVG,
// mime::IMAGE_JPEG,
// mime::IMAGE_GIF,
// mime::APPLICATION_JAVASCRIPT,
// mime::TEXT_CSS,
// ];
println!("[*] Cache busting");
let no_hash = vec![NoHashCategory::FileExtentions(vec!["wasm"])];
let config = BusterBuilder::default()
.source("../../static/cache/")
.result("./../../assets")
.no_hash(no_hash)
.follow_links(true)
.build()
.unwrap();
config.process().unwrap();
}
fn process_file_map() {
let contents = fs::read_to_string(CACHE_BUSTER_DATA_FILE).unwrap();
let files: FileMap = serde_json::from_str(&contents).unwrap();
let mut map = HashMap::with_capacity(files.map.len());
for (k, v) in files.map.iter() {
map.insert(k.strip_prefix("../.").unwrap().to_owned(),
v.strip_prefix("./../.").unwrap().to_owned()
);
}
let new_filemap = FileMap{
map,
base_dir: files.base_dir.strip_prefix("./../.").unwrap().to_owned(),
};
let dest = Path::new("../../").join(CACHE_BUSTER_DATA_FILE);
fs::write(&dest, serde_json::to_string(&new_filemap).unwrap()).unwrap();
}