Compare commits

..

2 Commits

Author SHA1 Message Date
realaravinth
cc1b48db27 cache buster version lock in 2021-04-13 22:59:39 +05:30
realaravinth
de6ceb1b3f broken route prefix 2021-04-12 19:03:55 +05:30
485 changed files with 10060 additions and 29288 deletions

View File

@@ -1,14 +0,0 @@
/target
tarpaulin-report.html
.env
cobertura.xml
prod/
node_modules/
/static-assets/bundle
./templates/**/*.js
/static/cache/bundle/*
src/cache_buster_data.json
browser/target
browser/cobertura.xml
browser/docs

View File

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

View File

@@ -1,21 +0,0 @@
module.exports = {
env: {
browser: true,
es2021: true,
},
extends: ["eslint:recommended", "plugin:@typescript-eslint/recommended"],
parser: "@typescript-eslint/parser",
parserOptions: {
ecmaVersion: 12,
sourceType: "module",
},
plugins: ["@typescript-eslint"],
rules: {
"@typescript-eslint/no-explicit-any": "off",
"@typescript-eslint/ban-types": "off",
indent: ["error", 2],
"linebreak-style": ["error", "unix"],
quotes: ["error", "double"],
semi: ["error", "always"],
},
};

12
.github/FUNDING.yml vendored
View File

@@ -1,12 +0,0 @@
# These are supported funding model platforms
# github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
# patreon: # Replace with a single Patreon username
open_collective: # Replace with a single Open Collective username
# ko_fi: # Replace with a single Ko-fi username
# tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel
# community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry
liberapay: mcaptcha
issuehunt: # Replace with a single IssueHunt username
# otechie: # Replace with a single Otechie username
custom: ['https://mcaptcha.org/donate']

View File

@@ -1,75 +0,0 @@
name: Lint
on:
pull_request:
types: [opened, synchronize, reopened]
push:
branches:
- master
- db-abstract
jobs:
fmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: ⚡ Cache
uses: actions/cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
node_modules
./docs/openapi/node_modules
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- uses: actions/checkout@v2
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
components: rustfmt
- name: Check with rustfmt
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: ⚡ Cache
uses: actions/cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
node_modules
./docs/openapi/node_modules
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- uses: actions/checkout@v2
- name: Install Rust
uses: actions-rs/toolchain@v1
with:
toolchain: stable
components: clippy
override: true
- uses: actions/setup-node@v2
with:
node-version: "14.x"
- name: Build frontend
run: make frontend
- name: Check with Clippy
uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --workspace --tests --all-features

View File

@@ -1,106 +0,0 @@
name: Coverage
on:
pull_request:
types: [opened, synchronize, reopened]
push:
branches:
- master
- db-abstract
jobs:
build_and_test:
strategy:
fail-fast: false
matrix:
version:
- stable
#- 1.51.0
name: ${{ matrix.version }} - x86_64-unknown-linux-gnu
runs-on: ubuntu-latest
services:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: password
POSTGRES_USER: postgres
POSTGRES_DB: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
mcaptcha-redis:
image: mcaptcha/cache
ports:
- 6379:6379
steps:
- uses: actions/checkout@v2
- name: ⚡ Cache
uses: actions/cache@v2
with:
path: |
~/.cargo/registry
~/.cargo/git
node_modules
./docs/openapi/node_modules
target
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
with:
node-version: "16.x"
- name: start smtp server
run: docker run -d -p 1080:1080 -p 10025:1025 maildev/maildev --incoming-user admin --incoming-pass password
- name: Install ${{ matrix.version }}
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.version }}-x86_64-unknown-linux-gnu
profile: minimal
override: true
- name: Build frontend
run: make frontend
- name: Run the frontend tests
run: make frontend-test
- name: Run migrations
run: make migrate
env:
POSTGRES_DATABASE_URL: "${{ env.POSTGRES_DATABASE_URL }}"
- name: build frontend
run: make frontend
- name: Generate coverage file
if: github.event_name == 'pull_request'
#if: (github.ref == 'refs/heads/master' || github.event_name == 'pull_request')
uses: actions-rs/tarpaulin@v0.1
with:
args: "-t 1200"
env:
POSTGRES_DATABASE_URL: "${{ env.POSTGRES_DATABASE_URL }}"
# GIT_HASH is dummy value. I guess build.rs is skipped in tarpaulin
# execution so this value is required for preventing meta tests from
# panicking
GIT_HASH: 8e77345f1597e40c2e266cb4e6dee74888918a61
CACHE_BUSTER_FILE_MAP: '{"map":{"./static/bundle/main.js":"./prod/bundle/main.1417115E59909BE0A01040A45A398ADB09D928DF89CCF038FA44B14850442096.js"},"base_dir":"./prod"}'
COMPILED_DATE: "2021-07-21"
- name: Upload to Codecov
if: github.event_name == 'pull_request'
uses: codecov/codecov-action@v2

View File

@@ -1,14 +1,12 @@
name: Build name: CI (Linux)
on: on:
schedule:
- cron: "0 9 * * *"
pull_request: pull_request:
types: [opened, synchronize, reopened] types: [opened, synchronize, reopened]
push: push:
branches: branches:
- master - master
- db-abstract
jobs: jobs:
build_and_test: build_and_test:
@@ -16,14 +14,14 @@ jobs:
fail-fast: false fail-fast: false
matrix: matrix:
version: version:
#- 1.51.0
- stable - stable
# - nightly - nightly
name: ${{ matrix.version }} - x86_64-unknown-linux-gnu name: ${{ matrix.version }} - x86_64-unknown-linux-gnu
runs-on: ubuntu-latest runs-on: ubuntu-latest
services: services:
postgres: postgres:
image: postgres image: postgres
env: env:
@@ -37,10 +35,6 @@ jobs:
--health-retries 5 --health-retries 5
ports: ports:
- 5432:5432 - 5432:5432
mcaptcha-redis:
image: mcaptcha/cache
ports:
- 6379:6379
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@@ -50,23 +44,15 @@ jobs:
path: | path: |
~/.cargo/registry ~/.cargo/registry
~/.cargo/git ~/.cargo/git
node_modules
./docs/openapi/node_modules
target target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- uses: borales/actions-yarn@v2.0.0
- name: load env
run: |
source .env_sample \
&& echo "POSTGRES_DATABASE_URL=$POSTGRES_DATABASE_URL" >> $GITHUB_ENV
- uses: actions/setup-node@v2
with: with:
node-version: '16.x' cmd: install # will run `yarn install` command
- uses: borales/actions-yarn@v2.0.0
- name: start smtp server with:
run: docker run -d -p 1080:1080 -p 10025:1025 maildev/maildev --incoming-user admin --incoming-pass password cmd: build # will run `yarn build` command
- name: Install ${{ matrix.version }} - name: Install ${{ matrix.version }}
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
@@ -76,36 +62,66 @@ jobs:
override: true override: true
- name: Run migrations - name: Run migrations
run: make migrate uses: actions-rs/cargo@v1
with:
command: run
args: --bin tests-migrate
env: env:
POSTGRES_DATABASE_URL: "${{ env.POSTGRES_DATABASE_URL }}" DATABASE_URL: postgres://postgres:password@localhost:5432/postgres
- name: build - name: check build
run: make uses: actions-rs/cargo@v1
with:
command: check
args: --all --bins --examples --tests
env: env:
POSTGRES_DATABASE_URL: "${{ env.POSTGRES_DATABASE_URL }}" DATABASE_URL: postgres://postgres:password@localhost:5432/postgres
# - name: build frontend
# run: make frontend
#
- name: lint frontend
run: yarn lint
- name: run tests - name: tests
run: make test uses: actions-rs/cargo@v1
timeout-minutes: 40
with:
command: test
args: --all --all-features --no-fail-fast
env: env:
POSTGRES_DATABASE_URL: "${{ env.POSTGRES_DATABASE_URL }}" DATABASE_URL: postgres://postgres:password@localhost:5432/postgres
- name: Generate coverage file
if: matrix.version == 'stable' && (github.ref == 'refs/heads/master' || github.event_name == 'pull_request')
uses: actions-rs/tarpaulin@v0.1
with:
version: '0.15.0'
args: '-t 1200'
env:
DATABASE_URL: postgres://postgres:password@localhost:5432/postgres
# GIT_HASH is dummy value. I guess build.rs is skipped in tarpaulin
# execution so this value is required for preventing meta tests from
# panicking
GIT_HASH: 8e77345f1597e40c2e266cb4e6dee74888918a61
OPEN_API_DOCS: 8e77345f1597e40c2e266cb4e6dee74888918a61
CACHE_BUSTER_FILE_MAP: '{"map":{"./static/bundle/main.js":"./prod/bundle/main.1417115E59909BE0A01040A45A398ADB09D928DF89CCF038FA44B14850442096.js"},"base_dir":"./prod"}'
- name: Upload to Codecov
if: matrix.version == 'stable' && (github.ref == 'refs/heads/master' || github.event_name == 'pull_request')
uses: codecov/codecov-action@v1
with:
file: cobertura.xml
- name: generate documentation - name: generate documentation
if: matrix.version == 'stable' && (github.repository == 'mCaptcha/mCaptcha') if: matrix.version == 'stable' && (github.repository == 'mCaptcha/guard')
run: make doc uses: actions-rs/cargo@v1
with:
command: doc
args: --no-deps --workspace --all-features
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" OPEN_API_DOCS: 8e77345f1597e40c2e266cb4e6dee74888918a61
- name: Deploy to GitHub Pages - name: Deploy to GitHub Pages
if: matrix.version == 'stable' && (github.repository == 'mCaptcha/mCaptcha') if: matrix.version == 'stable' && (github.repository == 'mCaptcha/guard')
uses: JamesIves/github-pages-deploy-action@3.7.1 uses: JamesIves/github-pages-deploy-action@3.7.1
with: with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

9
.gitignore vendored
View File

@@ -5,12 +5,3 @@ tarpaulin-report.html
cobertura.xml cobertura.xml
prod/ prod/
node_modules/ node_modules/
/static-assets/bundle
static/cache/bundle
./templates/**/*.js
/static-assets/bundle/*
src/cache_buster_data.json
coverage
dist
assets
yarn-error.log

View File

@@ -1,19 +0,0 @@
## 0.1.0(unreleased)
### 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:
> ...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

2932
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,57 +1,57 @@
[package] [package]
name = "mcaptcha" name = "guard"
version = "0.1.0" version = "0.1.0"
description = "mCaptcha - a PoW-based CAPTCHA system" description = "mCaptcha - a PoW-based CAPTCHA system"
homepage = "https://mcaptcha.org" homepage = "https://mcaptcha.org"
repository = "https://github.com/mCaptcha/mCaptcha" repository = "https://github.com/mCaptcha/guard"
documentation = "https://mcaptcha.org/docs/" documentation = "https://mcaptcha.org/docs/"
license = "AGPLv3 or later version" lisense = "AGPLv3 or later version"
authors = ["realaravinth <realaravinth@batsense.net>"] authors = ["realaravinth <realaravinth@batsense.net>"]
edition = "2021" edition = "2018"
default-run = "mcaptcha" default-run = "guard"
build = "build.rs" 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 = "guard"
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 = "3.3.2"
actix = "0.13" actix = "0.10"
actix-identity = "0.4.0" actix-identity = "0.3"
actix-http = "3.0.4" actix-http = "2.2"
actix-rt = "2" actix-rt = "1"
actix-cors = "0.6.1" actix-cors = "0.5.4"
actix-service = "2.0.0" actix-service = "1.0.6"
async-trait = "0.1.51"
csrf = "0.4.0"
mime_guess = "2.0.3" mime_guess = "2.0.3"
rust-embed = "6.4.0" rust-embed = "5.9.0"
cache-buster = { git = "https://github.com/realaravinth/cache-buster" } cache-buster = { version = "0.1.0", git = "https://github.com/realaravinth/cache-buster" }
futures = "0.3.15" futures = "0.3.14"
tokio = { version = "1.14", features = ["sync"]}
sqlx = { version = "0.4.0", features = [ "runtime-actix-rustls", "postgres" ] }
argon2-creds = { version = "0.2", git = "https://github.com/realaravinth/argon2-creds", commit = "61f2d1d" }
sqlx = { version = "0.5.13", features = [ "runtime-actix-rustls", "postgres", "time", "offline" ] }
argon2-creds = { branch = "master", git = "https://github.com/realaravinth/argon2-creds"}
#argon2-creds = { version="*", path = "../../argon2-creds/" }
config = "0.11" config = "0.11"
validator = { version = "0.15", features = ["derive"]} validator = { version = "0.13", features = ["derive"]}
derive_builder = "0.11" derive_builder = "0.10"
derive_more = "0.99" derive_more = "0.99"
serde = "1" serde = "1"
serde_json = "1" serde_json = "1"
serde_yaml = "0.8.17"
url = "2.2" url = "2.2"
urlencoding = "2.1.0"
pretty_env_logger = "0.4" pretty_env_logger = "0.4"
log = "0.4" log = "0.4"
@@ -59,48 +59,23 @@ log = "0.4"
lazy_static = "1.4" lazy_static = "1.4"
libmcaptcha = { branch = "master", git = "https://github.com/mCaptcha/libmcaptcha", features = ["full"] } # m_captcha = { version = "0.1.2", git = "https://github.com/mCaptcha/mCaptcha" }
#libmcaptcha = { path = "../libmcaptcha", features = ["full"]} m_captcha = { branch = "master", git = "https://github.com/mCaptcha/mCaptcha" }
rand = "0.8" rand = "0.8"
sailfish = "0.4.0" sailfish = "0.3.2"
mime = "0.3.16"
lettre = { version = "0.10.0-rc.3", features = [
"builder",
"tokio1",
"tokio1-native-tls",
"smtp-transport"
]}
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]
git = "https://github.com/realaravinth/actix-web"
package = "actix-web-codegen"
[dependencies.actix-auth-middleware]
version = "0.2.0"
git = "https://github.com/realaravinth/actix-auth-middleware"
features = ["actix_identity_backend"]
[build-dependencies] [build-dependencies]
serde_yaml = "0.8.17"
serde = "1"
serde_json = "1" serde_json = "1"
sqlx = { version = "0.5.13", features = [ "runtime-actix-rustls", "postgres", "time", "offline" ] } yaml-rust = "0.4.5"
cache-buster = { version = "0.1.0", git = "https://github.com/realaravinth/cache-buster" }
mime = "0.3.16"
log = "0.4"
config = "0.11"
url = "2.2"
[dev-dependencies] [dev-dependencies]
pow_sha256 = { version = "0.2.1", git = "https://github.com/mcaptcha/pow_sha256" } pow_sha256 = { version = "0.2.1", git = "https://github.com/mcaptcha/pow_sha256" }
awc = "3.0.0"
[target.x86_64-unknown-linux-musl]
linker = "x86_64"

View File

@@ -1,5 +0,0 @@
[build.env]
passthrough = [
"RUST_BACKTRACE",
"RUST_LOG",
]

View File

@@ -1,31 +1,6 @@
# Development Setup # Development Setup
## To quickly make changes: ## Setting up development environment
We have a docker-compose config that you can use to quickly spin up dev
environment.
From the root of the repo, run:
```bash
$ docker-compose -d up
```
### Logs from docker:
- Logs from database and web server as they are generated:
```bash
$ docker-compose logs -f
```
- from just webserver:
```bash
$ docker-compose logs -f mcaptcha
```
## Setting up elaborate development environment
### Toolchain ### Toolchain
@@ -38,7 +13,7 @@ You'll have to install before you can start writing code.
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh $ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
``` ```
2. Install Node `v14.16.0`: 2. Install Node:
Please refer to [official instructions](https://nodejs.org/en/download/) Please refer to [official instructions](https://nodejs.org/en/download/)
3. Install yarn: 3. Install yarn:
@@ -60,7 +35,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
@@ -88,7 +63,7 @@ $ docker start mcaptcha-postgres
4. Set configurations: 4. Set configurations:
```bash ```bash
$ cd mcaptcha # your copy of https://github.com/mCaptcha/mcaptcha $ cd guard # your copy of https://github.com/mCaptcha/guard
$ echo 'export DATABASE_URL="postgres://postgres:password@localhost:5432/postgres"' > .env $ echo 'export DATABASE_URL="postgres://postgres:password@localhost:5432/postgres"' > .env
``` ```
@@ -103,7 +78,7 @@ $ echo 'export DATABASE_URL="postgres://postgres:password@localhost:5432/postgre
However, this project ships with a utility to run migrations! However, this project ships with a utility to run migrations!
```bash ```bash
$ cd mcaptcha # your copy of https://github.com/mCaptcha/mcaptcha $ cd guard # your copy of https://github.com/mCaptcha/guard
$ cargo run --bin tests-migrate $ cargo run --bin tests-migrate
``` ```
@@ -114,27 +89,20 @@ That's it, you are all set!
### Compile: ### Compile:
```bash ```bash
$ cd mcaptcha # your copy of https://github.com/mCaptcha/mcaptcha $ cd guard # your copy of https://github.com/mCaptcha/guard
$ make $ make
``` ```
### Additional commands: ### Additional commands:
```bash ```bash
mcaptcha git:(master) ✗ make help guard git:(master) ✗ make help
default Run app in debug mode docs - build documentation
clean Delete build artifacts run - run developer instance
coverage Generate code coverage report in HTML format test - run unit and integration tests
dev-env Setup development environment migrate - run database migrations
doc Generate documentation dev-env - download dependencies
docker Build Docker image clean - drop builds and environments
docker-publish Build and publish Docker image coverage - build test coverage in HTML format
frontend Build frontend xml-coverage - build test coverage in XML for upload to codecov
frontend-test Run frontend tests
lint Lint codebase
migrate Run database migrations
release Build app with release optimizations
test Run all available tests
xml-test-coverage Generate code coverage report in XML format
help Prints help for targets with comments
``` ```

View File

@@ -1,52 +0,0 @@
FROM node:16.0.0 as frontend
RUN set -ex; \
apt-get update; \
DEBIAN_FRONTEND=noninteractive \
apt-get install -y --no-install-recommends make
RUN mkdir -p /src/docs/openapi/
COPY package.json yarn.lock /src/
COPY docs/openapi/package.json docs/openapi/yarn.lock /src/docs/openapi/
WORKDIR /src
RUN yarn install && cd docs/openapi && yarn install
WORKDIR /src
RUN mkdir -p /src/static/cache/bundle
COPY tsconfig.json webpack.config.js jest.config.ts /src/
COPY templates /src/templates/
COPY docs/openapi /src/docs/openapi/
COPY Makefile /src/
COPY scripts /src/scripts
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
WORKDIR /src
COPY . .
COPY --from=cacher /src/target target
#COPY --from=cacher /src/db/db-core/target /src/db/db-core/target
#COPY --from=cacher /src/db/db-sqlx-postgres/target /src/db/db-sqlx-postgres/target
#COPY --from=cacher /src/db/db-migrations/target /src/db/db-migrations/target
#COPY --from=cacher /src/utils/cache-bust/target /src/utils/cache-bust/target
COPY --from=frontend /src/static/cache/bundle/ /src/static/cache/bundle/
RUN cargo --version
RUN make cache-bust
RUN cargo build --release
FROM debian:bullseye as mCaptcha
LABEL org.opencontainers.image.source https://github.com/mCaptcha/mCaptcha
RUN useradd -ms /bin/bash -u 1001 mcaptcha
WORKDIR /home/mcaptcha
COPY --from=rust /src/target/release/mcaptcha /usr/local/bin/
COPY --from=rust /src/config/default.toml /etc/mcaptcha/config.toml
USER mcaptcha
CMD [ "/usr/local/bin/mcaptcha" ]

158
Makefile
View File

@@ -1,127 +1,49 @@
BUNDLE = static/cache/bundle # WIP
OPENAPI = docs/openapi default: build-frontend
CLEAN_UP = $(BUNDLE) src/cache_buster_data.json assets
define frontend_env ## install frontend deps
yarn install
cd docs/openapi && yarn install
endef
define cache_bust ## run cache_busting program
cd utils/cache-bust && cargo run
endef
default: frontend ## Build app in debug mode
$(call cache_bust)
cargo build cargo build
check: ## Check for syntax errors on all workspaces run: build-frontend-dev
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
@cargo clean
@yarn cache clean
@-rm $(CLEAN_UP)
coverage: migrate ## Generate code coverage report in HTML format
$(call cache_bust)
cargo tarpaulin -t 1200 --out Html
doc: ## Generate documentation
#yarn doc
cargo doc --no-deps --workspace --all-features
docker: ## Build Docker image
docker build -t mcaptcha/mcaptcha:master -t mcaptcha/mcaptcha:latest .
docker-publish: docker ## Build and publish Docker image
docker push mcaptcha/mcaptcha:master
docker push mcaptcha/mcaptcha:latest
env: ## Setup development environtment
cargo fetch
$(call frontend_env)
frontend-env: ## Install frontend deps
$(call frontend_env)
frontend: ## Build frontend
$(call frontend_env)
cd $(OPENAPI) && yarn build
yarn install
@-rm -rf $(BUNDLE)
@-mkdir $(BUNDLE)
yarn build
@yarn run sass -s \
compressed templates/main.scss \
./static/cache/bundle/css/main.css
@yarn run sass -s \
compressed templates/mobile.scss \
./static/cache/bundle/css/mobile.css
@yarn run sass -s \
compressed templates/widget/main.scss \
./static/cache/bundle/css/widget.css
@./scripts/librejs.sh
@./scripts/cachebust.sh
frontend-test: ## Run frontend tests
cd $(OPENAPI)&& yarn test
yarn test
lint: ## Lint codebase
cargo fmt -v --all -- --emit files
cargo clippy --workspace --tests --all-features
yarn lint
cd $(OPENAPI)&& yarn test
migrate: ## Run database migrations
cd db/db-migrations/ && \
DATABASE_URL=${POSTGRES_DATABASE_URL} cargo run
release: frontend ## Build app with release optimizations
$(call cache_bust)
cargo build --release
run: frontend ## Run app in debug mode
cargo run cargo run
dev-env:
cargo fetch
yarn install
sqlx-offline-data: ## prepare sqlx offline data docs:
cargo sqlx prepare --database-url=${POSTGRES_DATABASE_URL} -- --bin mcaptcha \ cargo doc --no-deps --workspace --all-features
--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 build-frontend-dev:
cd db/db-sqlx-postgres &&\ yarn start
DATABASE_URL=${POSTGRES_DATABASE_URL}\
cargo test --no-fail-fast
test: frontend-test frontend ## Run all available tests
$(call cache_bust)
cd db/db-sqlx-postgres &&\
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 build-frontend:
$(call cache_bust) yarn build
test: migrate
cargo test
xml-test-coverage: migrate
cargo tarpaulin -t 1200 --out Xml cargo tarpaulin -t 1200 --out Xml
help: ## Prints help for targets with comments coverage: migrate
@cat $(MAKEFILE_LIST) | grep -E '^[a-zA-Z_-]+:.*?## .*$$' | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' cargo tarpaulin -t 1200 --out Html
release: build-frontend
cargo build --release
clean:
cargo clean
yarn clean
migrate:
cargo run --bin tests-migrate
help:
@echo ' docs - build documentation'
@echo ' run - run developer instance'
@echo ' test - run unit and integration tests'
@echo ' migrate - run database migrations'
@echo ' dev-env - download dependencies'
@echo ' clean - drop builds and environments'
@echo ' coverage - build test coverage in HTML format'
@echo ' xml-coverage - build test coverage in XML for upload to codecov'
@echo ''

155
README.md
View File

@@ -1,131 +1,80 @@
<div align="center"> <div align="center">
<img width="100px" alt="mcaptcha logo" src="./docs/res/icon-trans.png" /> <h1>mCaptcha Guard</h1>
<h1>mCaptcha</h1>
<p> <p>
<strong> <strong>Back-end component of mCaptcha</strong>
Proof of work based, privacy respecting CAPTCHA system with a kickass UX.
</strong>
</p> </p>
[![Documentation](https://img.shields.io/badge/docs-master-blue?style=flat-square)](https://mcaptcha.github.io/mCaptcha/mCaptcha/) [![Documentation](https://img.shields.io/badge/docs-master-blue)](https://mcaptcha.github.io/guard/guard/)
[![Build](https://github.com/mCaptcha/mCaptcha/actions/workflows/linux.yml/badge.svg)](https://github.com/mCaptcha/mCaptcha/actions/workflows/linux.yml) ![CI (Linux)](<https://github.com/mCaptcha/guard/workflows/CI%20(Linux)/badge.svg>)
[![Docker](https://img.shields.io/docker/pulls/mcaptcha/mcaptcha)](https://hub.docker.com/r/mcaptcha/mcaptcha) [![dependency status](https://deps.rs/repo/github/mCaptcha/guard/status.svg)](https://deps.rs/repo/github/mCaptcha/guard)
[![dependency status](https://deps.rs/repo/github/mCaptcha/mCaptcha/status.svg?style=flat-square)](https://deps.rs/repo/github/mCaptcha/mCaptcha) [![codecov](https://codecov.io/gh/mCaptcha/guard/branch/master/graph/badge.svg)](https://codecov.io/gh/mCaptcha/guard)
[![codecov](https://codecov.io/gh/mCaptcha/mCaptcha/branch/master/graph/badge.svg?style=flat-square)](https://codecov.io/gh/mCaptcha/mCaptcha)
<br /> <br />
[![AGPL License](https://img.shields.io/badge/license-AGPL-blue.svg?style=flat-square)](http://www.gnu.org/licenses/agpl-3.0) [![AGPL License](https://img.shields.io/badge/license-AGPL-blue.svg)](http://www.gnu.org/licenses/agpl-3.0)
[![Chat](https://img.shields.io/badge/matrix-+mcaptcha:matrix.batsense.net-purple?style=flat-square)](https://matrix.to/#/+mcaptcha:matrix.batsense.net)
**STATUS: ACTIVE DEVELOPMENT**
</div> </div>
</div> </div>
**Skip to [demo](#demo)** Guard is the back-end component of [mCaptcha](https://mcaptcha.org)
system.
[mCaptcha](https://mcaptcha.org) is a privacy respecting, _free_ CAPTCHA **STATUS: UNUSABLE BUT ACTIVE DEVELOPMENT**
system with a kickass UX. Your users no longer have to interact with
ridiculous image-based CAPTCHA system, wasting precious mental
bandwidth. Instead, your computer will do the work for you, [see for
yourself!](https://demo.mcaptcha.org/widget/?sitekey=pHy0AktWyOKuxZDzFfoaewncWecCHo23)
## How does it work? ### Development:
mCaptcha uses SHA256 based proof-of-work(PoW) to rate limit users. See [DEVELOPMENT.md](./DEVELOPMENT.md)
When a user wants to do something on an mCaptcha-protected website, ### How to build
1. they will have to generate proof-of-work(a bunch of math that will takes - Install Cargo using [rustup](https://rustup.rs/) with:
time to compute) and submit it to mCaptcha.
2. We'll validate the proof: ```
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
- **if validation is unsuccessful**, they will be prevented from
accessing their target website
- **if validation is successful**, read on,
3. They will be issued a token that they should submit along
with their request/form submission to the target website.
4. The target website should validate the user-submitted token with mCaptcha
before processing the user's request.
The whole process is automated from the user's POV. All they have to do
is click on a button to initiate the process.
mCaptcha makes interacting with websites (computationally)expensive for
the user. A well-behaving user will experience a slight delay(no delay
when under moderate load to 2s when under attack; PoW difficulty is
variable) but if someone wants to hammer your site, they will have to do
more work to send requests than your server will have to do to respond
to their request.
## Why use mCaptcha?
- [x] **Free software, privacy focused**
- [x] **Seamless UX** - No more annoying CAPTCHAs!
- [x] **No tracking:** Our CAPTCHA routes are cookie free!
- [x] **IP address independent:** your users are behind a NAT? We got you covered!
- [x] **Resistant to replay attacks:** proof-of-work configurations have
short lifetimes(30s) and can be used only once. If a user submits a
PoW to an already used configuration or an expired one, their proof
will be rejected.
## Demo
## Client-side widget:
mCaptcha's UX is super silent, solving CAPTCHAs have never been more
easier. One click and you are on your way.
To observe mCaptcha in action, open dev tools and
monitor console and network activity.
1. [Link to widget](https://demo.mcaptcha.org/widget/?sitekey=pHy0AktWyOKuxZDzFfoaewncWecCHo23)
2. [Video](https://github.com/mCaptcha/mCaptcha/blob/master/docs/res/widget-in-action.mp4?raw=true):
### Demo servers are available at:
- https://demo.mcaptcha.org/
- https://demo2.mcaptcha.org/ (runs on a Raspberry Pi!)
> Core functionality is working but it's still very much
> work-in-progress. Since we don't have a stable release yet, hosted
> demo servers might be a few versions behind `master`. Please check footer for
> build commit.
Feel free to provide bogus information while signing up(project under
development, database frequently wiped).
### Self-hosted:
Clone the repo and run the following from the root of the repo:
```bash
git clone https://github.com/mCaptcha/mCaptcha.git
docker-compose up -d
``` ```
After the containers are up, visit [http://localhost:7000](http://localhost:7000) and login with the default credentials: - Clone the repository with:
- username: aaronsw ```
- password: password $ git clone https://github.com/mCaptcha/guard
```
- Build with Cargo:
It takes a while to build the image so please be patient :) ```
$ cd guard && cargo build
```
See [DEPLOYMENT.md](./docs/DEPLOYMENT.md) detailed alternate deployment ### Configuration:
methods.
## Development: Guard is highly configurable.
Configuration is applied/merged in the following order:
See [HACKING.md](./docs/HACKING.md) 1. `config/default.toml`
2. environment variables.
## Deployment: #### Setup
See [DEPLOYMENT.md](./docs/DEPLOYMENT.md) ##### Environment variables:
## Configuration: Setting environment variables are optional. The configuration files have
all the necessary parameters listed. By setting environment variables,
you will be overriding the values set in the configuration files.
See [CONFIGURATION.md](./docs/CONFIGURATION.md) ###### Database:
| Name | Value |
| ------------------------- | -------------------------------------- |
| `GUARD_DATEBASE_PASSWORD` | Postgres password |
| `GUARD_DATEBASE_NAME` | Postgres database name |
| `GUARD_DATEBASE_PORT` | Postgres port |
| `GUARD_DATEBASE_HOSTNAME` | Postgres hostmane |
| `GUARD_DATEBASE_USERNAME` | Postgres username |
| `GUARD_DATEBASE_POOL` | Postgres database connection pool size |
###### Server:
| Name | Value |
| ----------------------------------- | --------------------------------------------------- |
| `GUARD_SERVER_PORT` (or) `PORT`\*\* | The port on which you want wagon to listen to |
| `GUARD_SERVER_IP` | The IP address on which you want wagon to listen to |
| `GUARD_SERVER_STATIC_FILES_DIR` | Path to directory containing static files |

View File

@@ -1,22 +1,26 @@
/* /*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net> * Copyright (C) 2021 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::process::Command; use std::process::Command;
use sqlx::types::time::OffsetDateTime; use cache_buster::BusterBuilder;
#[path = "./src/settings.rs"]
mod settings;
fn main() { fn main() {
// note: add error checking yourself. // note: add error checking yourself.
@@ -27,6 +31,35 @@ fn main() {
let git_hash = String::from_utf8(output.stdout).unwrap(); let git_hash = String::from_utf8(output.stdout).unwrap();
println!("cargo:rustc-env=GIT_HASH={}", git_hash); println!("cargo:rustc-env=GIT_HASH={}", git_hash);
let now = OffsetDateTime::now_utc().format("%y-%m-%d"); let yml = include_str!("./openapi.yaml");
println!("cargo:rustc-env=COMPILED_DATE={}", &now); let api_json: serde_json::Value = serde_yaml::from_str(yml).unwrap();
println!(
"cargo:rustc-env=OPEN_API_DOCS={}",
serde_json::to_string(&api_json).unwrap()
);
cache_bust();
}
fn cache_bust() {
let settings = settings::Settings::new().unwrap();
let types = vec![
mime::IMAGE_PNG,
mime::IMAGE_SVG,
mime::IMAGE_JPEG,
mime::IMAGE_GIF,
mime::APPLICATION_JAVASCRIPT,
mime::TEXT_CSS,
];
let config = BusterBuilder::default()
.source("./static")
.result("./prod")
.prefix(settings.server.url_prefix)
.mime_types(types)
.copy(true)
.follow_links(true)
.build()
.unwrap();
config.process().unwrap();
} }

View File

@@ -1,132 +0,0 @@
# Contributor Covenant Code of Conduct
## Our Pledge
We as members, contributors, and leaders pledge to make participation in our
community a harassment-free experience for everyone, regardless of age, body
size, visible or invisible disability, ethnicity, sex characteristics, gender
identity and expression, level of experience, education, socio-economic status,
nationality, personal appearance, race, caste, color, religion, or sexual identity
and orientation.
We pledge to act and interact in ways that contribute to an open, welcoming,
diverse, inclusive, and healthy community.
## Our Standards
Examples of behavior that contributes to a positive environment for our
community include:
- Demonstrating empathy and kindness toward other people
- Being respectful of differing opinions, viewpoints, and experiences
- Giving and gracefully accepting constructive feedback
- Accepting responsibility and apologizing to those affected by our mistakes,
and learning from the experience
- Focusing on what is best not just for us as individuals, but for the
overall community
Examples of unacceptable behavior include:
- The use of sexualized language or imagery, and sexual attention or
advances of any kind
- Trolling, insulting or derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or email
address, without their explicit permission
- Other conduct which could reasonably be considered inappropriate in a
professional setting
## Enforcement Responsibilities
Community leaders are responsible for clarifying and enforcing our standards of
acceptable behavior and will take appropriate and fair corrective action in
response to any behavior that they deem inappropriate, threatening, offensive,
or harmful.
Community leaders have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, and will communicate reasons for moderation
decisions when appropriate.
## Scope
This Code of Conduct applies within all community spaces, and also applies when
an individual is officially representing the community in public spaces.
Examples of representing our community include using an official e-mail address,
posting via an official social media account, or acting as an appointed
representative at an online or offline event.
## Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported to the community leaders responsible for enforcement at
[INSERT CONTACT METHOD].
All complaints will be reviewed and investigated promptly and fairly.
All community leaders are obligated to respect the privacy and security of the
reporter of any incident.
## Enforcement Guidelines
Community leaders will follow these Community Impact Guidelines in determining
the consequences for any action they deem in violation of this Code of Conduct:
### 1. Correction
**Community Impact**: Use of inappropriate language or other behavior deemed
unprofessional or unwelcome in the community.
**Consequence**: A private, written warning from community leaders, providing
clarity around the nature of the violation and an explanation of why the
behavior was inappropriate. A public apology may be requested.
### 2. Warning
**Community Impact**: A violation through a single incident or series
of actions.
**Consequence**: A warning with consequences for continued behavior. No
interaction with the people involved, including unsolicited interaction with
those enforcing the Code of Conduct, for a specified period of time. This
includes avoiding interactions in community spaces as well as external channels
like social media. Violating these terms may lead to a temporary or
permanent ban.
### 3. Temporary Ban
**Community Impact**: A serious violation of community standards, including
sustained inappropriate behavior.
**Consequence**: A temporary ban from any sort of interaction or public
communication with the community for a specified period of time. No public or
private interaction with the people involved, including unsolicited interaction
with those enforcing the Code of Conduct, is allowed during this period.
Violating these terms may lead to a permanent ban.
### 4. Permanent Ban
**Community Impact**: Demonstrating a pattern of violation of community
standards, including sustained inappropriate behavior, harassment of an
individual, or aggression toward or disparagement of classes of individuals.
**Consequence**: A permanent ban from any sort of public interaction within
the community.
## Attribution
This Code of Conduct is adapted from the [Contributor Covenant][homepage],
version 2.0, available at
[https://www.contributor-covenant.org/version/2/0/code_of_conduct.html][v2.0].
Community Impact Guidelines were inspired by
[Mozilla's code of conduct enforcement ladder][mozilla coc].
For answers to common questions about this code of conduct, see the FAQ at
[https://www.contributor-covenant.org/faq][faq]. Translations are available
at [https://www.contributor-covenant.org/translations][translations].
[homepage]: https://www.contributor-covenant.org
[v2.0]: https://www.contributor-covenant.org/version/2/0/code_of_conduct.html
[mozilla coc]: https://github.com/mozilla/diversity
[faq]: https://www.contributor-covenant.org/faq
[translations]: https://www.contributor-covenant.org/translations

View File

@@ -1,40 +1,4 @@
debug = true debug = true
source_code = "https://github.com/mCaptcha/mCaptcha"
commercial = false
allow_demo = true
allow_registration = true
[server]
# Please set a unique value, your mCaptcha instance's security depends on this being
# unique
cookie_secret = "Zae0OOxf^bOJ#zN^&k7VozgW&QAx%n02TQFXpRMG4cCU0xMzgu3dna@tQ9dvc&TlE6p*n#kXUdLZJCQsuODIV%r$@o4%770ePQB7m#dpV!optk01NpY0@615w5e2Br4d"
# The port at which you want authentication to listen to
# takes a number, choose from 1000-10000 if you dont know what you are doing
port = 7000
#IP address. Enter 0.0.0.0 to listen on all availale addresses
ip= "0.0.0.0"
# enter your hostname, eg: example.com
domain = "localhost"
# Set true if you have setup TLS with a reverse proxy like Nginx.
# Does HTTPS redirect and sends additional headers that can only be used if
# HTTPS available to improve security
proxy_has_tls = false
#url_prefix = ""
[captcha]
# Please set a unique value, your mCaptcha instance's security depends on this being
# unique
salt = "asdl;kjfhjawehfpa;osdkjasdvjaksndfpoanjdfainsdfaijdsfajlkjdsaf;ajsdfweroire"
# garbage collection period to manage mCaptcha system
# leave untouched if you don't know what you are doing
gc = 30
enable_stats = true
[captcha.default_difficulty_strategy]
avg_traffic_difficulty = 50000 # almost instant solution
peak_sustainable_traffic_difficulty = 3000000 # roughly 1.5s
broke_my_site_traffic_difficulty = 5000000 # greater than 3.5s
duration = 30 # cooldown period in seconds
[database] [database]
# This section deals with the database location and how to access it # This section deals with the database location and how to access it
@@ -51,21 +15,25 @@ password = "password"
name = "postgres" name = "postgres"
pool = 4 pool = 4
[redis] # This section deals with the configuration of the actual server
# This section deals with the database location and how to access it [server]
# Please note that at the moment, we have support for only postgresqa. # Please set a unique value, your mCaptcha instance's security depends on this being
# Example, if you are Batman, your config would be: # unique
# hostname = "batcave.org" cookie_secret = "Zae0OOxf^bOJ#zN^&k7VozgW&QAx%n02TQFXpRMG4cCU0xMzgu3dna@tQ9dvc&TlE6p*n#kXUdLZJCQsuODIV%r$@o4%770ePQB7m#dpV!optk01NpY0@615w5e2Br4d"
# port = "5432" # The port at which you want authentication to listen to
# username = "batman" # takes a number, choose from 1000-10000 if you dont know what you are doing
# password = "somereallycomplicatedBatmanpassword" port = 7000
url = "redis://127.0.0.1" #IP address. Enter 0.0.0.0 to listen on all availale addresses
pool = 4 ip= "0.0.0.0"
# enter your hostname, eg: example.com
domain = "localhost"
allow_registration = true
url_prefix = "/test"
[smtp] [pow]
from = "admin@localhost" # Please set a unique value, your mCaptcha instance's security depends on this being
reply = "admin@localhost" # unique
url = "127.0.0.1" salt = "asdl;kjfhjawehfpa;osdkjasdvjaksndfpoanjdfainsdfaijdsfajlkjdsaf;ajsdfweroire"
port = 10025 # garbage collection period to manage mCaptcha system
username = "admin" # leave untouched if you don't know what you are doing
password = "password" gc = 30

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,40 +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::env;
use sqlx::postgres::PgPoolOptions;
#[cfg(not(tarpaulin_include))]
#[actix_rt::main]
async fn main() {
//TODO featuregate sqlite and postgres
postgres_migrate().await;
}
async fn postgres_migrate() {
let db_url = env::var("POSTGRES_DATABASE_URL").expect("set POSTGRES_DATABASE_URL env var");
let db = PgPoolOptions::new()
.max_connections(2)
.connect(&db_url)
.await
.expect("Unable to form database pool");
sqlx::migrate!("../db-sqlx-postgres/migrations/")
.run(&db)
.await
.unwrap();
}

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,4 +0,0 @@
CREATE TABLE IF NOT EXISTS mcaptcha_pow_fetched_stats (
config_id INTEGER references mcaptcha_config(config_id) ON DELETE CASCADE,
time timestamptz NOT NULL DEFAULT now()
);

View File

@@ -1,4 +0,0 @@
CREATE TABLE IF NOT EXISTS mcaptcha_pow_solved_stats (
config_id INTEGER references mcaptcha_config(config_id) ON DELETE CASCADE,
time timestamptz NOT NULL DEFAULT now()
);

View File

@@ -1,4 +0,0 @@
CREATE TABLE IF NOT EXISTS mcaptcha_pow_confirmed_stats (
config_id INTEGER references mcaptcha_config(config_id) ON DELETE CASCADE,
time timestamptz NOT NULL DEFAULT now()
);

View File

@@ -1,10 +0,0 @@
-- Add migration script here
CREATE TABLE IF NOT EXISTS mcaptcha_notifications (
id SERIAL PRIMARY KEY NOT NULL,
tx INTEGER NOT NULL references mcaptcha_users(ID) ON DELETE CASCADE,
rx INTEGER NOT NULL references mcaptcha_users(ID) ON DELETE CASCADE,
heading varchar(30) NOT NULL,
message varchar(250) NOT NULL,
read BOOLEAN DEFAULT NULL,
received timestamptz NOT NULL DEFAULT now()
);

View File

@@ -1,6 +0,0 @@
CREATE TABLE IF NOT EXISTS mcaptcha_sitekey_user_provided_avg_traffic (
config_id INTEGER PRIMARY KEY UNIQUE NOT NULL references mcaptcha_config(config_id) ON DELETE CASCADE,
avg_traffic INTEGER DEFAULT NULL,
peak_sustainable_traffic INTEGER DEFAULT NULL,
broke_my_site_traffic INTEGER DEFAULT NULL
);

View File

@@ -1,3 +0,0 @@
ALTER TABLE mcaptcha_sitekey_user_provided_avg_traffic
ALTER COLUMN avg_traffic SET NOT NULL,
ALTER COLUMN peak_sustainable_traffic SET NOT NULL;

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,24 +0,0 @@
-- gets all unread notifications a user has
SELECT
mcaptcha_notifications.id,
mcaptcha_notifications.heading,
mcaptcha_notifications.message,
mcaptcha_notifications.received,
mcaptcha_users.name
FROM
mcaptcha_notifications
INNER JOIN
mcaptcha_users
ON
mcaptcha_notifications.tx = mcaptcha_users.id
WHERE
mcaptcha_notifications.rx = (
SELECT
id
FROM
mcaptcha_users
WHERE
name = $1
)
AND
mcaptcha_notifications.read IS NULL;

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,14 +0,0 @@
-- mark a notification as read
UPDATE mcaptcha_notifications
SET read = TRUE
WHERE
mcaptcha_notifications.id = $1
AND
mcaptcha_notifications.rx = (
SELECT
id
FROM
mcaptcha_users
WHERE
name = $2
);

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

@@ -1,29 +0,0 @@
version: "3.9"
services:
mcaptcha:
image: mcaptcha/mcaptcha:latest
ports:
- 7000:7000
environment:
DATABASE_URL: postgres://postgres:password@mcaptcha_postgres:5432/postgres # set password at placeholder
MCAPTCHA_REDIS_URL: redis://mcaptcha-redis/
RUST_LOG: debug
PORT: 7000
depends_on:
- mcaptcha-postgres
- mcaptcha-redis
mcaptcha_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,87 +0,0 @@
# Configuration
mCaptcha is highly configurable.
Configuration is applied/merged in the following order:
1. path to configuration file passed in via `MCAPTCHA_CONFIG`
2. `./config/default.toml`
3. `/etc/mcaptcha/config.toml`
4. environment variables.
## Setup
### Environment variables
Setting environment variables are optional. The configuration files have
all the necessary parameters listed. By setting environment variables,
you will be overriding the values set in the configuration files.
### General
| Name | Value |
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------- |
| `MCAPTCHA_CONFIG` | Path to configuration file |
| `MCAPTCHA_COMMERCIAL` | Does this instance offer commercial plans? Please consider donating if it does :D |
| `MCAPTCHA_SOURCE_CODE` | Link to the source code of this instance |
| `MCAPTCHA_ALLOW_REGISTRATION` | Is registration allowed on this instance? |
| `MCAPTCHA_ALLOW_DEMO` | Allow demo access to the server? If registration(previous option) is disabled then demo users will not be allowed |
#### Database
| Name | Value |
| ------------------------------------ | ------------------------------------------------------------- |
| `MCAPTCHA_DATEBASE_PASSWORD` | Postgres password |
| `MCAPTCHA_DATEBASE_NAME` | Postgres database name |
| `MCAPTCHA_DATEBASE_PORT` | Postgres port |
| `MCAPTCHA_DATEBASE_HOSTNAME` | Postgres hostname |
| `MCAPTCHA_DATEBASE_USERNAME` | Postgres username |
| `MCAPTCHA_DATEBASE_POOL` | Postgres database connection pool size |
| `DATABSE_URL` (overrides above vars) | databse URL in `postgres://user:pass@host:port/dbname` format |
#### Redis
| Name | Value |
| --------------------- | -------------------------- |
| `MCAPTCHA_REDIS_URL` | Redis URL |
| `MCAPTCHA_REDIS_POOL` | Redis connection pool size |
#### Server
| Name | Value |
| ---------------------------------------- | ---------------------------------------------------------------------------------- |
| `MCAPTCHA_SERVER_PORT` | The port on which you want mCaptcha to listen to |
| `PORT`(overrides `MCAPTCHA_SERVER_PORT`) | The port on which you want mCaptcha to listen to |
| `MCAPTCHA_SERVER_IP` | The IP address on which you want mCaptcha to listen to |
| `MCAPTCHA_SERVER_DOMAIN` | Domain under which mCaptcha will be\* |
| `MCAPTCHA_SERVER_COOKIE_SECRET` | Cookie secret, must be long and random |
| `MCAPTCHA_SERVER_PROXY_HAS_TLS` | Is mCaptcha behind a proxy? If yes, mCaptcha can send additional headers like HSTS |
\* Authentication doesn't work without `MCAPTCHA_DOMAIN` set to the correct domain
### Captcha
| Name | Value |
| --------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `MCAPTCHA_CAPTCHA_SALT` | Salt has to be long and random |
| `MCAPTCHA_CAPTCHA_GC` | Garbage collection duration in seconds, requires tuning but 30 is a good starting point |
| `MCAPTCHA_CAPTCHA_AVG_TRAFFIC_DIFFICULTY`% | Difficulty factor to use in CAPTCHA configuration estimation for average traffic metric |
| `MCAPTCHA_CAPTCHA_PEAK_TRAFFIC_DIFFICULTY`% | Difficulty factor to use in CAPTCHA configuration estimation for peak traffic metric |
| `MCAPTCHA_CAPTCHA_BROKE_MY_SITE_TRAFFIC_DIFFICULTY`% | Difficulty factor to use in CAPTCHA configuration estimation for traffic that took the website down |
\% See commits
[`54b14291ec140e`](https://github.com/mCaptcha/mCaptcha/commit/54b14291ec140ea4cbbf73462d3d6fc2d39f2d2c)
and
[`42544ec421e0`](https://github.com/mCaptcha/mCaptcha/commit/42544ec421e0c3ec4a8d132e6101ab4069bf0065)
for more info.
### SMTP
| Name | Value |
| ------------------------ | ----------------------------------------------- |
| `MCAPTCHA_SMTP_FROM` | email address from which the email will be sent |
| `MCAPTCHA_SMTP_REPLY_TO` | email address to which reply can be sent |
| `MCAPTCHA_URL` | SMTP server URL |
| `MCAPTCHA_SMTP_PORT` | SMTP server port |
| `MCAPTCHA_SMTP_USERNAME` | SMTP username |
| `MCAPTCHA_SMTP_PASSWORD` | SMTP password |

View File

@@ -1,154 +0,0 @@
# Deployment instructions:
See [CONFIGURATION.md](./CONFIGURATION.md) for configuration instructions
There are three ways to deploy mCaptcha:
1. Docker
2. Docker compose
3. Bare metal
## Docker
NOTE: We'll publish pre-built images once we reach `alpha`.
1. Build image:
```bash
$ cd mcaptcha && docker build -t mcaptcha/mcaptcha:latest .
```
2. Set configuration in [configuration file](../config/default.toml)
3. Run image:
If you have already have a Postgres instance running, then:
```bash
docker run -p <host-machine-port>:<port-in-configuration-file> \
--add-host=database:<database-ip-addrss> \
-e RUST_LOG=debug \
-e DATABASE_URL="postgres://<db-user>:<db-password>@database:<db-port>/<db-name>" \
mcaptcha/mcaptcha:latest
```
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
configuration](../docker-compose.yml) is available that will launch both
a database instance mcaptcha instance.
## With docker-compose
1. Follow steps above to build docker image.
2. Set database password [docker-compose configuration](../docker-compose.yml).
3. Launch network:
```bash
$ docker-compose up -d
```
## Bare metal:
The process is tedious, most of this will be automated with a script in
the future.
### 1. Install postgres if you don't have it already.
### 2. Create new user for running `mcaptcha`:
```bash
$ sudo useradd -b /srv -m -s /usr/bin/zsh mcaptcha
```
### 3. Create new user in Postgres
```bash
$ sudo -iu postgres # switch to `postgres` user
$ psql
postgres=# CREATE USER mcaptcha WITH PASSWORD 'my super long password and yes you need single quote`;
$ createdb -O mcaptcha mcaptcha # create db 'mcaptcha' with 'mcaptcha' as owner
```
### 4. Install and load [`mCaptcha/cache`](https://github.com/mCaptcha/cache) module:
See [`mCaptcha/cache`](https://github.com/mCaptcha/cache) for more
details.
### 4. Build `mcaptcha`:
To build `mcaptcha`, you need the following dependencies:
1. rust
2. node(`v14.16.0`)
3. yarn(JavaScript package manager)
4. make
## How to build
1. Install Cargo using [rustup](https://rustup.rs/) with:
```bash
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```
2. Install node(`v14.16.0`)
3. Install yarn(JavaScript package manager)
4. Build with make:
```bash
$ make dev-env && \
make release
```
### 5. Install package:
```bash
$ sudo cp ./target/release/mcaptcha /usr/bin/ && \
mkdir sudo /etc/mcaptcha && \
sudo cp config/default.toml /etc/mcaptcha/config.toml
```
### 6. Systemd service configuration:
1. Copy the following to `/etc/systemd/system/mcaptcha.service`:
```systemd
[Unit]
Description=mCaptcha: a CAPTCHA system that gives attackers a run for their money
[Service]
Type=simple
User=mcaptcha
ExecStart=/usr/bin/mcaptcha
Restart=on-failure
RestartSec=1
SuccessExitStatus=3 4
RestartForceExitStatus=3 4
SystemCallArchitectures=native
MemoryDenyWriteExecute=true
NoNewPrivileges=true
Environment="RUST_LOG=info"
[Unit]
After=sound.target
Wants=network-online.target
Wants=network-online.target
Requires=postgresql.service
After=syslog.target
[Install]
WantedBy=multi-user.target
```
2. Enable service:
```bash
$ sudo systemctl daemon-reload && \
sudo systemctl enable mcaptcha && \ # Auto startup during boot
sudo systemctl start mcaptcha
``
```

View File

Before

Width:  |  Height:  |  Size: 665 B

After

Width:  |  Height:  |  Size: 665 B

View File

Before

Width:  |  Height:  |  Size: 628 B

After

Width:  |  Height:  |  Size: 628 B

View File

@@ -39,7 +39,7 @@
window.onload = function() { window.onload = function() {
// Begin Swagger UI call region // Begin Swagger UI call region
const ui = SwaggerUIBundle({ const ui = SwaggerUIBundle({
url: "/docs/openapi.yaml", url: "./openapi.json",
dom_id: '#swagger-ui', dom_id: '#swagger-ui',
deepLinking: true, deepLinking: true,
presets: [ presets: [

View File

@@ -1,4 +0,0 @@
./.idea
./node_modules/
./dist/
_build/

View File

@@ -1 +0,0 @@
extends: spectral:oas

View File

@@ -1,27 +0,0 @@
{
"name": "mcaptcha",
"version": "0.1.0",
"description": "mCaptcha CAPTCHA service's API",
"main": "index.js",
"scripts": {
"build": "swagger-cli bundle openapi.yaml --outfile dist/openapi.yaml --type yaml",
"test": "npm run build && spectral lint dist/openapi.yaml",
"serve": "npm run build && redoc-cli serve dist/openapi.yaml --port 7000 --options.onlyRequiredInSamples",
"html": "npm run build && redoc-cli bundle dist/openapi.yaml --output dist/index.html --options.onlyRequiredInSamples",
"clean": "rm -r dist"
},
"repository": {
"type": "git",
"url": "git+https://github.com/mCaptcha/mCaptcha.git"
},
"license": "AGPL3",
"bugs": {
"url": "https://github.com/mCaptcha/mCaptcha/issues"
},
"homepage": "https://github.com/mCaptcha/mCaptcha#readme",
"dependencies": {
"@apidevtools/swagger-cli": "^4.0.4",
"@stoplight/spectral": "^6.1.0",
"redoc-cli": "^0.13.0"
}
}

File diff suppressed because it is too large Load Diff

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

View File

@@ -1,190 +0,0 @@
/*
* For a detailed explanation regarding each configuration property and type check, visit:
* https://jestjs.io/docs/en/configuration.html
*/
export default {
// All imported modules in your tests should be mocked automatically
// automock: false,
// Stop running tests after `n` failures
// bail: 0,
// The directory where Jest should store its cached dependency information
// cacheDirectory: "/tmp/jest_rs",
// Automatically clear mock calls and instances between every test
clearMocks: true,
// Indicates whether the coverage information should be collected while executing the test
collectCoverage: true,
// An array of glob patterns indicating a set of files for which coverage information should be collected
// collectCoverageFrom: undefined,
// The directory where Jest should output its coverage files
coverageDirectory: 'coverage',
// An array of regexp pattern strings used to skip coverage collection
coveragePathIgnorePatterns: ['/node_modules/', 'setupTests.ts', 'setUpTests.ts'],
// Indicates which provider should be used to instrument code for coverage
coverageProvider: 'v8',
// A list of reporter names that Jest uses when writing coverage reports
// coverageReporters: [
// "json",
// "text",
// "lcov",
// "clover"
// ],
// An object that configures minimum threshold enforcement for coverage results
// coverageThreshold: undefined,
// A path to a custom dependency extractor
// dependencyExtractor: undefined,
// Make calling deprecated APIs throw helpful error messages
// errorOnDeprecated: false,
// Force coverage collection from ignored files using an array of glob patterns
// forceCoverageMatch: [],
// A path to a module which exports an async function that is triggered once before all test suites
// globalSetup: undefined,
// A path to a module which exports an async function that is triggered once after all test suites
// globalTeardown: undefined,
// A set of global variables that need to be available in all test environments
// globals: {},
// The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers.
// maxWorkers: "50%",
// An array of directory names to be searched recursively up from the requiring module's location
// moduleDirectories: [
// "node_modules"
// ],
// An array of file extensions your modules use
// moduleFileExtensions: [
// "js",
// "json",
// "jsx",
// "ts",
// "tsx",
// "node"
// ],
// A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module
// moduleNameMapper: {},
// An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader
// modulePathIgnorePatterns: [],
// Activates notifications for test results
// notify: false,
// An enum that specifies notification mode. Requires { notify: true }
// notifyMode: "failure-change",
// A preset that is used as a base for Jest's configuration
// preset: undefined,
// Run tests from one or more projects
// projects: undefined,
// Use this configuration option to add custom reporters to Jest
// reporters: undefined,
// Automatically reset mock state between every test
// resetMocks: false,
// Reset the module registry before running each individual test
// resetModules: false,
// A path to a custom resolver
// resolver: undefined,
// Automatically restore mock state between every test
// restoreMocks: false,
// The root directory that Jest should scan for tests and modules within
// rootDir: undefined,
// A list of paths to directories that Jest should use to search for files in
roots: ['templates/'],
// Allows you to use a custom runner instead of Jest's default test runner
// runner: "jest-runner",
// The paths to modules that run some code to configure or set up the testing environment before each test
// setupFiles: [],
// A list of paths to modules that run some code to configure or set up the testing framework before each test
// setupFilesAfterEnv: [],
// The number of seconds after which a test is considered as slow and reported as such in the results.
// slowTestThreshold: 5,
// A list of paths to snapshot serializer modules Jest should use for snapshot testing
// snapshotSerializers: [],
// The test environment that will be used for testing
testEnvironment: "jest-environment-jsdom",
// Options that will be passed to the testEnvironment
// testEnvironmentOptions: {},
// Adds a location field to test results
// testLocationInResults: false,
// The glob patterns Jest uses to detect test files
testMatch: ['**/__tests__/**/*.[jt]s?(x)', '**/?(*.)+(spec|test).[tj]s?(x)'],
// An array of regexp pattern strings that are matched against all test paths, matched tests are skipped
// testPathIgnorePatterns: [
// "/node_modules/"
// ],
// The regexp pattern or array of patterns that Jest uses to detect test files
// testRegex: [],
// This option allows the use of a custom results processor
// testResultsProcessor: undefined,
// This option allows use of a custom test runner
// testRunner: "jasmine2",
// This option sets the URL for the jsdom environment. It is reflected in properties such as location.href
testURL: 'http://localhost:7000/?sitekey=imbatman',
// Setting this value to "fake" allows the use of fake timers for functions such as "setTimeout"
// timers: "real",
// A map from regular expressions to paths to transformers
// transform: undefined,
transform: {
'^.+\\.(ts|tsx)$': 'ts-jest',
},
// An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation
// transformIgnorePatterns: [
// "/node_modules/",
// "\\.pnp\\.[^\\/]+$"
// ],
// An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them
// unmockedModulePathPatterns: undefined,
// Indicates whether each individual test should be reported during the run
// verbose: undefined,
// An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode
// watchPathIgnorePatterns: [],
// Whether to use watchman for file crawling
// watchman: true,
};

View File

@@ -1,7 +1,6 @@
CREATE TABLE IF NOT EXISTS mcaptcha_users ( CREATE TABLE IF NOT EXISTS mcaptcha_users (
name VARCHAR(100) NOT NULL UNIQUE, name VARCHAR(100) NOT NULL UNIQUE,
email VARCHAR(100) UNIQUE DEFAULT NULL, email VARCHAR(100) UNIQUE DEFAULT NULL,
email_verified BOOLEAN DEFAULT NULL,
secret varchar(50) NOT NULL UNIQUE, secret varchar(50) NOT NULL UNIQUE,
password TEXT NOT NULL, password TEXT NOT NULL,
ID SERIAL PRIMARY KEY NOT NULL ID SERIAL PRIMARY KEY NOT NULL

View File

@@ -2,6 +2,6 @@ CREATE TABLE IF NOT EXISTS mcaptcha_config (
config_id SERIAL PRIMARY KEY NOT NULL, config_id SERIAL PRIMARY KEY NOT NULL,
user_id INTEGER NOT NULL references mcaptcha_users(ID) ON DELETE CASCADE, user_id INTEGER NOT NULL references mcaptcha_users(ID) ON DELETE CASCADE,
key varchar(100) NOT NULL UNIQUE, key varchar(100) NOT NULL UNIQUE,
name varchar(100) NOT NULL, name varchar(100) DEFAULT NULL,
duration integer NOT NULL DEFAULT 30 duration integer NOT NULL DEFAULT 30
); );

View File

@@ -1,43 +1,33 @@
{ {
"name": "vanilla", "name": "frontend",
"version": "0.1.0",
"description": "mCaptcha/guard frontend",
"main": "index.js", "main": "index.js",
"version": "1.0.0", "repository": "https://github.com/mCaptcha/guard",
"license": "AGPL-3.0", "author": "Aravinth Manivannan <realaravinth@batsense.net>",
"license": "AGPLv3 or above",
"scripts": { "scripts": {
"build": "webpack --mode production", "start": "webpack --config webpack.dev.js",
"lint": "yarn run eslint templates", "build": "webpack --config webpack.prod.js"
"start": "webpack-dev-server --mode development --progress --color",
"test": "jest"
}, },
"private": true,
"devDependencies": { "devDependencies": {
"@types/jest": "^27.0.2", "css-loader": "^2.1.0",
"@types/jsdom": "^16.2.10", "file-loader": "^3.0.1",
"@types/node": "^16.10.4", "html-loader": "^0.5.5",
"@types/sinon": "^10.0.0", "html-webpack-plugin": "^3.2.0",
"@typescript-eslint/eslint-plugin": "^5.0.0", "mini-css-extract-plugin": "^0.5.0",
"@typescript-eslint/parser": "^5.0.0", "node-sass": "^4.11.0",
"@wasm-tool/wasm-pack-plugin": "^1.4.0", "optimize-css-assets-webpack-plugin": "^5.0.1",
"css-loader": "^6.4.0", "sass-loader": "^7.1.0",
"css-minimizer-webpack-plugin": "^3.1.1", "style-loader": "^0.23.1",
"sass": "^1.25.0", "webpack": "^4.29.6",
"eslint": "^8.0.0", "webpack-cli": "^3.2.3",
"jest": "^27.2.5", "webpack-dev-server": "^3.2.1",
"jest-fetch-mock": "^3.0.3", "webpack-merge": "^4.2.1"
"jsdom": "^18.0.0",
"mini-css-extract-plugin": "^2.4.2",
"sass-loader": "^12.2.0",
"sinon": "^11.1.2",
"ts-jest": "^27.0.5",
"ts-loader": "^9.2.6",
"ts-node": "^10.3.0",
"typescript": "^4.1.0",
"webpack": "^5.0.0",
"webpack-cli": "^4.6.0",
"webpack-dev-server": "^4.3.1"
}, },
"dependencies": { "dependencies": {
"@mcaptcha/pow-wasm": "^0.1.0-alpha-1", "clean-webpack-plugin": "^2.0.0"
"@mcaptcha/pow_sha256-polyfill": "^0.1.0-alpha-1",
"@mcaptcha/vanilla-glue": "^0.1.0-alpha-1"
} }
} }

View File

@@ -1 +0,0 @@
max_width = 89

View File

@@ -1,6 +0,0 @@
template_dirs = ["templates"]
#escape = true
delimiter = "."
[optimizations]
rm_whitespace = true

1
sailfish.yml Normal file
View File

@@ -0,0 +1 @@
delimiter: "."

View File

@@ -1,41 +0,0 @@
#!/bin/bash
set -Eeuo pipefail
trap cleanup SIGINT SIGTERM ERR EXIT
readonly PROJECT_ROOT=$(realpath $(dirname $(dirname "${BASH_SOURCE[0]}")))
source $PROJECT_ROOT/scripts/lib.sh
readonly DIST=$PROJECT_ROOT/static/cache/bundle/
file_extension() {
echo $1 | rev | tr
}
cache_bust(){
name=$(get_file_name $1)
extension="${name##*.}"
filename="${name%.*}"
file_hash=$(sha256sum $1 | cut -d " " -f 1 | tr "[:lower:]" "[:upper:]")
msg "${GREEN}- Processing $name: $filename.$file_hash.$extension"
sed -i \
"s/$name/assets\/bundle\/$filename.$file_hash.$extension/" \
$(find $DIST -type f -a -name "*.js")
}
setup_colors
msg "${BLUE}[*] Setting up files for cache busting"
for file in $(find $DIST -type f -a -name "*.js")
do
name=$(get_file_name $file)
case $name in
"bench.js")
cache_bust $file
;;
esac
done

View File

@@ -1,22 +0,0 @@
#!/bin/bash
cleanup() {
trap - SIGINT SIGTERM ERR EXIT
# script cleanup here
}
setup_colors() {
if [[ -t 2 ]] && [[ -z "${NO_COLOR-}" ]] && [[ "${TERM-}" != "dumb" ]]; then
NOCOLOR='\033[0m' RED='\033[0;31m' GREEN='\033[0;32m' ORANGE='\033[0;33m' BLUE='\033[0;34m' PURPLE='\033[0;35m' CYAN='\033[0;36m' YELLOW='\033[1;33m'
else
NOCOLOR='' RED='' GREEN='' ORANGE='' BLUE='' PURPLE='' CYAN='' YELLOW=''
fi
}
msg() {
echo >&2 -e "${1-}"
}
get_file_name() {
basename -- $1
}

View File

@@ -1,62 +0,0 @@
#!/bin/bash
set -Eeuo pipefail
trap cleanup SIGINT SIGTERM ERR EXIT
readonly PROJECT_ROOT=$(realpath $(dirname $(dirname "${BASH_SOURCE[0]}")))
readonly DIST=$PROJECT_ROOT/static/cache/bundle/
readonly SOURCE="// @source https://github.com/mCaptcha/mCaptcha"
readonly LICENSE_END="// @license-end"
source $PROJECT_ROOT/scripts/lib.sh
print_license_msg() {
msg "${GREEN}- Applying $1 on $(get_file_name $2)"
}
apply_agpl() {
print_license_msg "AGPL" $1
local AGPL="// @license magnet:?xt=urn:btih:0b31508aeb0634b347b8270c7bee4d411b5d4109&dn=agpl-3.0.txt AGPL-3.0"
echo $AGPL >> $1
}
apply_x11() {
print_license_msg "X11" $1
local MIT="// @license magnet:?xt=urn:btih:5305d91886084f776adcf57509a648432709a7c7&dn=x11.txt X11"
echo $MIT >> $1
}
apply_apache() {
print_license_msg "APACHE" $1
local APACHE="// @license magnet:?xt=urn:btih:8e4f440f4c65981c5bf93c76d35135ba5064d8b7&dn=apache-2.0.txt Apache-2.0"
echo $APACHE >> $1
}
setup_colors
msg "${BLUE}[*] LibreJS processor running"
for file in $(find $DIST -type f -a -name "*.js")
do
contents=$(cat $file)
: > $file
name=$(get_file_name $file)
case $name in
"bundle.js")
apply_agpl $file
;;
"verificationWidget.js" | "bench.js")
apply_x11 $file
apply_apache $file
;;
*)
msg "${RED}- [!] License not configured for $name. Applying default license"
apply_agpl $file
;;
esac
echo $SOURCE >> $file
echo $contents >> $file
echo $LICENSE_END >> $file
done

View File

@@ -1,50 +0,0 @@
#!/bin/bash
# hotfix for DB error: too many connections, can't create new client
#
# 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.
set -Eeuo pipefail
for ut in \
api::v1::meta::tests::build_details_works \
api::v1::mcaptcha::easy::tests::isoloated_test::easy_configuration_works \
api::v1::meta::tests::health_works \
api::v1::pow::tests::scope_pow_works \
api::v1::account::test::uname_email_exists_works \
api::v1::mcaptcha::easy::tests::easy_works \
api::v1::pow::get_config::tests::get_pow_config_works \
api::v1::pow::verify_pow::tests::verify_pow_works \
api::v1::mcaptcha::update::tests::update_and_get_mcaptcha_works \
date::tests::print_date_test \
api::v1::tests::auth::serverside_password_validation_works \
docs::tests::docs_works \
email::verification::tests::email_verification_works \
errors::tests::error_works \
pages::errors::tests::error_pages_work \
pages::panel::notifications::tests::print_date_test \
api::v1::notifications::add::tests::notification_works \
api::v1::account::test::username_update_works \
pages::panel::sitekey::tests::get_sitekey_routes_work \
api::v1::mcaptcha::test::level_routes_work \
pages::routes::tests::sitemap_works \
api::v1::tests::protected::protected_routes_work \
pages::tests::public_pages_tempaltes_work \
static_assets::filemap::tests::filemap_works \
static_assets::static_files::tests::favicons_work \
static_assets::static_files::tests::static_assets_work \
pages::tests::protected_pages_templates_work \
test::version_source_code_url_works \
widget::test::captcha_widget_route_works \
pages::panel::sitekey::edit::test::edit_sitekey_work \
api::v1::pow::verify_token::tests::validate_captcha_token_works \
api::v1::notifications::get::tests::notification_get_works \
api::v1::notifications::mark_read::tests::notification_mark_read_works \
api::v1::account::test::email_udpate_password_validation_del_userworks \
api::v1::tests::auth::auth_works \
pages::panel::sitekey::view::test::view_sitekey_work \
api::v1::account::password::tests::update_password_works \
pages::panel::sitekey::list::test::list_sitekeys_work
do
cargo test -- $ut
done

View File

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

View File

@@ -1,18 +1,18 @@
/* /*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net> * Copyright (C) 2021 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,64 +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 actix_identity::Identity;
use actix_web::{web, HttpResponse, Responder};
use super::auth::runners::Password;
use crate::errors::*;
use crate::AppData;
#[my_codegen::post(
path = "crate::V1_API_ROUTES.account.delete",
wrap = "crate::api::v1::get_middleware()"
)]
pub async fn delete_account(
id: Identity,
payload: web::Json<Password>,
data: AppData,
) -> ServiceResult<impl Responder> {
use argon2_creds::Config;
let username = id.identity().unwrap();
let hash = data
.db
.get_password(&db_core::Login::Username(&username))
.await?;
if Config::verify(&hash.hash, &payload.password)? {
runners::delete_user(&username, &data).await?;
id.forget();
Ok(HttpResponse::Ok())
} else {
Err(ServiceError::WrongPassword)
}
}
pub mod runners {
use super::*;
pub async fn delete_user(name: &str, data: &AppData) -> ServiceResult<()> {
data.db.delete_user(name).await?;
Ok(())
}
}
pub fn services(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(delete_account);
}

View File

@@ -1,70 +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 actix_identity::Identity;
use actix_web::{web, HttpResponse, Responder};
use db_core::UpdateEmail;
use serde::{Deserialize, Serialize};
use super::{AccountCheckPayload, AccountCheckResp};
use crate::errors::*;
use crate::AppData;
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Email {
pub email: String,
}
#[my_codegen::post(path = "crate::V1_API_ROUTES.account.email_exists")]
pub async fn email_exists(
payload: web::Json<AccountCheckPayload>,
data: AppData,
) -> ServiceResult<impl Responder> {
let exists = data.db.email_exists(&payload.val).await?;
let resp = AccountCheckResp { exists };
Ok(HttpResponse::Ok().json(resp))
}
/// update email
#[my_codegen::post(
path = "crate::V1_API_ROUTES.account.update_email",
wrap = "crate::api::v1::get_middleware()"
)]
async fn set_email(
id: Identity,
payload: web::Json<Email>,
data: AppData,
) -> ServiceResult<impl Responder> {
let username = id.identity().unwrap();
data.creds.email(&payload.email)?;
let update_email = UpdateEmail {
username: &username,
new_email: &payload.email,
};
data.db.update_email(&update_email).await?;
Ok(HttpResponse::Ok())
}
pub fn services(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(email_exists);
cfg.service(set_email);
}

View File

@@ -1,84 +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 serde::{Deserialize, Serialize};
pub mod delete;
pub mod email;
pub mod password;
pub mod secret;
#[cfg(test)]
pub mod test;
pub mod username;
pub use super::auth;
pub use super::mcaptcha;
pub mod routes {
pub struct Account {
pub delete: &'static str,
pub email_exists: &'static str,
pub get_secret: &'static str,
pub update_email: &'static str,
pub update_password: &'static str,
pub update_secret: &'static str,
pub username_exists: &'static str,
pub update_username: &'static str,
}
impl Account {
pub const fn new() -> Account {
let get_secret = "/api/v1/account/secret/get";
let update_secret = "/api/v1/account/secret/update";
let delete = "/api/v1/account/delete";
let email_exists = "/api/v1/account/email/exists";
let username_exists = "/api/v1/account/username/exists";
let update_username = "/api/v1/account/username/update";
let update_email = "/api/v1/account/email/update";
let update_password = "/api/v1/account/password/update";
Account {
delete,
email_exists,
get_secret,
update_email,
update_password,
update_secret,
username_exists,
update_username,
}
}
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct AccountCheckPayload {
pub val: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct AccountCheckResp {
pub exists: bool,
}
pub fn services(cfg: &mut actix_web::web::ServiceConfig) {
delete::services(cfg);
email::services(cfg);
username::services(cfg);
secret::services(cfg);
password::services(cfg);
}

View File

@@ -1,192 +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 actix_identity::Identity;
use actix_web::{web, HttpResponse, Responder};
use argon2_creds::Config;
use db_core::Login;
use serde::{Deserialize, Serialize};
use crate::errors::*;
use crate::*;
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct ChangePasswordReqest {
pub password: String,
pub new_password: String,
pub confirm_new_password: String,
}
pub struct UpdatePassword {
pub new_password: String,
pub confirm_new_password: String,
}
impl From<ChangePasswordReqest> for UpdatePassword {
fn from(s: ChangePasswordReqest) -> Self {
UpdatePassword {
new_password: s.new_password,
confirm_new_password: s.confirm_new_password,
}
}
}
async fn update_password_runner(
user: &str,
update: UpdatePassword,
data: &Data,
) -> ServiceResult<()> {
if update.new_password != update.confirm_new_password {
return Err(ServiceError::PasswordsDontMatch);
}
let new_hash = data.creds.password(&update.new_password)?;
let p = db_core::NameHash {
username: user.to_owned(),
hash: new_hash,
};
data.db.update_password(&p).await?;
Ok(())
}
#[my_codegen::post(
path = "crate::V1_API_ROUTES.account.update_password",
wrap = "crate::api::v1::get_middleware()"
)]
async fn update_user_password(
id: Identity,
data: AppData,
payload: web::Json<ChangePasswordReqest>,
) -> ServiceResult<impl Responder> {
if payload.new_password != payload.confirm_new_password {
return Err(ServiceError::PasswordsDontMatch);
}
let username = id.identity().unwrap();
// TODO: verify behavior when account is not found
let res = data.db.get_password(&Login::Username(&username)).await?;
if Config::verify(&res.hash, &payload.password)? {
let update: UpdatePassword = payload.into_inner().into();
update_password_runner(&username, update, &data).await?;
Ok(HttpResponse::Ok())
} else {
Err(ServiceError::WrongPassword)
}
}
pub fn services(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(update_user_password);
}
#[cfg(test)]
pub mod tests {
use super::*;
use actix_web::http::StatusCode;
use actix_web::test;
use crate::api::v1::ROUTES;
use crate::tests::*;
#[actix_rt::test]
pub async fn update_password_works() {
const NAME: &str = "updatepassuser";
const PASSWORD: &str = "longpassword2";
const EMAIL: &str = "updatepassuser@a.com";
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 cookies = get_cookie!(signin_resp);
let app = get_app!(data).await;
let new_password = "newpassword";
let update_password = ChangePasswordReqest {
password: PASSWORD.into(),
new_password: new_password.into(),
confirm_new_password: PASSWORD.into(),
};
let res = update_password_runner(NAME, update_password.into(), data).await;
assert!(res.is_err());
assert_eq!(res, Err(ServiceError::PasswordsDontMatch));
let update_password = ChangePasswordReqest {
password: PASSWORD.into(),
new_password: new_password.into(),
confirm_new_password: new_password.into(),
};
assert!(update_password_runner(NAME, update_password.into(), data)
.await
.is_ok());
let update_password = ChangePasswordReqest {
password: new_password.into(),
new_password: new_password.into(),
confirm_new_password: PASSWORD.into(),
};
bad_post_req_test(
data,
NAME,
new_password,
ROUTES.account.update_password,
&update_password,
ServiceError::PasswordsDontMatch,
)
.await;
let update_password = ChangePasswordReqest {
password: PASSWORD.into(),
new_password: PASSWORD.into(),
confirm_new_password: PASSWORD.into(),
};
bad_post_req_test(
data,
NAME,
new_password,
ROUTES.account.update_password,
&update_password,
ServiceError::WrongPassword,
)
.await;
let update_password = ChangePasswordReqest {
password: new_password.into(),
new_password: PASSWORD.into(),
confirm_new_password: PASSWORD.into(),
};
let update_password_resp = test::call_service(
&app,
post_request!(&update_password, ROUTES.account.update_password)
.cookie(cookies)
.to_request(),
)
.await;
assert_eq!(update_password_resp.status(), StatusCode::OK);
}
}

View File

@@ -1,63 +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 actix_identity::Identity;
use actix_web::{HttpResponse, Responder};
use db_core::prelude::*;
use crate::api::v1::mcaptcha::get_random;
use crate::errors::*;
use crate::AppData;
#[my_codegen::get(
path = "crate::V1_API_ROUTES.account.get_secret",
wrap = "crate::api::v1::get_middleware()"
)]
async fn get_secret(id: Identity, data: AppData) -> ServiceResult<impl Responder> {
let username = id.identity().unwrap();
let secret = data.db.get_secret(&username).await?;
Ok(HttpResponse::Ok().json(secret))
}
#[my_codegen::post(
path = "crate::V1_API_ROUTES.account.update_secret",
wrap = "crate::api::v1::get_middleware()"
)]
async fn update_user_secret(
id: Identity,
data: AppData,
) -> ServiceResult<impl Responder> {
let username = id.identity().unwrap();
let mut secret;
loop {
secret = get_random(32);
match data.db.update_secret(&username, &secret).await {
Ok(_) => break,
Err(DBError::SecretTaken) => continue,
Err(e) => return Err(e.into()),
}
}
Ok(HttpResponse::Ok())
}
pub fn services(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(get_secret);
cfg.service(update_user_secret);
}

View File

@@ -1,247 +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 actix_web::http::StatusCode;
use actix_web::test;
use super::email::*;
use super::username::Username;
use super::*;
use crate::api::v1::auth::runners::Password;
use crate::api::v1::ROUTES;
use crate::*;
use crate::errors::*;
use crate::tests::*;
#[actix_rt::test]
pub async fn uname_email_exists_works() {
const NAME: &str = "testuserexists";
const PASSWORD: &str = "longpassword2";
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 cookies = get_cookie!(signin_resp);
let app = get_app!(data).await;
// chech if get user secret works
let resp = test::call_service(
&app,
test::TestRequest::get()
.cookie(cookies.clone())
.uri(ROUTES.account.get_secret)
.to_request(),
)
.await;
assert_eq!(resp.status(), StatusCode::OK);
// chech if get user secret works
let resp = test::call_service(
&app,
test::TestRequest::post()
.cookie(cookies.clone())
.uri(ROUTES.account.update_secret)
.to_request(),
)
.await;
assert_eq!(resp.status(), StatusCode::OK);
let mut payload = AccountCheckPayload { val: NAME.into() };
let user_exists_resp = test::call_service(
&app,
post_request!(&payload, ROUTES.account.username_exists)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(user_exists_resp.status(), StatusCode::OK);
let mut resp: AccountCheckResp = test::read_body_json(user_exists_resp).await;
assert!(resp.exists);
payload.val = PASSWORD.into();
let user_doesnt_exist = test::call_service(
&app,
post_request!(&payload, ROUTES.account.username_exists)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(user_doesnt_exist.status(), StatusCode::OK);
resp = test::read_body_json(user_doesnt_exist).await;
assert!(!resp.exists);
let email_doesnt_exist = test::call_service(
&app,
post_request!(&payload, ROUTES.account.email_exists)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(email_doesnt_exist.status(), StatusCode::OK);
resp = test::read_body_json(email_doesnt_exist).await;
assert!(!resp.exists);
payload.val = EMAIL.into();
let email_exist = test::call_service(
&app,
post_request!(&payload, ROUTES.account.email_exists)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(email_exist.status(), StatusCode::OK);
resp = test::read_body_json(email_exist).await;
assert!(resp.exists);
}
#[actix_rt::test]
pub async fn email_udpate_password_validation_del_userworks() {
const NAME: &str = "testuser2";
const PASSWORD: &str = "longpassword2";
const EMAIL: &str = "testuser1@a.com2";
const NAME2: &str = "eupdauser";
const EMAIL2: &str = "eupdauser@a.com";
let data = get_data().await;
let data = &data;
delete_user(data, NAME).await;
delete_user(data, NAME2).await;
let _ = register_and_signin(data, NAME2, EMAIL2, PASSWORD).await;
let (_creds, signin_resp) = register_and_signin(data, NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
let app = get_app!(data).await;
// update email
let mut email_payload = Email {
email: EMAIL.into(),
};
let email_update_resp = test::call_service(
&app,
post_request!(&email_payload, ROUTES.account.update_email)
//post_request!(&email_payload, EMAIL_UPDATE)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(email_update_resp.status(), StatusCode::OK);
// check duplicate email while duplicate email
email_payload.email = EMAIL2.into();
bad_post_req_test(
data,
NAME,
PASSWORD,
ROUTES.account.update_email,
&email_payload,
ServiceError::EmailTaken,
)
.await;
// wrong password while deleteing account
let mut payload = Password {
password: NAME.into(),
};
bad_post_req_test(
data,
NAME,
PASSWORD,
ROUTES.account.delete,
&payload,
ServiceError::WrongPassword,
)
.await;
// delete account
payload.password = PASSWORD.into();
let delete_user_resp = test::call_service(
&app,
post_request!(&payload, ROUTES.account.delete)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(delete_user_resp.status(), StatusCode::OK);
// try to delete an account that doesn't exist
let account_not_found_resp = test::call_service(
&app,
post_request!(&payload, ROUTES.account.delete)
.cookie(cookies)
.to_request(),
)
.await;
assert_eq!(account_not_found_resp.status(), StatusCode::NOT_FOUND);
let txt: ErrorToResponse = test::read_body_json(account_not_found_resp).await;
assert_eq!(txt.error, format!("{}", ServiceError::AccountNotFound));
}
#[actix_rt::test]
pub async fn username_update_works() {
const NAME: &str = "testuserupda";
const EMAIL: &str = "testuserupda@sss.com";
const EMAIL2: &str = "testuserupda2@sss.com";
const PASSWORD: &str = "longpassword2";
const NAME2: &str = "terstusrtds";
const NAME_CHANGE: &str = "terstusrtdsxx";
let data = get_data().await;
let data = &data;
futures::join!(
delete_user(data, NAME),
delete_user(data, NAME2),
delete_user(data, NAME_CHANGE),
);
let _ = register_and_signin(data, NAME2, EMAIL2, PASSWORD).await;
let (_creds, signin_resp) = register_and_signin(data, NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
let app = get_app!(data).await;
// update username
let mut username_udpate = Username {
username: NAME_CHANGE.into(),
};
let username_update_resp = test::call_service(
&app,
post_request!(&username_udpate, ROUTES.account.update_username)
.cookie(cookies)
.to_request(),
)
.await;
assert_eq!(username_update_resp.status(), StatusCode::OK);
// check duplicate username with duplicate username
username_udpate.username = NAME2.into();
bad_post_req_test(
data,
NAME_CHANGE,
PASSWORD,
ROUTES.account.update_username,
&username_udpate,
ServiceError::UsernameTaken,
)
.await;
}

View File

@@ -1,77 +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 actix_identity::Identity;
use actix_web::{web, HttpResponse, Responder};
use serde::{Deserialize, Serialize};
use super::{AccountCheckPayload, AccountCheckResp};
use crate::errors::*;
use crate::AppData;
#[my_codegen::post(path = "crate::V1_API_ROUTES.account.username_exists")]
async fn username_exists(
payload: web::Json<AccountCheckPayload>,
data: AppData,
) -> ServiceResult<impl Responder> {
let resp = runners::username_exists(&payload, &data).await?;
Ok(HttpResponse::Ok().json(resp))
}
pub mod runners {
use super::*;
pub async fn username_exists(
payload: &AccountCheckPayload,
data: &AppData,
) -> ServiceResult<AccountCheckResp> {
let exists = data.db.username_exists(&payload.val).await?;
Ok(AccountCheckResp { exists })
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Username {
pub username: String,
}
/// update username
#[my_codegen::post(
path = "crate::V1_API_ROUTES.account.update_username",
wrap = "crate::api::v1::get_middleware()"
)]
async fn set_username(
id: Identity,
payload: web::Json<Username>,
data: AppData,
) -> ServiceResult<impl Responder> {
let username = id.identity().unwrap();
let processed_uname = data.creds.username(&payload.username)?;
data.db.update_username(&username, &processed_uname).await?;
id.forget();
id.remember(processed_uname);
Ok(HttpResponse::Ok())
}
pub fn services(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(username_exists);
cfg.service(set_username);
}

View File

@@ -1,201 +1,317 @@
/* /*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net> * Copyright (C) 2021 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::http::header; use actix_web::{get, post, web, HttpResponse, Responder};
use actix_web::{web, HttpResponse, Responder}; use log::debug;
use db_core::errors::DBError;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use super::mcaptcha::get_random; use super::mcaptcha::get_random;
use crate::errors::*; use crate::errors::*;
use crate::AppData; use crate::Data;
pub mod routes { #[derive(Clone, Debug, Deserialize, Serialize)]
use actix_auth_middleware::GetLoginRoute; pub struct Register {
pub username: String,
pub struct Auth { pub password: String,
pub logout: &'static str, pub email: Option<String>,
pub login: &'static str,
pub register: &'static str,
}
impl Auth {
pub const fn new() -> Auth {
let login = "/api/v1/signin";
let logout = "/logout";
let register = "/api/v1/signup";
Auth {
logout,
login,
register,
}
}
}
impl GetLoginRoute for Auth {
fn get_login_route(&self, src: Option<&str>) -> String {
if let Some(redirect_to) = src {
format!(
"{}?redirect_to={}",
self.login,
urlencoding::encode(redirect_to)
)
} else {
self.login.to_string()
}
}
}
} }
pub mod runners { #[derive(Clone, Debug, Deserialize, Serialize)]
use super::*; pub struct Login {
pub username: String,
#[derive(Clone, Debug, Deserialize, Serialize)] pub password: String,
pub struct Register {
pub username: String,
pub password: String,
pub confirm_password: String,
pub email: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Login {
// login accepts both username and email under "username field"
// TODO update all instances where login is used
pub login: String,
pub password: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Password {
pub password: String,
}
/// returns Ok(()) when everything checks out and the user is authenticated. Erros otherwise
pub async fn login_runner(payload: Login, data: &AppData) -> ServiceResult<String> {
use argon2_creds::Config;
let verify = |stored: &str, received: &str| {
if Config::verify(stored, received)? {
Ok(())
} else {
Err(ServiceError::WrongPassword)
}
};
let s = if payload.login.contains('@') {
data.db
.get_password(&db_core::Login::Email(&payload.login))
.await?
} else {
data.db
.get_password(&db_core::Login::Username(&payload.login))
.await?
};
verify(&s.hash, &payload.password)?;
Ok(s.username)
}
pub async fn register_runner(
payload: &Register,
data: &AppData,
) -> ServiceResult<()> {
if !data.settings.allow_registration {
return Err(ServiceError::ClosedForRegistration);
}
if payload.password != payload.confirm_password {
return Err(ServiceError::PasswordsDontMatch);
}
let username = data.creds.username(&payload.username)?;
let hash = data.creds.password(&payload.password)?;
if let Some(email) = &payload.email {
data.creds.email(email)?;
}
let mut secret;
loop {
secret = get_random(32);
let p = db_core::Register {
username: &username,
hash: &hash,
email: payload.email.as_deref(),
secret: &secret,
};
match data.db.register(&p).await {
Ok(_) => break,
Err(DBError::SecretTaken) => continue,
Err(e) => return Err(e.into()),
}
}
Ok(())
}
} }
pub fn services(cfg: &mut web::ServiceConfig) { #[derive(Clone, Debug, Deserialize, Serialize)]
cfg.service(register); pub struct Password {
cfg.service(login); pub password: String,
cfg.service(signout);
} }
#[my_codegen::post(path = "crate::V1_API_ROUTES.auth.register")]
async fn register( #[post("/api/v1/signup")]
payload: web::Json<runners::Register>, pub async fn signup(
data: AppData, payload: web::Json<Register>,
data: web::Data<Data>,
) -> ServiceResult<impl Responder> { ) -> ServiceResult<impl Responder> {
runners::register_runner(&payload, &data).await?; if !crate::SETTINGS.server.allow_registration {
Err(ServiceError::ClosedForRegistration)?
}
let username = data.creds.username(&payload.username)?;
let hash = data.creds.password(&payload.password)?;
// let payload = payload.into_inner();
// let email = payload.email.clone();
// if payload.email.is_some() {
// let email = email.clone().unwrap();
// data.creds.email(Some(&email))?;
// }
if let Some(email) = &payload.email {
data.creds.email(Some(&email))?;
}
let mut secret;
loop {
secret = get_random(32);
let res;
if let Some(email) = &payload.email {
res = sqlx::query!(
"INSERT INTO mcaptcha_users
(name , password, email, secret) VALUES ($1, $2, $3, $4)",
&username,
&hash,
&email,
&secret,
)
.execute(&data.db)
.await;
} else {
res = sqlx::query!(
"INSERT INTO mcaptcha_users
(name , password, secret) VALUES ($1, $2, $3)",
&username,
&hash,
&secret,
)
.execute(&data.db)
.await;
}
if res.is_ok() {
break;
} else {
if let Err(sqlx::Error::Database(err)) = res {
if err.code() == Some(Cow::from("23505")) {
let msg = err.message();
if msg.contains("mcaptcha_users_name_key") {
Err(ServiceError::UsernameTaken)?;
} else if msg.contains("mcaptcha_users_secret_key") {
continue;
} else {
Err(ServiceError::InternalServerError)?;
}
} else {
Err(sqlx::Error::Database(err))?;
}
};
}
}
Ok(HttpResponse::Ok()) Ok(HttpResponse::Ok())
} }
#[my_codegen::post(path = "crate::V1_API_ROUTES.auth.login")] #[post("/api/v1/signin")]
async fn login( pub async fn signin(
id: Identity, id: Identity,
payload: web::Json<runners::Login>, payload: web::Json<Login>,
query: web::Query<super::RedirectQuery>, data: web::Data<Data>,
data: AppData,
) -> ServiceResult<impl Responder> { ) -> ServiceResult<impl Responder> {
let username = runners::login_runner(payload.into_inner(), &data).await?; use argon2_creds::Config;
id.remember(username); use sqlx::Error::RowNotFound;
// Ok(HttpResponse::Ok())
let query = query.into_inner(); let rec = sqlx::query_as!(
if let Some(redirect_to) = query.redirect_to { Password,
Ok(HttpResponse::Found() r#"SELECT password FROM mcaptcha_users WHERE name = ($1)"#,
.append_header((header::LOCATION, redirect_to)) &payload.username,
.finish()) )
} else { .fetch_one(&data.db)
Ok(HttpResponse::Ok().finish()) .await;
match rec {
Ok(s) => {
if Config::verify(&s.password, &payload.password)? {
debug!("remembered {}", payload.username);
id.remember(payload.into_inner().username);
Ok(HttpResponse::Ok())
} else {
Err(ServiceError::WrongPassword)
}
}
Err(RowNotFound) => return Err(ServiceError::UsernameNotFound),
Err(_) => return Err(ServiceError::InternalServerError)?,
} }
} }
#[my_codegen::get( #[derive(Clone, Debug, Deserialize, Serialize)]
path = "crate::V1_API_ROUTES.auth.logout", pub struct Secret {
wrap = "crate::api::v1::get_middleware()" pub secret: String,
)] }
async fn signout(id: Identity) -> impl Responder {
if id.identity().is_some() { #[get("/api/v1/account/secret/")]
pub async fn get_secret(id: Identity, data: web::Data<Data>) -> ServiceResult<impl Responder> {
is_authenticated(&id)?;
let username = id.identity().unwrap();
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))
}
#[post("/api/v1/account/secret/")]
pub async fn update_user_secret(
id: Identity,
data: web::Data<Data>,
) -> ServiceResult<impl Responder> {
is_authenticated(&id)?;
let username = id.identity().unwrap();
let mut secret;
loop {
secret = get_random(32);
let res = sqlx::query!(
"UPDATE mcaptcha_users set secret = $1
WHERE name = $2",
&secret,
&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 {
Err(sqlx::Error::Database(err))?;
}
};
}
}
Ok(HttpResponse::Ok())
}
#[post("/api/v1/signout")]
pub async fn signout(id: Identity) -> impl Responder {
if let Some(_) = id.identity() {
id.forget(); id.forget();
} }
HttpResponse::Found() HttpResponse::Ok()
.append_header((header::LOCATION, crate::PAGES.auth.login)) }
.finish()
/// Check if user is authenticated
// TODO use middleware
pub fn is_authenticated(id: &Identity) -> ServiceResult<()> {
// access request identity
id.identity().ok_or(ServiceError::AuthorizationRequired)?;
Ok(())
}
#[post("/api/v1/account/delete")]
pub async fn delete_account(
id: Identity,
payload: web::Json<Password>,
data: web::Data<Data>,
) -> ServiceResult<impl Responder> {
use argon2_creds::Config;
use sqlx::Error::RowNotFound;
is_authenticated(&id)?;
let username = id.identity().unwrap();
let rec = sqlx::query_as!(
Password,
r#"SELECT password FROM mcaptcha_users WHERE name = ($1)"#,
&username,
)
.fetch_one(&data.db)
.await;
id.forget();
match rec {
Ok(s) => {
if Config::verify(&s.password, &payload.password)? {
sqlx::query!("DELETE FROM mcaptcha_users WHERE name = ($1)", &username)
.execute(&data.db)
.await?;
Ok(HttpResponse::Ok())
} else {
Err(ServiceError::WrongPassword)
}
}
Err(RowNotFound) => return Err(ServiceError::UsernameNotFound),
Err(_) => return Err(ServiceError::InternalServerError)?,
}
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct AccountCheckPayload {
pub val: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct AccountCheckResp {
pub exists: bool,
}
#[post("/api/v1/account/username/exists")]
pub async fn username_exists(
payload: web::Json<AccountCheckPayload>,
data: web::Data<Data>,
) -> ServiceResult<impl Responder> {
let res = sqlx::query!(
"SELECT EXISTS (SELECT 1 from mcaptcha_users WHERE name = $1)",
&payload.val,
)
.fetch_one(&data.db)
.await?;
let mut resp = AccountCheckResp { exists: false };
if let Some(x) = res.exists {
if x {
resp.exists = true;
}
}
Ok(HttpResponse::Ok().json(resp))
}
#[post("/api/v1/account/email/exists")]
pub async fn email_exists(
payload: web::Json<AccountCheckPayload>,
data: web::Data<Data>,
) -> ServiceResult<impl Responder> {
let res = sqlx::query!(
"SELECT EXISTS (SELECT 1 from mcaptcha_users WHERE email = $1)",
&payload.val,
)
.fetch_one(&data.db)
.await?;
let mut resp = AccountCheckResp { exists: false };
if let Some(x) = res.exists {
if x {
resp.exists = true;
}
}
Ok(HttpResponse::Ok().json(resp))
} }

View File

@@ -1,99 +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 actix_identity::Identity;
use actix_web::{web, HttpResponse, Responder};
use libmcaptcha::defense::Level;
use serde::{Deserialize, Serialize};
use db_core::errors::DBError;
use db_core::CreateCaptcha as DBCreateCaptcha;
use super::get_random;
use crate::errors::*;
use crate::AppData;
#[derive(Serialize, Deserialize)]
pub struct CreateCaptcha {
pub levels: Vec<Level>,
pub duration: u32,
pub description: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct MCaptchaDetails {
pub name: String,
pub key: String,
}
// TODO redo mcaptcha table to include levels as json field
// so that the whole thing can be added/udpaed in a single stroke
#[my_codegen::post(
path = "crate::V1_API_ROUTES.captcha.create",
wrap = "crate::api::v1::get_middleware()"
)]
pub async fn create(
payload: web::Json<CreateCaptcha>,
data: AppData,
id: Identity,
) -> ServiceResult<impl Responder> {
let username = id.identity().unwrap();
let mcaptcha_config = runner::create(&payload, &data, &username).await?;
Ok(HttpResponse::Ok().json(mcaptcha_config))
}
pub mod runner {
use super::*;
use libmcaptcha::DefenseBuilder;
pub async fn create(
payload: &CreateCaptcha,
data: &AppData,
username: &str,
) -> ServiceResult<MCaptchaDetails> {
let mut defense = DefenseBuilder::default();
for level in payload.levels.iter() {
defense.add_level(*level)?;
}
defense.build()?;
let mut key;
let duration = payload.duration as i32;
loop {
key = get_random(32);
let p = DBCreateCaptcha {
description: &payload.description,
key: &key,
duration,
};
match data.db.create_captcha(username, &p).await {
Ok(_) => break,
Err(DBError::SecretTaken) => continue,
Err(e) => return Err(e.into()),
}
}
data.db
.add_captcha_levels(username, &key, &payload.levels)
.await?;
let mcaptcha_config = MCaptchaDetails {
name: payload.description.clone(),
key,
};
Ok(mcaptcha_config)
}
}

View File

@@ -1,58 +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 actix_identity::Identity;
use actix_web::{web, HttpResponse, Responder};
use libmcaptcha::master::messages::RemoveCaptcha;
use serde::{Deserialize, Serialize};
use db_core::Login;
use crate::errors::*;
use crate::AppData;
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct DeleteCaptcha {
pub key: String,
pub password: String,
}
#[my_codegen::post(
path = "crate::V1_API_ROUTES.captcha.delete",
wrap = "crate::api::v1::get_middleware()"
)]
async fn delete(
payload: web::Json<DeleteCaptcha>,
data: AppData,
id: Identity,
) -> ServiceResult<impl Responder> {
use argon2_creds::Config;
let username = id.identity().unwrap();
let hash = data.db.get_password(&Login::Username(&username)).await?;
if !Config::verify(&hash.hash, &payload.password)? {
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 {
log::error!("Error while trying to remove captcha from cache {}", err);
}
Ok(HttpResponse::Ok())
}

View File

@@ -0,0 +1,159 @@
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use actix_identity::Identity;
use actix_web::{post, web, HttpResponse, Responder};
use serde::{Deserialize, Serialize};
use super::is_authenticated;
use crate::api::v1::mcaptcha::mcaptcha::MCaptchaDetails;
use crate::errors::*;
use crate::Data;
#[derive(Deserialize, Serialize)]
pub struct UpdateDuration {
pub key: String,
pub duration: i32,
}
#[post("/api/v1/mcaptcha/domain/token/duration/update")]
pub async fn update_duration(
payload: web::Json<UpdateDuration>,
data: web::Data<Data>,
id: Identity,
) -> ServiceResult<impl Responder> {
is_authenticated(&id)?;
let username = id.identity().unwrap();
if payload.duration > 0 {
sqlx::query!(
"UPDATE mcaptcha_config set duration = $1
WHERE key = $2 AND user_id = (SELECT ID FROM mcaptcha_users WHERE name = $3)",
&payload.duration,
&payload.key,
&username,
)
.execute(&data.db)
.await?;
Ok(HttpResponse::Ok())
} else {
// when mCaptcha/mCaptcha #2 is fixed, this wont be necessary
Err(ServiceError::CaptchaError(
m_captcha::errors::CaptchaError::CaptchaDurationZero,
))
}
}
#[derive(Deserialize, Serialize)]
pub struct GetDurationResp {
pub duration: i32,
}
#[derive(Deserialize, Serialize)]
pub struct GetDuration {
pub token: String,
}
#[post("/api/v1/mcaptcha/domain/token/duration/get")]
pub async fn get_duration(
payload: web::Json<MCaptchaDetails>,
data: web::Data<Data>,
id: Identity,
) -> ServiceResult<impl Responder> {
is_authenticated(&id)?;
let username = id.identity().unwrap();
let duration = sqlx::query_as!(
GetDurationResp,
"SELECT duration FROM mcaptcha_config
WHERE key = $1 AND user_id = (SELECT ID FROM mcaptcha_users WHERE name = $2)",
&payload.key,
&username,
)
.fetch_one(&data.db)
.await?;
Ok(HttpResponse::Ok().json(duration))
}
#[cfg(test)]
mod tests {
use actix_web::http::{header, StatusCode};
use actix_web::test;
use super::*;
use crate::tests::*;
use crate::*;
#[actix_rt::test]
async fn update_duration() {
const NAME: &str = "testuserduration";
const PASSWORD: &str = "longpassworddomain";
const EMAIL: &str = "testuserduration@a.com";
const GET_URL: &str = "/api/v1/mcaptcha/domain/token/duration/get";
const UPDATE_URL: &str = "/api/v1/mcaptcha/domain/token/duration/update";
{
let data = Data::new().await;
delete_user(NAME, &data).await;
}
register_and_signin(NAME, EMAIL, PASSWORD).await;
let (data, _, signin_resp, token_key) = add_token_util(NAME, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
let mut app = get_app!(data).await;
let update = UpdateDuration {
key: token_key.key.clone(),
duration: 40,
};
// check default
let get_level_resp = test::call_service(
&mut app,
post_request!(&token_key, GET_URL)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(get_level_resp.status(), StatusCode::OK);
let res_levels: GetDurationResp = test::read_body_json(get_level_resp).await;
assert_eq!(res_levels.duration, 30);
// update and check changes
let update_duration = test::call_service(
&mut app,
post_request!(&update, UPDATE_URL)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(update_duration.status(), StatusCode::OK);
let get_level_resp = test::call_service(
&mut app,
post_request!(&token_key, GET_URL)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(get_level_resp.status(), StatusCode::OK);
let res_levels: GetDurationResp = test::read_body_json(get_level_resp).await;
assert_eq!(res_levels.duration, 40);
}
}

View File

@@ -1,388 +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 actix_identity::Identity;
use actix_web::{web, HttpResponse, Responder};
use libmcaptcha::{defense::Level, defense::LevelBuilder};
use serde::{Deserialize, Serialize};
use db_core::TrafficPattern;
use super::create::{runner::create as create_runner, CreateCaptcha};
use super::update::{runner::update_captcha as update_captcha_runner, UpdateCaptcha};
use crate::errors::*;
use crate::settings::DefaultDifficultyStrategy;
use crate::AppData;
pub mod routes {
pub struct Easy {
/// easy is using defaults
pub create: &'static str,
pub update: &'static str,
}
impl Easy {
pub const fn new() -> Self {
Self {
create: "/api/v1/mcaptcha/add/easy",
update: "/api/v1/mcaptcha/update/easy",
}
}
}
}
pub fn services(cfg: &mut web::ServiceConfig) {
cfg.service(update);
cfg.service(create);
}
#[derive(Default, Serialize, Deserialize, Clone, Debug)]
/// User's traffic pattern; used in generating a captcha configuration
pub struct TrafficPatternRequest {
/// 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>,
/// Captcha description
pub description: String,
}
impl From<&TrafficPatternRequest> for TrafficPattern {
fn from(t: &TrafficPatternRequest) -> Self {
TrafficPattern {
avg_traffic: t.avg_traffic,
peak_sustainable_traffic: t.peak_sustainable_traffic,
broke_my_site_traffic: t.broke_my_site_traffic,
}
}
}
pub fn calculate(
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(
path = "crate::V1_API_ROUTES.captcha.easy.create",
wrap = "crate::api::v1::get_middleware()"
)]
async fn create(
payload: web::Json<TrafficPatternRequest>,
data: AppData,
id: Identity,
) -> ServiceResult<impl Responder> {
let username = id.identity().unwrap();
let payload = payload.into_inner();
let pattern = (&payload).into();
let levels =
calculate(&pattern, &data.settings.captcha.default_difficulty_strategy)?;
let msg = CreateCaptcha {
levels,
duration: data.settings.captcha.default_difficulty_strategy.duration,
description: payload.description,
};
let mcaptcha_config = create_runner(&msg, &data, &username).await?;
data.db
.add_traffic_pattern(&username, &mcaptcha_config.key, &pattern)
.await?;
Ok(HttpResponse::Ok().json(mcaptcha_config))
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct UpdateTrafficPattern {
pub pattern: TrafficPatternRequest,
pub key: String,
}
#[my_codegen::post(
path = "crate::V1_API_ROUTES.captcha.easy.update",
wrap = "crate::api::v1::get_middleware()"
)]
async fn update(
payload: web::Json<UpdateTrafficPattern>,
data: AppData,
id: Identity,
) -> ServiceResult<impl Responder> {
let username = id.identity().unwrap();
let payload = payload.into_inner();
let pattern = (&payload.pattern).into();
let levels =
calculate(&pattern, &data.settings.captcha.default_difficulty_strategy)?;
let msg = UpdateCaptcha {
levels,
duration: data.settings.captcha.default_difficulty_strategy.duration,
description: payload.pattern.description,
key: payload.key,
};
update_captcha_runner(&msg, &data, &username).await?;
data.db.delete_traffic_pattern(&username, &msg.key).await?;
data.db
.add_traffic_pattern(&username, &msg.key, &pattern)
.await?;
Ok(HttpResponse::Ok())
}
#[cfg(test)]
pub mod tests {
use actix_web::http::StatusCode;
use actix_web::test;
use actix_web::web::Bytes;
use super::*;
use crate::api::v1::mcaptcha::create::MCaptchaDetails;
use crate::api::v1::ROUTES;
use crate::tests::*;
use crate::*;
mod isoloated_test {
use super::{calculate, LevelBuilder};
use db_core::TrafficPattern;
#[test]
fn easy_configuration_works() {
let settings = crate::tests::get_settings();
let mut payload = TrafficPattern {
avg_traffic: 100_000,
peak_sustainable_traffic: 1_000_000,
broke_my_site_traffic: Some(10_000_000),
};
let strategy = &settings.captcha.default_difficulty_strategy;
let l1 = LevelBuilder::default()
.difficulty_factor(strategy.avg_traffic_difficulty)
.unwrap()
.visitor_threshold(payload.avg_traffic)
.build()
.unwrap();
let l2 = LevelBuilder::default()
.difficulty_factor(strategy.peak_sustainable_traffic_difficulty)
.unwrap()
.visitor_threshold(payload.peak_sustainable_traffic)
.build()
.unwrap();
let l3 = LevelBuilder::default()
.difficulty_factor(strategy.broke_my_site_traffic_difficulty)
.unwrap()
.visitor_threshold(payload.broke_my_site_traffic.unwrap())
.build()
.unwrap();
let levels = vec![l1, l2, l3];
assert_eq!(calculate(&payload, strategy).unwrap(), levels);
let estimated_lmax = LevelBuilder::default()
.difficulty_factor(strategy.broke_my_site_traffic_difficulty)
.unwrap()
.visitor_threshold(1500000)
.build()
.unwrap();
payload.broke_my_site_traffic = None;
assert_eq!(
calculate(&payload, strategy).unwrap(),
vec![l1, l2, estimated_lmax]
);
let lmax = LevelBuilder::default()
.difficulty_factor(strategy.broke_my_site_traffic_difficulty)
.unwrap()
.visitor_threshold(u32::MAX)
.build()
.unwrap();
let very_large_l2_peak_traffic = u32::MAX - 1;
let very_large_l2 = LevelBuilder::default()
.difficulty_factor(strategy.peak_sustainable_traffic_difficulty)
.unwrap()
.visitor_threshold(very_large_l2_peak_traffic)
.build()
.unwrap();
// payload.broke_my_site_traffic = Some(very_large_l2_peak_traffic);
payload.peak_sustainable_traffic = very_large_l2_peak_traffic;
assert_eq!(
calculate(&payload, strategy).unwrap(),
vec![l1, very_large_l2, lmax]
);
}
}
#[actix_rt::test]
pub async fn easy_works() {
const NAME: &str = "defaultuserconfgworks";
const PASSWORD: &str = "longpassworddomain";
const EMAIL: &str = "defaultuserconfgworks@a.com";
let data = crate::tests::get_data().await;
let data = &data;
delete_user(data, NAME).await;
let (_creds, signin_resp) =
register_and_signin(data, NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
let app = get_app!(data).await;
let payload = TrafficPatternRequest {
avg_traffic: 100_000,
peak_sustainable_traffic: 1_000_000,
broke_my_site_traffic: Some(10_000_000),
description: NAME.into(),
};
let default_levels = calculate(
&(&payload).into(),
&data.settings.captcha.default_difficulty_strategy,
)
.unwrap();
// START create_easy
let add_token_resp = test::call_service(
&app,
post_request!(&payload, ROUTES.captcha.easy.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_level_resp = test::call_service(
&app,
post_request!(&token_key, ROUTES.captcha.get)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(get_level_resp.status(), StatusCode::OK);
let res_levels: Vec<Level> = test::read_body_json(get_level_resp).await;
assert_eq!(res_levels, default_levels);
// END create_easy
// START update_easy
let update_pattern = TrafficPatternRequest {
avg_traffic: 1_000,
peak_sustainable_traffic: 10_000,
broke_my_site_traffic: Some(1_000_000),
description: NAME.into(),
};
let updated_default_values = calculate(
&(&update_pattern).into(),
&data.settings.captcha.default_difficulty_strategy,
)
.unwrap();
let payload = UpdateTrafficPattern {
pattern: update_pattern,
key: token_key.key.clone(),
};
let update_token_resp = test::call_service(
&app,
post_request!(&payload, ROUTES.captcha.easy.update)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(update_token_resp.status(), StatusCode::OK);
let get_level_resp = test::call_service(
&app,
post_request!(&token_key, ROUTES.captcha.get)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(get_level_resp.status(), StatusCode::OK);
let res_levels: Vec<Level> = test::read_body_json(get_level_resp).await;
assert_ne!(res_levels, default_levels);
assert_eq!(res_levels, updated_default_values);
// END update_easy
// test easy edit page
let easy_url = PAGES.panel.sitekey.get_edit_easy(&token_key.key);
let easy_edit_page = test::call_service(
&app,
test::TestRequest::get()
.uri(&easy_url)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(easy_edit_page.status(), StatusCode::OK);
let body: Bytes = test::read_body(easy_edit_page).await;
let body = String::from_utf8(body.to_vec()).unwrap();
assert!(body.contains(&token_key.name));
assert!(body.contains(
&payload
.pattern
.broke_my_site_traffic
.as_ref()
.unwrap()
.to_string()
));
assert!(body.contains(&payload.pattern.avg_traffic.to_string()));
assert!(body.contains(&payload.pattern.peak_sustainable_traffic.to_string()));
}
}

View File

@@ -1,52 +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 actix_identity::Identity;
use actix_web::{web, HttpResponse, Responder};
use serde::{Deserialize, Serialize};
use super::create::MCaptchaDetails;
use crate::errors::*;
use crate::AppData;
#[my_codegen::post(
path = "crate::V1_API_ROUTES.captcha.get",
wrap = "crate::api::v1::get_middleware()"
)]
pub async fn get_captcha(
payload: web::Json<MCaptchaDetails>,
data: AppData,
id: Identity,
) -> ServiceResult<impl Responder> {
let username = id.identity().unwrap();
let levels = data
.db
.get_captcha_levels(Some(&username), &payload.key)
.await?;
Ok(HttpResponse::Ok().json(levels))
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Levels {
levels: I32Levels,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct I32Levels {
pub difficulty_factor: i32,
pub visitor_threshold: i32,
}

View File

@@ -0,0 +1,334 @@
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use actix_identity::Identity;
use actix_web::{post, web, HttpResponse, Responder};
use m_captcha::{defense::Level, DefenseBuilder};
use serde::{Deserialize, Serialize};
use super::is_authenticated;
use crate::api::v1::mcaptcha::mcaptcha::MCaptchaDetails;
use crate::errors::*;
use crate::Data;
#[derive(Serialize, Deserialize)]
pub struct AddLevels {
pub levels: Vec<Level>,
// name is config_name
pub key: String,
}
// TODO try for non-existent token names
#[post("/api/v1/mcaptcha/levels/add")]
pub async fn add_levels(
payload: web::Json<AddLevels>,
data: web::Data<Data>,
id: Identity,
) -> ServiceResult<impl Responder> {
is_authenticated(&id)?;
let mut defense = DefenseBuilder::default();
let username = id.identity().unwrap();
for level in payload.levels.iter() {
defense.add_level(level.clone())?;
}
defense.build()?;
for level in payload.levels.iter() {
let difficulty_factor = level.difficulty_factor as i32;
let visitor_threshold = level.visitor_threshold as i32;
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?;
}
Ok(HttpResponse::Ok())
}
#[post("/api/v1/mcaptcha/levels/update")]
pub async fn update_levels(
payload: web::Json<AddLevels>,
data: web::Data<Data>,
id: Identity,
) -> ServiceResult<impl Responder> {
is_authenticated(&id)?;
let username = id.identity().unwrap();
let mut defense = DefenseBuilder::default();
for level in payload.levels.iter() {
defense.add_level(level.clone())?;
}
// I feel this is necessary as both difficulty factor _and_ visitor threshold of a
// level could change so doing this would not require us to send level_id to client
// still, needs to be benchmarked
defense.build()?;
sqlx::query!(
"DELETE FROM mcaptcha_levels
WHERE config_id = (
SELECT config_id FROM mcaptcha_config where key = ($1)
AND user_id = (
SELECT ID from mcaptcha_users WHERE name = $2
)
)",
&payload.key,
&username
)
.execute(&data.db)
.await?;
for level in payload.levels.iter() {
let difficulty_factor = level.difficulty_factor as i32;
let visitor_threshold = level.visitor_threshold as i32;
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?;
}
Ok(HttpResponse::Ok())
}
#[post("/api/v1/mcaptcha/levels/delete")]
pub async fn delete_levels(
payload: web::Json<AddLevels>,
data: web::Data<Data>,
id: Identity,
) -> ServiceResult<impl Responder> {
is_authenticated(&id)?;
let username = id.identity().unwrap();
for level in payload.levels.iter() {
let difficulty_factor = level.difficulty_factor as i32;
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 = $3)
) AND difficulty_factor = ($2);",
&payload.key,
difficulty_factor,
&username
)
.execute(&data.db)
.await?;
}
Ok(HttpResponse::Ok())
}
#[post("/api/v1/mcaptcha/levels/get")]
pub async fn get_levels(
payload: web::Json<MCaptchaDetails>,
data: web::Data<Data>,
id: Identity,
) -> ServiceResult<impl Responder> {
is_authenticated(&id)?;
let username = id.identity().unwrap();
let levels = get_levels_util(&payload.key, &username, &data).await?;
Ok(HttpResponse::Ok().json(levels))
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Levels {
levels: I32Levels,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct I32Levels {
pub difficulty_factor: i32,
pub visitor_threshold: i32,
}
async fn get_levels_util(key: &str, username: &str, data: &Data) -> 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)
);",
key,
&username
)
.fetch_all(&data.db)
.await?;
Ok(levels)
}
#[cfg(test)]
mod tests {
use actix_web::http::{header, StatusCode};
use actix_web::test;
use super::*;
use crate::tests::*;
use crate::*;
#[actix_rt::test]
async fn level_routes_work() {
const NAME: &str = "testuserlevelroutes";
const PASSWORD: &str = "longpassworddomain";
const EMAIL: &str = "testuserlevelrouts@a.com";
const UPDATE_URL: &str = "/api/v1/mcaptcha/levels/update";
const DEL_URL: &str = "/api/v1/mcaptcha/levels/delete";
const GET_URL: &str = "/api/v1/mcaptcha/levels/get";
{
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 mut app = get_app!(data).await;
/*
let add_level = AddLevels {
levels: levels.clone(),
key: key.key.clone(),
};
// 1. add level
let add_token_resp = test::call_service(
&mut app,
post_request!(&add_level, ADD_URL)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(add_token_resp.status(), StatusCode::OK);
*/
// 2. get level
let levels = vec![L1, L2];
let get_level_resp = test::call_service(
&mut app,
post_request!(&key, GET_URL)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(get_level_resp.status(), StatusCode::OK);
let res_levels: Vec<Level> = test::read_body_json(get_level_resp).await;
assert_eq!(res_levels, levels);
// 3. update level
let l1 = Level {
difficulty_factor: 10,
visitor_threshold: 10,
};
let l2 = Level {
difficulty_factor: 5000,
visitor_threshold: 5000,
};
let levels = vec![l1, l2];
let add_level = AddLevels {
levels: levels.clone(),
key: key.key.clone(),
};
let add_token_resp = test::call_service(
&mut app,
post_request!(&add_level, UPDATE_URL)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(add_token_resp.status(), StatusCode::OK);
let get_level_resp = test::call_service(
&mut app,
post_request!(&key, GET_URL)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(get_level_resp.status(), StatusCode::OK);
let res_levels: Vec<Level> = test::read_body_json(get_level_resp).await;
assert_eq!(res_levels, levels);
// 4. delete level
let l1 = Level {
difficulty_factor: 10,
visitor_threshold: 10,
};
let l2 = Level {
difficulty_factor: 5000,
visitor_threshold: 5000,
};
let levels = vec![l1, l2];
let add_level = AddLevels {
levels: levels.clone(),
key: key.key.clone(),
};
let add_token_resp = test::call_service(
&mut app,
post_request!(&add_level, DEL_URL)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(add_token_resp.status(), StatusCode::OK);
let get_level_resp = test::call_service(
&mut app,
post_request!(&key, GET_URL)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(get_level_resp.status(), StatusCode::OK);
let res_levels: Vec<Level> = test::read_body_json(get_level_resp).await;
assert_eq!(res_levels, Vec::new());
}
}

View File

@@ -0,0 +1,290 @@
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::borrow::Cow;
use actix_identity::Identity;
use actix_web::{post, web, HttpResponse, Responder};
use serde::{Deserialize, Serialize};
use super::{get_random, is_authenticated};
use crate::errors::*;
use crate::Data;
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct MCaptchaID {
pub name: Option<String>,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct MCaptchaDetails {
pub name: Option<String>,
pub key: String,
}
#[post("/api/v1/mcaptcha/add")]
pub async fn add_mcaptcha(data: web::Data<Data>, id: Identity) -> ServiceResult<impl Responder> {
is_authenticated(&id)?;
let username = id.identity().unwrap();
let mut key;
let resp;
loop {
key = get_random(32);
let res = sqlx::query!(
"INSERT INTO mcaptcha_config
(key, user_id)
VALUES ($1, (SELECT ID FROM mcaptcha_users WHERE name = $2))",
&key,
&username,
)
.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 {
Err(sqlx::Error::Database(err))?;
}
}
Err(e) => Err(e)?,
Ok(_) => {
resp = MCaptchaDetails { key, name: None };
break;
}
}
}
Ok(HttpResponse::Ok().json(resp))
}
#[post("/api/v1/mcaptcha/update/key")]
pub async fn update_token(
payload: web::Json<MCaptchaDetails>,
data: web::Data<Data>,
id: Identity,
) -> ServiceResult<impl Responder> {
use std::borrow::Cow;
is_authenticated(&id)?;
let username = id.identity().unwrap();
let mut key;
loop {
key = get_random(32);
let res = update_token_helper(&key, &payload.key, &username, &data).await;
if res.is_ok() {
break;
} else {
if let Err(sqlx::Error::Database(err)) = res {
if err.code() == Some(Cow::from("23505")) {
continue;
} else {
Err(sqlx::Error::Database(err))?;
}
};
}
}
let resp = MCaptchaDetails {
key,
name: payload.into_inner().name,
};
Ok(HttpResponse::Ok().json(resp))
}
async fn update_token_helper(
key: &str,
old_key: &str,
username: &str,
data: &Data,
) -> 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(())
}
#[post("/api/v1/mcaptcha/get")]
pub async fn get_token(
payload: web::Json<MCaptchaDetails>,
data: web::Data<Data>,
id: Identity,
) -> ServiceResult<impl Responder> {
is_authenticated(&id)?;
let username = id.identity().unwrap();
let res = match sqlx::query_as!(
MCaptchaDetails,
"SELECT key, name from mcaptcha_config
WHERE key = ($1) AND user_id = (SELECT ID FROM mcaptcha_users WHERE name = $2) ",
&payload.key,
&username,
)
.fetch_one(&data.db)
.await
{
Err(sqlx::Error::RowNotFound) => Err(ServiceError::TokenNotFound),
Ok(m) => Ok(m),
Err(e) => {
let e: ServiceError = e.into();
Err(e)
}
}?;
Ok(HttpResponse::Ok().json(res))
}
#[post("/api/v1/mcaptcha/delete")]
pub async fn delete_mcaptcha(
payload: web::Json<MCaptchaDetails>,
data: web::Data<Data>,
id: Identity,
) -> ServiceResult<impl Responder> {
is_authenticated(&id)?;
let username = id.identity().unwrap();
sqlx::query!(
"DELETE FROM mcaptcha_config
WHERE key = ($1) AND user_id = (SELECT ID FROM mcaptcha_users WHERE name = $2) ",
&payload.key,
&username,
)
.execute(&data.db)
.await?;
Ok(HttpResponse::Ok())
}
// Workflow:
// 1. Sign up
// 2. Sign in
// 3. Add domain(DNS TXT record verification? / put string at path)
// 4. Create token
// 5. Add levels
// 6. Update duration
// 7. Start syatem
#[cfg(test)]
mod tests {
use actix_web::http::{header, StatusCode};
use actix_web::test;
use super::*;
use crate::tests::*;
use crate::*;
#[actix_rt::test]
async fn add_mcaptcha_works() {
const NAME: &str = "testusermcaptcha";
const PASSWORD: &str = "longpassworddomain";
const EMAIL: &str = "testusermcaptcha@a.com";
const DEL_URL: &str = "/api/v1/mcaptcha/delete";
{
let data = Data::new().await;
delete_user(NAME, &data).await;
}
// 1. add mcaptcha token
register_and_signin(NAME, EMAIL, PASSWORD).await;
let (data, _, signin_resp, token_key) = add_token_util(NAME, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
let mut app = get_app!(data).await;
// let mut domain = MCaptchaID {
// name: TOKEN_NAME.into(),
// };
// 4. delete token
let del_token = test::call_service(
&mut app,
post_request!(&token_key, DEL_URL)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(del_token.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn update_and_get_mcaptcha_works() {
const NAME: &str = "updateusermcaptcha";
const PASSWORD: &str = "longpassworddomain";
const EMAIL: &str = "testupdateusermcaptcha@a.com";
const UPDATE_URL: &str = "/api/v1/mcaptcha/update/key";
const GET_URL: &str = "/api/v1/mcaptcha/get";
{
let data = Data::new().await;
delete_user(NAME, &data).await;
}
// 1. add mcaptcha token
register_and_signin(NAME, EMAIL, PASSWORD).await;
let (data, _, signin_resp, token_key) = add_token_util(NAME, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
let mut app = get_app!(data).await;
// 2. update token key
let update_token_resp = test::call_service(
&mut app,
post_request!(&token_key, UPDATE_URL)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(update_token_resp.status(), StatusCode::OK);
let updated_token: MCaptchaDetails = test::read_body_json(update_token_resp).await;
// get token key with updated key
let get_token_resp = test::call_service(
&mut app,
post_request!(&updated_token, GET_URL)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(get_token_resp.status(), StatusCode::OK);
// check if they match
let mut get_token_key: MCaptchaDetails = test::read_body_json(get_token_resp).await;
assert_eq!(get_token_key.key, updated_token.key);
get_token_key.key = "nonexistent".into();
let get_nonexistent_token_resp = test::call_service(
&mut app,
post_request!(&get_token_key, GET_URL)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(get_nonexistent_token_resp.status(), StatusCode::NOT_FOUND);
}
}

View File

@@ -1,28 +1,25 @@
/* /*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net> * Copyright (C) 2021 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 create; pub mod duration;
pub mod delete; pub mod levels;
pub mod easy; pub mod mcaptcha;
pub mod get;
pub mod stats; pub use super::auth::is_authenticated;
#[cfg(test)]
pub mod test;
pub mod update;
pub fn get_random(len: usize) -> String { pub fn get_random(len: usize) -> String {
use std::iter; use std::iter;
@@ -37,42 +34,3 @@ pub fn get_random(len: usize) -> String {
.take(len) .take(len)
.collect::<String>() .collect::<String>()
} }
pub fn services(cfg: &mut actix_web::web::ServiceConfig) {
easy::services(cfg);
cfg.service(stats::get);
cfg.service(create::create);
cfg.service(get::get_captcha);
cfg.service(update::update_key);
cfg.service(update::update_captcha);
cfg.service(delete::delete);
}
pub mod routes {
use super::easy::routes::Easy;
use super::stats::routes::Stats;
pub struct Captcha {
pub create: &'static str,
pub update: &'static str,
pub get: &'static str,
pub delete: &'static str,
pub update_key: &'static str,
pub easy: Easy,
pub stats: Stats,
}
impl Captcha {
pub const fn new() -> Self {
Self {
create: "/api/v1/mcaptcha/create",
update: "/api/v1/mcaptcha/update",
get: "/api/v1/mcaptcha/get",
update_key: "/api/v1/mcaptcha/update/key",
delete: "/api/v1/mcaptcha/delete",
easy: Easy::new(),
stats: Stats::new(),
}
}
}
}

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