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
113 changed files with 3258 additions and 5066 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"

View File

@@ -2,18 +2,4 @@
### Changed ### Changed
- ([`7d0e4c6`](https://github.com/mCaptcha/mCaptcha/commit/7d0e4c6be4b0769921cda7681858ebe16ec9a07b)) Add `secret` parameter to token verification request payload(`/api/v1/pow/siteverify`) to mitigate a security issue that @gusted found: - Rename pow section in settings to captcha and add options to configure([`42544ec42`](https://github.com/mCaptcha/mCaptcha/commit/42544ec421e0c3ec4a8d132e6101ab4069bf0065))
> ...A malicious user could grab the sitekey
> and use that sitekey with mcaptcha to use it for their own server.
> While they can now go abuse it for illegal stuff or other stuff.
> You might decide, oh I don't want this! and terminate a legitimate
> siteKey.
> New request payload:
```json
{
"secret": "<your-users-secret>", // found in /settings in the dashbaord
"token": "<token-presented-by-the-user>",
"key": "<your-sitekey>"
}
```
- ([`42544ec42`](https://github.com/mCaptcha/mCaptcha/commit/42544ec421e0c3ec4a8d132e6101ab4069bf0065)) Rename pow section in settings to captcha and add options to configure

471
Cargo.lock generated

File diff suppressed because it is too large Load Diff

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

@@ -17,32 +17,23 @@ COPY Makefile /src/
COPY scripts /src/scripts COPY scripts /src/scripts
RUN make frontend RUN make frontend
FROM rust:latest as planner
RUN cargo install cargo-chef
WORKDIR /src
COPY . /src/
RUN cargo chef prepare --recipe-path recipe.json
FROM rust:latest as cacher
WORKDIR /src/
RUN cargo install cargo-chef
COPY --from=planner /src/recipe.json recipe.json
RUN cargo chef cook --release --recipe-path recipe.json
FROM rust:latest as rust FROM rust:latest as rust
WORKDIR /src WORKDIR /src
COPY . . RUN mkdir src && echo "fn main() {}" > src/main.rs
COPY --from=cacher /src/target target COPY Cargo.toml .
#COPY --from=cacher /src/db/db-core/target /src/db/db-core/target RUN sed -i '/.*build.rs.*/d' Cargo.toml
#COPY --from=cacher /src/db/db-sqlx-postgres/target /src/db/db-sqlx-postgres/target COPY Cargo.lock .
#COPY --from=cacher /src/db/db-migrations/target /src/db/db-migrations/target COPY migrations /src/migrations
#COPY --from=cacher /src/utils/cache-bust/target /src/utils/cache-bust/target COPY sqlx-data.json /src/
COPY --from=frontend /src/static/cache/bundle/ /src/static/cache/bundle/ COPY src/tests-migrate.rs /src/src/tests-migrate.rs
COPY src/settings.rs /src/src/settings.rs
RUN cargo --version RUN cargo --version
RUN make cache-bust RUN cargo build --release
COPY . /src
COPY --from=frontend /src/static/cache/bundle/ /src/static/cache/bundle/
RUN cargo build --release RUN cargo build --release
FROM debian:bullseye as mCaptcha FROM debian:bullseye
LABEL org.opencontainers.image.source https://github.com/mCaptcha/mCaptcha LABEL org.opencontainers.image.source https://github.com/mCaptcha/mCaptcha
RUN useradd -ms /bin/bash -u 1001 mcaptcha RUN useradd -ms /bin/bash -u 1001 mcaptcha
WORKDIR /home/mcaptcha WORKDIR /home/mcaptcha

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,19 +103,12 @@ 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 up -d
``` ```
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/DEPLOYMENT.md) detailed alternate deployment See [DEPLOYMENT.md](./docs/DEPLOYMET.md) detailed alternate deployment
methods. methods.
## Development: ## Development:
@@ -124,7 +117,7 @@ See [HACKING.md](./docs/HACKING.md)
## Deployment: ## Deployment:
See [DEPLOYMENT.md](./docs/DEPLOYMENT.md) See [DEPLOYMENT.md](./docs/DEPLOYMET.md)
## Configuration: ## Configuration:

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,355 +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>;
/// get a user's secret from a captcha key
async fn get_secret_from_captcha(&self, key: &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,288 +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 secret from captcha key
let secret_from_captcha = db.get_secret_from_captcha(&c.key).await.unwrap();
assert_eq!(secret_from_captcha.secret, p.secret, "user secret matches");
// 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,971 +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 std::str::FromStr;
use db_core::dev::*;
use sqlx::postgres::PgPoolOptions;
use sqlx::types::time::OffsetDateTime;
use sqlx::ConnectOptions;
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 disable_logging: bool,
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) => {
let mut connect_options =
sqlx::postgres::PgConnectOptions::from_str(&fresh.url).unwrap();
if fresh.disable_logging {
connect_options.disable_statement_logging();
}
sqlx::postgres::PgConnectOptions::from_str(&fresh.url)
.unwrap()
.disable_statement_logging();
fresh
.pool_options
.connect_with(connect_options)
.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)
}
/// get a user's secret from a captcha key
async fn get_secret_from_captcha(&self, key: &str) -> DBResult<Secret> {
let secret = sqlx::query_as!(
Secret,
r#"SELECT secret FROM mcaptcha_users WHERE ID = (
SELECT user_id FROM mcaptcha_config WHERE key = $1
)"#,
key,
)
.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,92 +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,
disable_logging: false,
});
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

@@ -1,4 +1,4 @@
version: "3.9" version: '3.9'
services: services:
mcaptcha: mcaptcha:
@@ -6,15 +6,11 @@ services:
ports: ports:
- 7000:7000 - 7000:7000
environment: environment:
DATABASE_URL: postgres://postgres:password@mcaptcha_postgres:5432/postgres # set password at placeholder DATABASE_URL: postgres://postgres:password@postgres:5432/postgres # set password at placeholder
MCAPTCHA_REDIS_URL: redis://mcaptcha-redis/ MCAPTCHA_REDIS_URL: redis://mcaptcha-redis/
RUST_LOG: debug RUST_LOG: debug
PORT: 7000
depends_on:
- mcaptcha-postgres
- mcaptcha-redis
mcaptcha_postgres: postgres:
image: postgres:13.2 image: postgres:13.2
volumes: volumes:
- mcaptcha-data:/var/lib/postgresql/ - mcaptcha-data:/var/lib/postgresql/

View File

@@ -34,14 +34,14 @@ docker run -p <host-machine-port>:<port-in-configuration-file> \
If you don't have a Postgres instance running, you can either install If you don't have a Postgres instance running, you can either install
one using a package manager or launch one with docker. A [docker-compose one using a package manager or launch one with docker. A [docker-compose
configuration](../docker-compose.yml) is available that will launch both configuration]('../docker-compose.yml) is available that will launch both
a database instance mcaptcha instance. a database instance mcaptcha instance.
## With docker-compose ## With docker-compose
1. Follow steps above to build docker image. 1. Follow steps above to build docker image.
2. Set database password [docker-compose configuration](../docker-compose.yml). 2. Set database password [docker-compose configuration]('../docker-compose.yml).
3. Launch network: 3. Launch network:

View File

@@ -60,7 +60,7 @@ refer to [official instructions](https://www.gnu.org/software/make/)
### External Dependencies: ### External Dependencies:
### Postgres database: ### Postgres databse:
The backend requires a Postgres database. We have The backend requires a Postgres database. We have
compiletime SQL checks so without a database available, you won't be compiletime SQL checks so without a database available, you won't be
@@ -125,7 +125,7 @@ $ make
default Run app in debug mode default Run app in debug mode
clean Delete build artifacts clean Delete build artifacts
coverage Generate code coverage report in HTML format coverage Generate code coverage report in HTML format
dev-env Setup development environment dev-env Setup development environtment
doc Generate documentation doc Generate documentation
docker Build Docker image docker Build Docker image
docker-publish Build and publish Docker image docker-publish Build and publish Docker image

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

@@ -1,18 +1,18 @@
/* /*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net> * Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the * published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version. * License, or (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* 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/>.
*/ */
pub mod v1; pub mod v1;

View File

@@ -1,19 +1,19 @@
/* /*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net> * Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the * published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version. * License, or (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* 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_identity::Identity; use actix_identity::Identity;
use actix_web::{web, HttpResponse, Responder}; use actix_web::{web, HttpResponse, Responder};
@@ -32,20 +32,30 @@ 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 {
runners::delete_user(&username, &data).await?; Ok(s) => {
id.forget(); if Config::verify(&s.password, &payload.password)? {
Ok(HttpResponse::Ok()) runners::delete_user(&username, &data).await?;
} else { id.forget();
Err(ServiceError::WrongPassword) Ok(HttpResponse::Ok())
} else {
Err(ServiceError::WrongPassword)
}
}
Err(RowNotFound) => Err(ServiceError::AccountNotFound),
Err(_) => Err(ServiceError::InternalServerError),
} }
} }
@@ -54,7 +64,9 @@ 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,
data.db.update_email(&update_email).await?; )
.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());
}
};
}
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,15 +83,26 @@ 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 {
let update: UpdatePassword = payload.into_inner().into(); Ok(s) => {
update_password_runner(&username, update, &data).await?; if Config::verify(&s.password, &payload.password)? {
Ok(HttpResponse::Ok()) let update: UpdatePassword = payload.into_inner().into();
} else { update_password_runner(&username, update, &data).await?;
Err(ServiceError::WrongPassword) Ok(HttpResponse::Ok())
} else {
Err(ServiceError::WrongPassword)
}
}
Err(RowNotFound) => Err(ServiceError::AccountNotFound),
Err(_) => Err(ServiceError::InternalServerError),
} }
} }
@@ -96,27 +111,28 @@ pub fn services(cfg: &mut actix_web::web::ServiceConfig) {
} }
#[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

@@ -1,34 +1,49 @@
/* /*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net> * Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the * published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version. * License, or (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* 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 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

@@ -1,19 +1,19 @@
/* /*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net> * Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the * published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version. * License, or (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* 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::http::StatusCode; use actix_web::http::StatusCode;
use actix_web::test; use actix_web::test;
@@ -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)?; let email_fut = sqlx::query_as!(
Ok(s.username) 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,
match data.db.register(&p).await { &secret,
Ok(_) => break, )
Err(DBError::SecretTaken) => continue, .execute(&data.db)
Err(e) => return Err(e.into()), .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());
}
};
} }
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()?;
let mut key; debug!("creating config");
let duration = payload.duration as i32; let mcaptcha_config =
loop { // add_mcaptcha_util(payload.duration, &payload.description, &data, username).await?;
key = get_random(32);
let p = DBCreateCaptcha {
description: &payload.description,
key: &key,
duration,
};
match data.db.create_captcha(username, &p).await { {
Ok(_) => break, let mut key;
Err(DBError::SecretTaken) => continue,
Err(e) => return Err(e.into()), let resp;
loop {
key = get_random(32);
let res = sqlx::query!(
"INSERT INTO mcaptcha_config
(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;
} }
} }
data.db }
.add_captcha_levels(username, &key, &payload.levels) resp
.await?; };
let mcaptcha_config = MCaptchaDetails {
name: payload.description.clone(), debug!("config created");
key,
}; let mut futs = Vec::with_capacity(payload.levels.len());
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);
}
try_join_all(futs).await?;
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?;
if let Err(err) = data.captcha.remove(RemoveCaptcha(payload.key)).await { let rec = sqlx::query_as!(
log::error!("Error while trying to remove captcha from cache {}", err); 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 {
log::error!(
"Error while trying to remove captcha from cache {}",
err
);
}
Ok(HttpResponse::Ok())
} else {
Err(ServiceError::WrongPassword)
}
}
Err(RowNotFound) => Err(ServiceError::UsernameNotFound),
Err(_) => Err(ServiceError::InternalServerError),
} }
Ok(HttpResponse::Ok())
} }

View File

@@ -1,26 +1,24 @@
/* /*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net> * Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the * published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version. * License, or (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* 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_identity::Identity; use actix_identity::Identity;
use actix_web::{web, HttpResponse, Responder}; 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,96 +47,103 @@ 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 { pub fn calculate(
TrafficPattern { &self,
avg_traffic: t.avg_traffic, strategy: &DefaultDifficultyStrategy,
peak_sustainable_traffic: t.peak_sustainable_traffic, ) -> ServiceResult<Vec<Level>> {
broke_my_site_traffic: t.broke_my_site_traffic, let mut levels = vec![
} LevelBuilder::default()
.difficulty_factor(strategy.avg_traffic_difficulty)?
.visitor_threshold(self.avg_traffic)
.build()?,
LevelBuilder::default()
.difficulty_factor(strategy.peak_sustainable_traffic_difficulty)?
.visitor_threshold(self.peak_sustainable_traffic)
.build()?,
];
let mut highest_level = LevelBuilder::default();
highest_level.difficulty_factor(strategy.broke_my_site_traffic_difficulty)?;
match self.broke_my_site_traffic {
Some(broke_my_site_traffic) => {
highest_level.visitor_threshold(broke_my_site_traffic)
}
None => match self
.peak_sustainable_traffic
.checked_add(self.peak_sustainable_traffic / 2)
{
Some(num) => highest_level.visitor_threshold(num),
// TODO check for overflow: database saves these values as i32, so this u32 is cast
// into i32. Should choose bigger number or casts properly
None => highest_level.visitor_threshold(u32::MAX),
},
};
levels.push(highest_level.build()?);
Ok(levels)
} }
} }
pub fn calculate(
tp: &TrafficPattern,
strategy: &DefaultDifficultyStrategy,
) -> ServiceResult<Vec<Level>> {
let mut levels = vec![
LevelBuilder::default()
.difficulty_factor(strategy.avg_traffic_difficulty)?
.visitor_threshold(tp.avg_traffic)
.build()?,
LevelBuilder::default()
.difficulty_factor(strategy.peak_sustainable_traffic_difficulty)?
.visitor_threshold(tp.peak_sustainable_traffic)
.build()?,
];
let mut highest_level = LevelBuilder::default();
highest_level.difficulty_factor(strategy.broke_my_site_traffic_difficulty)?;
match tp.broke_my_site_traffic {
Some(broke_my_site_traffic) => {
highest_level.visitor_threshold(broke_my_site_traffic)
}
None => match tp
.peak_sustainable_traffic
.checked_add(tp.peak_sustainable_traffic / 2)
{
Some(num) => highest_level.visitor_threshold(num),
// TODO check for overflow: database saves these values as i32, so this u32 is cast
// into i32. Should choose bigger number or casts properly
None => highest_level.visitor_threshold(u32::MAX),
},
};
levels.push(highest_level.build()?);
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 (
.await?; 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?;
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)
.await?; 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?;
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,39 +296,38 @@ 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,18 +355,16 @@ 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 {
pattern: update_pattern, pattern: update_pattern,

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

@@ -1,24 +1,25 @@
/* /*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net> * Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the * published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version. * License, or (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* 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_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};
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!(
.await?; "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?;
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

@@ -1,19 +1,19 @@
/* /*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net> * Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the * published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version. * License, or (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* 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 derive_builder::Builder; use derive_builder::Builder;
@@ -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

@@ -1,19 +1,19 @@
/* /*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net> * Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the * published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version. * License, or (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* 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;

View File

@@ -1,19 +1,19 @@
/* /*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net> * Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the * published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version. * License, or (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* 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_identity::Identity; use actix_identity::Identity;
use actix_web::{web, HttpResponse, Responder}; use actix_web::{web, HttpResponse, Responder};
@@ -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

@@ -1,30 +1,37 @@
/* /*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net> * Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the * published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version. * License, or (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* 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_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

@@ -1,19 +1,19 @@
/* /*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net> * Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the * published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version. * License, or (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* 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_identity::Identity; use actix_identity::Identity;
use actix_web::{web, HttpResponse, Responder}; use actix_web::{web, HttpResponse, Responder};
@@ -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",
.await?; payload.id,
&receiver
)
.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;
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

@@ -1,19 +1,19 @@
/* /*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net> * Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the * published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version. * License, or (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* 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/>.
*/ */
pub mod add; pub mod add;
pub mod get; pub mod get;

View File

@@ -1,19 +1,19 @@
/* /*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net> * Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the * published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version. * License, or (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* 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::prelude::*; //use actix::prelude::*;
use actix_web::{web, HttpResponse, Responder}; use actix_web::{web, HttpResponse, Responder};
@@ -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,68 +42,73 @@ 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 {
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()),
}
}
match data.captcha.get_pow(payload.key.clone()).await { Some(false) => Err(ServiceError::TokenNotFound),
Ok(Some(config)) => { None => Err(ServiceError::TokenNotFound),
data.stats.record_fetch(&data, &payload.key).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
data.stats.record_fetch(&data, &payload.key).await?;
Ok(HttpResponse::Ok().json(config))
}
Err(e) => Err(e.into()),
} }
// match res.exists {
// Some(true) => {
// 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.
/// ///
/// This fn gets mcaptcha config from database, builds [Defense][libmcaptcha::Defense], /// This fn gets mcaptcha config from database, builds [Defense][libmcaptcha::Defense],
/// creates [MCaptcha][libmcaptcha::MCaptcha] and adds it to [Master][libmcaptcha::Defense] /// creates [MCaptcha][libmcaptcha::MCaptcha] and adds it to [Master][libmcaptcha::Defense]
pub async fn init_mcaptcha(data: &AppData, key: &str) -> ServiceResult<()> { async fn init_mcaptcha(data: &AppData, key: &str) -> ServiceResult<()> {
println!("Initializing captcha");
// 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();
@@ -118,13 +124,12 @@ pub async fn init_mcaptcha(data: &AppData, key: &str) -> ServiceResult<()> {
} }
let defense = defense.build()?; let defense = defense.build()?;
println!("{:?}", defense);
// create captcha // create captcha
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();
@@ -142,12 +147,11 @@ pub 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::*;
@@ -157,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 {
@@ -184,98 +188,4 @@ pub mod tests {
let config: PoWConfig = test::read_body_json(get_config_resp).await; let config: PoWConfig = test::read_body_json(get_config_resp).await;
assert_eq!(config.difficulty_factor, L1.difficulty_factor); assert_eq!(config.difficulty_factor, L1.difficulty_factor);
} }
#[actix_rt::test]
pub async fn pow_difficulty_factor_increases_on_visitor_count_increase() {
use super::*;
use crate::tests::*;
use crate::*;
use actix_web::test;
use libmcaptcha::defense::Level;
use crate::api::v1::mcaptcha::create::CreateCaptcha;
use crate::api::v1::mcaptcha::create::MCaptchaDetails;
const NAME: &str = "powusrworks2";
const PASSWORD: &str = "testingpas";
const EMAIL: &str = "randomuser2@a.com";
pub const L1: Level = Level {
difficulty_factor: 10,
visitor_threshold: 10,
};
pub const L2: Level = Level {
difficulty_factor: 20,
visitor_threshold: 20,
};
pub const L3: Level = Level {
difficulty_factor: 30,
visitor_threshold: 30,
};
let data = get_data().await;
let data = &data;
let levels = [L1, L2, L3];
delete_user(data, NAME).await;
let (_, signin_resp) = register_and_signin(data, NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
let app = get_app!(data).await;
let create_captcha = CreateCaptcha {
levels: levels.into(),
duration: 30,
description: "dummy".into(),
};
// 1. add level
let add_token_resp = test::call_service(
&app,
post_request!(&create_captcha, V1_API_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;
let get_config_payload = GetConfigPayload {
key: token_key.key.clone(),
};
let url = V1_API_ROUTES.pow.get_config;
let mut prev = 0;
for (count, l) in levels.iter().enumerate() {
for l in prev..l.visitor_threshold * 2 {
let get_config_resp = test::call_service(
&app,
post_request!(&get_config_payload, V1_API_ROUTES.pow.get_config)
.to_request(),
)
.await;
}
let get_config_resp = test::call_service(
&app,
post_request!(&get_config_payload, V1_API_ROUTES.pow.get_config)
.to_request(),
)
.await;
let config: PoWConfig = test::read_body_json(get_config_resp).await;
println!(
"[{count}] received difficulty_factor: {} prev difficulty_factor {}",
config.difficulty_factor, prev
);
if count == levels.len() - 1 {
assert!(config.difficulty_factor == prev);
} else {
assert!(config.difficulty_factor > prev);
}
prev = config.difficulty_factor;
}
// update and check changes
}
} }

View File

@@ -1,19 +1,19 @@
/* /*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net> * Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the * published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version. * License, or (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* 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; use actix_web::web;

View File

@@ -1,19 +1,19 @@
/* /*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net> * Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the * published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version. * License, or (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* 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/>.
*/ */
//! PoW Verification module //! PoW Verification module
use actix_web::{web, HttpResponse, Responder}; use actix_web::{web, HttpResponse, Responder};
@@ -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

@@ -1,19 +1,19 @@
/* /*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net> * Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the * published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version. * License, or (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* 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/>.
*/ */
//! PoW success token module //! PoW success token module
use actix_web::{web, HttpResponse, Responder}; use actix_web::{web, HttpResponse, Responder};
@@ -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;
@@ -29,45 +30,27 @@ pub struct CaptchaValidateResp {
pub valid: bool, pub valid: bool,
} }
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct VerifyCaptchaResultPayload {
pub secret: String,
pub key: String,
pub token: String,
}
impl From<VerifyCaptchaResultPayload> for VerifyCaptchaResult {
fn from(m: VerifyCaptchaResultPayload) -> Self {
VerifyCaptchaResult {
token: m.token,
key: m.key,
}
}
}
// API keys are mcaptcha actor names // API keys are mcaptcha actor names
/// route hander that validates a PoW solution token /// route hander that validates a PoW solution token
#[my_codegen::post(path = "V1_API_ROUTES.pow.validate_captcha_token()")] #[my_codegen::post(path = "V1_API_ROUTES.pow.validate_captcha_token()")]
pub async fn validate_captcha_token( pub async fn validate_captcha_token(
payload: web::Json<VerifyCaptchaResultPayload>, payload: web::Json<VerifyCaptchaResult>,
data: AppData, data: AppData,
) -> ServiceResult<impl Responder> { ) -> ServiceResult<impl Responder> {
let secret = data.db.get_secret_from_captcha(&payload.key).await?;
if secret.secret != payload.secret {
return Err(ServiceError::WrongPassword);
}
let payload: VerifyCaptchaResult = payload.into_inner().into();
let key = payload.key.clone(); let key = payload.key.clone();
let res = data.captcha.validate_verification_tokens(payload).await?; let res = data
let resp = CaptchaValidateResp { valid: res }; .captcha
data.stats.record_confirm(&data, &key).await?; .validate_verification_tokens(payload.into_inner())
.await?;
let payload = CaptchaValidateResp { valid: res };
record_confirm(&key, &data.db).await;
//println!("{:?}", &payload); //println!("{:?}", &payload);
Ok(HttpResponse::Ok().json(resp)) 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;
@@ -80,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";
@@ -89,26 +72,14 @@ 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 cookies = get_cookie!(signin_resp);
let secret = test::call_service(
&app,
test::TestRequest::get()
.cookie(cookies.clone())
.uri(V1_API_ROUTES.account.get_secret)
.to_request(),
)
.await;
assert_eq!(secret.status(), StatusCode::OK);
let secret: db_core::Secret = test::read_body_json(secret).await;
let secret = secret.secret;
let get_config_payload = GetConfigPayload { let get_config_payload = GetConfigPayload {
key: token_key.key.clone(), key: token_key.key.clone(),
@@ -147,35 +118,11 @@ pub mod tests {
assert_eq!(pow_verify_resp.status(), StatusCode::OK); assert_eq!(pow_verify_resp.status(), StatusCode::OK);
let client_token: ValidationToken = test::read_body_json(pow_verify_resp).await; let client_token: ValidationToken = test::read_body_json(pow_verify_resp).await;
let mut validate_payload = VerifyCaptchaResultPayload { let validate_payload = VerifyCaptchaResult {
token: client_token.token.clone(), token: client_token.token.clone(),
key: token_key.key.clone(), key: token_key.key.clone(),
secret: NAME.to_string(),
}; };
// siteverify authentication failure
bad_post_req_test(
data,
NAME,
PASSWORD,
VERIFY_TOKEN_URL,
&validate_payload,
ServiceError::WrongPassword,
)
.await;
// let validate_client_token = test::call_service(
// &app,
// post_request!(&validate_payload, VERIFY_TOKEN_URL).to_request(),
// )
// .await;
// assert_eq!(validate_client_token.status(), StatusCode::OK);
// let resp: CaptchaValidateResp =
// test::read_body_json(validate_client_token).await;
// assert!(resp.valid);
// verifying work
validate_payload.secret = secret.clone();
let validate_client_token = test::call_service( let validate_client_token = test::call_service(
&app, &app,
post_request!(&validate_payload, VERIFY_TOKEN_URL).to_request(), post_request!(&validate_payload, VERIFY_TOKEN_URL).to_request(),
@@ -194,5 +141,19 @@ pub mod tests {
.await; .await;
let resp: CaptchaValidateResp = test::read_body_json(string_not_found).await; let resp: CaptchaValidateResp = test::read_body_json(string_not_found).await;
assert!(!resp.valid); assert!(!resp.valid);
let validate_payload = VerifyCaptchaResult {
token: client_token.token.clone(),
key: client_token.token.clone(),
};
// key not found
let key_not_found = test::call_service(
&app,
post_request!(&validate_payload, VERIFY_TOKEN_URL).to_request(),
)
.await;
let resp: CaptchaValidateResp = test::read_body_json(key_not_found).await;
assert!(!resp.valid);
} }
} }

View File

@@ -1,19 +1,19 @@
/* /*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net> * Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the * published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version. * License, or (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* 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::GetLoginRoute; use actix_auth_middleware::GetLoginRoute;
use super::account::routes::Account; use super::account::routes::Account;

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

@@ -1,19 +1,19 @@
/* /*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net> * Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the * published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version. * License, or (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* 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/>.
*/ */
mod auth; mod auth;
mod protected; mod protected;

View File

@@ -1,23 +1,24 @@
/* /*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net> * Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the * published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version. * License, or (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* 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::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,29 +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");
disable_logging: !s.debug,
});
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))]
@@ -216,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

@@ -1,18 +1,18 @@
/* /*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net> * Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the * published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version. * License, or (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* 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/>.
*/ */
pub mod verification; pub mod verification;

View File

@@ -1,19 +1,19 @@
/* /*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net> * Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the * published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version. * License, or (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* 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/>.
*/ */
//! Email operations: verification, notification, etc //! Email operations: verification, notification, etc
use lettre::{ use lettre::{
message::{header, MultiPart, SinglePart}, message::{header, MultiPart, SinglePart},
@@ -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

@@ -1,19 +1,19 @@
/* /*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net> * Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the * published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version. * License, or (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* 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 std::convert::From; use std::convert::From;
@@ -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

@@ -1,25 +1,26 @@
/* /*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net> * Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the * published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version. * License, or (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* 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::{HttpResponse, Responder}; use actix_web::{HttpResponse, Responder};
use lazy_static::lazy_static; 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

@@ -1,19 +1,19 @@
/* /*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net> * Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the * published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version. * License, or (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* 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/>.
*/ */
pub mod login; pub mod login;
pub mod register; pub mod register;

View File

@@ -1,19 +1,19 @@
/* /*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net> * Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the * published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version. * License, or (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* 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::{HttpResponse, Responder}; use actix_web::{HttpResponse, Responder};
use lazy_static::lazy_static; use lazy_static::lazy_static;

View File

@@ -1,19 +1,17 @@
/* /*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net> * Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the * published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version. * License, or (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* 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

@@ -1,19 +1,19 @@
/* /*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net> * Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the * published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version. * License, or (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* 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::{HttpResponse, Responder}; use actix_web::{HttpResponse, Responder};
use lazy_static::lazy_static; use lazy_static::lazy_static;

View File

@@ -1,19 +1,19 @@
/* /*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net> * Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the * published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version. * License, or (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* 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 my_codegen::get; use my_codegen::get;

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

@@ -1,19 +1,19 @@
/* /*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net> * Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the * published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version. * License, or (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* 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::{HttpResponse, Responder}; use actix_web::{HttpResponse, Responder};
use lazy_static::lazy_static; use lazy_static::lazy_static;

View File

@@ -1,19 +1,19 @@
/* /*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net> * Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the * published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version. * License, or (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* 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/>.
*/ */
mod add; mod add;
mod delete; mod delete;

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

@@ -1,19 +1,19 @@
/* /*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net> * Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
* *
* This program is free software: you can redistribute it and/or modify * This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as * it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the * published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version. * License, or (at your option) any later version.
* *
* This program is distributed in the hope that it will be useful, * This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of * but WITHOUT ANY WARRANTY; without even the implied warranty of
* 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::GetLoginRoute; use actix_auth_middleware::GetLoginRoute;
use super::auth::routes::Auth; use super::auth::routes::Auth;

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, )
crate::FILES .await;
.get("./static/cache/img/icon-trans.png") assert_eq!(resp.status(), StatusCode::OK);
.unwrap(),
"/favicon.ico",
];
for u in urls.iter() { let resp = test::call_service(
println!("[*] Testing static asset at URL: {u}"); &app,
let resp = test::TestRequest::get()
test::call_service(&app, test::TestRequest::get().uri(u).to_request()) .uri(*crate::VERIFICATIN_WIDGET_JS)
.await; .to_request(),
assert_eq!(resp.status(), StatusCode::OK); )
} .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
.get("./static/cache/img/icon-trans.png")
.unwrap(),
)
.to_request(),
)
.await;
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn favicons_work() {
assert!(Favicons::get("favicon.ico").is_some());
//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;
assert_eq!(resp.status(), StatusCode::OK);
} }
} }

Some files were not shown because too many files have changed in this diff Show More