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
460 changed files with 9613 additions and 27033 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,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,74 +0,0 @@
name: Lint
on:
pull_request:
types: [opened, synchronize, reopened]
push:
branches:
- master
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,98 +0,0 @@
name: Coverage
on:
pull_request:
types: [opened, synchronize, reopened]
push:
branches:
- master
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') }}
- 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:
DATABASE_URL: postgres://postgres:password@localhost:5432/postgres
- name: build frontend
run: make frontend
- name: Generate coverage file
if: (github.ref == 'refs/heads/master' || github.event_name == 'pull_request')
uses: actions-rs/tarpaulin@v0.1
with:
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
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.ref == 'refs/heads/master' || github.event_name == 'pull_request')
uses: codecov/codecov-action@v2

View File

@@ -1,28 +1,27 @@
name: Build
name: CI (Linux)
on:
schedule:
- cron: "0 9 * * *"
pull_request:
types: [opened, synchronize, reopened]
push:
branches:
- master
jobs:
build_and_test:
strategy:
fail-fast: false
matrix:
version:
#- 1.51.0
- stable
# - nightly
- nightly
name: ${{ matrix.version }} - x86_64-unknown-linux-gnu
runs-on: ubuntu-latest
services:
postgres:
image: postgres
env:
@@ -36,10 +35,6 @@ jobs:
--health-retries 5
ports:
- 5432:5432
mcaptcha-redis:
image: mcaptcha/cache
ports:
- 6379:6379
steps:
- uses: actions/checkout@v2
@@ -49,17 +44,15 @@ jobs:
path: |
~/.cargo/registry
~/.cargo/git
node_modules
./docs/openapi/node_modules
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- uses: actions/setup-node@v2
- uses: borales/actions-yarn@v2.0.0
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
cmd: install # will run `yarn install` command
- uses: borales/actions-yarn@v2.0.0
with:
cmd: build # will run `yarn build` command
- name: Install ${{ matrix.version }}
uses: actions-rs/toolchain@v1
@@ -69,36 +62,66 @@ jobs:
override: true
- name: Run migrations
run: make migrate
uses: actions-rs/cargo@v1
with:
command: run
args: --bin tests-migrate
env:
DATABASE_URL: postgres://postgres:password@localhost:5432/postgres
- name: build
run: make
- name: check build
uses: actions-rs/cargo@v1
with:
command: check
args: --all --bins --examples --tests
env:
DATABASE_URL: postgres://postgres:password@localhost:5432/postgres
# - name: build frontend
# run: make frontend
#
- name: lint frontend
run: yarn lint
- name: run tests
run: make test
- name: tests
uses: actions-rs/cargo@v1
timeout-minutes: 40
with:
command: test
args: --all --all-features --no-fail-fast
env:
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
if: matrix.version == 'stable' && (github.repository == 'mCaptcha/mCaptcha')
run: make doc
if: matrix.version == 'stable' && (github.repository == 'mCaptcha/guard')
uses: actions-rs/cargo@v1
with:
command: doc
args: --no-deps --workspace --all-features
env:
DATABASE_URL: postgres://postgres:password@localhost:5432/postgres
GIT_HASH: 8e77345f1597e40c2e266cb4e6dee74888918a61 # dummy value
COMPILED_DATE: "2021-07-21"
OPEN_API_DOCS: 8e77345f1597e40c2e266cb4e6dee74888918a61
- 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
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

9
.gitignore vendored
View File

@@ -5,12 +5,3 @@ tarpaulin-report.html
cobertura.xml
prod/
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,5 +0,0 @@
## 0.1.0(unreleased)
### Changed
- Rename pow section in settings to captcha and add options to configure([`42544ec42`](https://github.com/mCaptcha/mCaptcha/commit/42544ec421e0c3ec4a8d132e6101ab4069bf0065))

2927
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,20 +1,20 @@
[package]
name = "mcaptcha"
name = "guard"
version = "0.1.0"
description = "mCaptcha - a PoW-based CAPTCHA system"
homepage = "https://mcaptcha.org"
repository = "https://github.com/mCaptcha/mCaptcha"
repository = "https://github.com/mCaptcha/guard"
documentation = "https://mcaptcha.org/docs/"
license = "AGPLv3 or later version"
lisense = "AGPLv3 or later version"
authors = ["realaravinth <realaravinth@batsense.net>"]
edition = "2021"
default-run = "mcaptcha"
edition = "2018"
default-run = "guard"
build = "build.rs"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[[bin]]
name = "mcaptcha"
name = "guard"
path = "./src/main.rs"
[[bin]]
@@ -22,35 +22,36 @@ name = "tests-migrate"
path = "./src/tests-migrate.rs"
[dependencies]
actix-web = "4.0.1"
actix = "0.13"
actix-identity = "0.4.0"
actix-http = "3.0.4"
actix-rt = "2"
actix-cors = "0.6.1"
actix-service = "2.0.0"
#my-codegen = {version="0.5.0-beta.5", package = "actix-web-codegen", git ="https://github.com/realaravinth/actix-web"}
actix-web = "3.3.2"
actix = "0.10"
actix-identity = "0.3"
actix-http = "2.2"
actix-rt = "1"
actix-cors = "0.5.4"
actix-service = "1.0.6"
csrf = "0.4.0"
mime_guess = "2.0.3"
rust-embed = "6.4.0"
cache-buster = { git = "https://github.com/realaravinth/cache-buster" }
rust-embed = "5.9.0"
cache-buster = { version = "0.1.0", git = "https://github.com/realaravinth/cache-buster" }
futures = "0.3.15"
tokio = { version = "1.14", features = ["sync"]}
futures = "0.3.14"
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"
validator = { version = "0.15", features = ["derive"]}
validator = { version = "0.13", features = ["derive"]}
derive_builder = "0.11"
derive_builder = "0.10"
derive_more = "0.99"
serde = "1"
serde_json = "1"
serde_yaml = "0.8.17"
url = "2.2"
urlencoding = "2.1.0"
pretty_env_logger = "0.4"
log = "0.4"
@@ -58,43 +59,23 @@ log = "0.4"
lazy_static = "1.4"
libmcaptcha = { branch = "master", git = "https://github.com/mCaptcha/libmcaptcha", features = ["full"] }
#libmcaptcha = { path = "../libmcaptcha", features = ["full"]}
# m_captcha = { version = "0.1.2", git = "https://github.com/mCaptcha/mCaptcha" }
m_captcha = { branch = "master", git = "https://github.com/mCaptcha/mCaptcha" }
rand = "0.8"
sailfish = "0.4.0"
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.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"]
sailfish = "0.3.2"
[build-dependencies]
serde_yaml = "0.8.17"
serde = "1"
serde_json = "1"
cache-buster = { version = "0.2.0", git = "https://github.com/realaravinth/cache-buster" }
yaml-rust = "0.4.5"
cache-buster = { version = "0.1.0", git = "https://github.com/realaravinth/cache-buster" }
mime = "0.3.16"
sqlx = { version = "0.5.13", features = [ "runtime-actix-rustls", "postgres", "time", "offline" ] }
log = "0.4"
config = "0.11"
url = "2.2"
[dev-dependencies]
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
## To quickly make changes:
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
## Setting up development environment
### 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
```
2. Install Node `v14.16.0`:
2. Install Node:
Please refer to [official instructions](https://nodejs.org/en/download/)
3. Install yarn:
@@ -88,7 +63,7 @@ $ docker start mcaptcha-postgres
4. Set configurations:
```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
```
@@ -103,7 +78,7 @@ $ echo 'export DATABASE_URL="postgres://postgres:password@localhost:5432/postgre
However, this project ships with a utility to run migrations!
```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
```
@@ -114,27 +89,20 @@ That's it, you are all set!
### Compile:
```bash
$ cd mcaptcha # your copy of https://github.com/mCaptcha/mcaptcha
$ cd guard # your copy of https://github.com/mCaptcha/guard
$ make
```
### Additional commands:
```bash
mcaptcha git:(master) ✗ make help
default Run app in debug mode
clean Delete build artifacts
coverage Generate code coverage report in HTML format
dev-env Setup development environtment
doc Generate documentation
docker Build Docker image
docker-publish Build and publish Docker image
frontend Build frontend
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
guard git:(master) ✗ make help
docs - build documentation
run - run developer instance
test - run unit and integration tests
migrate - run database migrations
dev-env - download dependencies
clean - drop builds and environments
coverage - build test coverage in HTML format
xml-coverage - build test coverage in XML for upload to codecov
```

View File

@@ -1,43 +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 rust
WORKDIR /src
RUN mkdir src && echo "fn main() {}" > src/main.rs
COPY Cargo.toml .
RUN sed -i '/.*build.rs.*/d' Cargo.toml
COPY Cargo.lock .
COPY migrations /src/migrations
COPY sqlx-data.json /src/
COPY src/tests-migrate.rs /src/src/tests-migrate.rs
COPY src/settings.rs /src/src/settings.rs
RUN cargo --version
RUN cargo build --release
COPY . /src
COPY --from=frontend /src/static/cache/bundle/ /src/static/cache/bundle/
RUN cargo build --release
FROM debian:bullseye
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" ]

120
Makefile
View File

@@ -1,85 +1,49 @@
BUNDLE = static/cache/bundle
OPENAPI = docs/openapi
CLEAN_UP = $(BUNDLE) src/cache_buster_data.json assets
define frontend_env ## install frontend deps
yarn install
cd docs/openapi && yarn install
endef
default: frontend ## Build app in debug mode
# WIP
default: build-frontend
cargo build
clean: ## Delete build artifacts
@cargo clean
@yarn cache clean
@-rm $(CLEAN_UP)
coverage: migrate ## Generate code coverage report in HTML format
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
cargo run --bin tests-migrate
release: frontend ## Build app with release optimizations
cargo build --release
run: frontend ## Run app in debug mode
run: build-frontend-dev
cargo run
test: frontend-test frontend ## Run all available tests
./scripts/tests.sh
# cargo test --all-features --no-fail-fast
dev-env:
cargo fetch
yarn install
xml-test-coverage: migrate ## Generate code coverage report in XML format
docs:
cargo doc --no-deps --workspace --all-features
build-frontend-dev:
yarn start
build-frontend:
yarn build
test: migrate
cargo test
xml-test-coverage: migrate
cargo tarpaulin -t 1200 --out Xml
help: ## Prints help for targets with comments
@cat $(MAKEFILE_LIST) | grep -E '^[a-zA-Z_-]+:.*?## .*$$' | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}'
coverage: migrate
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 ''

154
README.md
View File

@@ -1,124 +1,80 @@
<div align="center">
<img width="100px" alt="mcaptcha logo" src="./docs/res/icon-trans.png" />
<h1>mCaptcha</h1>
<h1>mCaptcha Guard</h1>
<p>
<strong>
Proof of work based, privacy respecting CAPTCHA system with a kickass UX.
</strong>
<strong>Back-end component of mCaptcha</strong>
</p>
[![Documentation](https://img.shields.io/badge/docs-master-blue?style=flat-square)](https://mcaptcha.github.io/mCaptcha/mCaptcha/)
[![Build](https://github.com/mCaptcha/mCaptcha/actions/workflows/linux.yml/badge.svg)](https://github.com/mCaptcha/mCaptcha/actions/workflows/linux.yml)
[![Docker](https://img.shields.io/docker/pulls/mcaptcha/mcaptcha)](https://hub.docker.com/r/mcaptcha/mcaptcha)
[![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/mCaptcha/branch/master/graph/badge.svg?style=flat-square)](https://codecov.io/gh/mCaptcha/mCaptcha)
[![Documentation](https://img.shields.io/badge/docs-master-blue)](https://mcaptcha.github.io/guard/guard/)
![CI (Linux)](<https://github.com/mCaptcha/guard/workflows/CI%20(Linux)/badge.svg>)
[![dependency status](https://deps.rs/repo/github/mCaptcha/guard/status.svg)](https://deps.rs/repo/github/mCaptcha/guard)
[![codecov](https://codecov.io/gh/mCaptcha/guard/branch/master/graph/badge.svg)](https://codecov.io/gh/mCaptcha/guard)
<br />
[![AGPL License](https://img.shields.io/badge/license-AGPL-blue.svg?style=flat-square)](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**
[![AGPL License](https://img.shields.io/badge/license-AGPL-blue.svg)](http://www.gnu.org/licenses/agpl-3.0)
</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
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)
**STATUS: UNUSABLE BUT ACTIVE DEVELOPMENT**
## 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
time to compute) and submit it to mCaptcha.
- Install Cargo using [rustup](https://rustup.rs/) with:
2. We'll validate the proof:
- **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
$ docker-compose -d up
```
$ curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```
It takes a while to build the image so please be patient :)
- Clone the repository with:
See [DEPLOYMENT.md](./docs/DEPLOYMET.md) detailed alternate deployment
methods.
```
$ git clone https://github.com/mCaptcha/guard
```
## Development:
- Build with Cargo:
See [HACKING.md](./docs/HACKING.md)
```
$ cd guard && cargo build
```
## Deployment:
### Configuration:
See [DEPLOYMENT.md](./docs/DEPLOYMET.md)
Guard is highly configurable.
Configuration is applied/merged in the following order:
## Configuration:
1. `config/default.toml`
2. environment variables.
See [CONFIGURATION.md](./docs/CONFIGURATION.md)
#### 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.
###### 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

@@ -14,10 +14,13 @@
* 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::process::Command;
use cache_buster::{BusterBuilder, NoHashCategory};
use sqlx::types::time::OffsetDateTime;
use cache_buster::BusterBuilder;
#[path = "./src/settings.rs"]
mod settings;
fn main() {
// note: add error checking yourself.
@@ -28,31 +31,32 @@ fn main() {
let git_hash = String::from_utf8(output.stdout).unwrap();
println!("cargo:rustc-env=GIT_HASH={}", git_hash);
let now = OffsetDateTime::now_utc().format("%y-%m-%d");
println!("cargo:rustc-env=COMPILED_DATE={}", &now);
let yml = include_str!("./openapi.yaml");
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() {
// until APPLICATION_WASM gets added to mime crate
// PR: https://github.com/hyperium/mime/pull/138
// let types = vec![
// mime::IMAGE_PNG,
// mime::IMAGE_SVG,
// mime::IMAGE_JPEG,
// mime::IMAGE_GIF,
// mime::APPLICATION_JAVASCRIPT,
// mime::TEXT_CSS,
// ];
println!("cargo:rerun-if-changed=static/cache");
let no_hash = vec![NoHashCategory::FileExtentions(vec!["wasm"])];
let 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/cache/")
.result("./assets")
.no_hash(no_hash)
.source("./static")
.result("./prod")
.prefix(settings.server.url_prefix)
.mime_types(types)
.copy(true)
.follow_links(true)
.build()
.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,39 +1,4 @@
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
[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]
# This section deals with the database location and how to access it
@@ -50,21 +15,25 @@ password = "password"
name = "postgres"
pool = 4
[redis]
# This section deals with the database location and how to access it
# Please note that at the moment, we have support for only postgresqa.
# Example, if you are Batman, your config would be:
# hostname = "batcave.org"
# port = "5432"
# username = "batman"
# password = "somereallycomplicatedBatmanpassword"
url = "redis://127.0.0.1"
pool = 4
# This section deals with the configuration of the actual server
[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"
allow_registration = true
url_prefix = "/test"
[smtp]
from = "admin@localhost"
reply = "admin@localhost"
url = "127.0.0.1"
port = 10025
username = "admin"
password = "password"
[pow]
# 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

View File

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

View File

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

View File

@@ -1,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() {
// Begin Swagger UI call region
const ui = SwaggerUIBundle({
url: "/docs/openapi.yaml",
url: "./openapi.json",
dom_id: '#swagger-ui',
deepLinking: true,
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 (
name VARCHAR(100) NOT NULL UNIQUE,
email VARCHAR(100) UNIQUE DEFAULT NULL,
email_verified BOOLEAN DEFAULT NULL,
secret varchar(50) NOT NULL UNIQUE,
password TEXT 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,
user_id INTEGER NOT NULL references mcaptcha_users(ID) ON DELETE CASCADE,
key varchar(100) NOT NULL UNIQUE,
name varchar(100) NOT NULL,
name varchar(100) DEFAULT NULL,
duration integer NOT NULL DEFAULT 30
);

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

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

View File

@@ -1,5 +1,5 @@
/*
* 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
* it under the terms of the GNU Affero General Public License as

View File

@@ -1,76 +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;
use sqlx::Error::RowNotFound;
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;
match rec {
Ok(s) => {
if Config::verify(&s.password, &payload.password)? {
runners::delete_user(&username, &data).await?;
id.forget();
Ok(HttpResponse::Ok())
} else {
Err(ServiceError::WrongPassword)
}
}
Err(RowNotFound) => Err(ServiceError::AccountNotFound),
Err(_) => Err(ServiceError::InternalServerError),
}
}
pub mod runners {
use super::*;
pub async fn delete_user(name: &str, data: &AppData) -> ServiceResult<()> {
sqlx::query!("DELETE FROM mcaptcha_users WHERE name = ($1)", name,)
.execute(&data.db)
.await?;
Ok(())
}
}
pub fn services(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(delete_account);
}

View File

@@ -1,94 +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::borrow::Cow;
use actix_identity::Identity;
use actix_web::{web, HttpResponse, Responder};
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 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))
}
/// 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 res = sqlx::query!(
"UPDATE mcaptcha_users set email = $1
WHERE name = $2",
&payload.email,
&username,
)
.execute(&data.db)
.await;
if res.is_err() {
if let Err(sqlx::Error::Database(err)) = res {
if err.code() == Some(Cow::from("23505"))
&& err.message().contains("mcaptcha_users_email_key")
{
return Err(ServiceError::EmailTaken);
} else {
return Err(sqlx::Error::Database(err).into());
}
};
}
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,206 +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 serde::{Deserialize, Serialize};
use sqlx::Error::RowNotFound;
use crate::api::v1::auth::runners::Password;
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)?;
sqlx::query!(
"UPDATE mcaptcha_users set password = $1
WHERE name = $2",
&new_hash,
&user,
)
.execute(&data.db)
.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();
let rec = sqlx::query_as!(
Password,
r#"SELECT password FROM mcaptcha_users WHERE name = ($1)"#,
&username,
)
.fetch_one(&data.db)
.await;
match rec {
Ok(s) => {
if Config::verify(&s.password, &payload.password)? {
let update: UpdatePassword = payload.into_inner().into();
update_password_runner(&username, update, &data).await?;
Ok(HttpResponse::Ok())
} else {
Err(ServiceError::WrongPassword)
}
}
Err(RowNotFound) => Err(ServiceError::AccountNotFound),
Err(_) => Err(ServiceError::InternalServerError),
}
}
pub fn services(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(update_user_password);
}
#[cfg(test)]
mod tests {
use super::*;
use actix_web::http::StatusCode;
use actix_web::test;
use crate::api::v1::ROUTES;
use crate::data::Data;
use crate::tests::*;
#[actix_rt::test]
async fn update_password_works() {
const NAME: &str = "updatepassuser";
const PASSWORD: &str = "longpassword2";
const EMAIL: &str = "updatepassuser@a.com";
{
let data = Data::new().await;
delete_user(NAME, &data).await;
}
let (data, _, signin_resp) = register_and_signin(NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
let 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(
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(
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,90 +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::borrow::Cow;
use actix_identity::Identity;
use actix_web::{HttpResponse, Responder};
use serde::{Deserialize, Serialize};
use crate::api::v1::mcaptcha::get_random;
use crate::errors::*;
use crate::AppData;
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Secret {
pub secret: String,
}
#[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 = sqlx::query_as!(
Secret,
r#"SELECT secret FROM mcaptcha_users WHERE name = ($1)"#,
&username,
)
.fetch_one(&data.db)
.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);
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 {
return Err(sqlx::Error::Database(err).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,249 +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::data::Data;
use crate::*;
use crate::errors::*;
use crate::tests::*;
#[actix_rt::test]
async fn uname_email_exists_works() {
const NAME: &str = "testuserexists";
const PASSWORD: &str = "longpassword2";
const EMAIL: &str = "testuserexists@a.com2";
{
let data = Data::new().await;
delete_user(NAME, &data).await;
}
let (data, _, signin_resp) = register_and_signin(NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
let 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]
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 = Data::new().await;
delete_user(NAME, &data).await;
delete_user(NAME2, &data).await;
}
let _ = register_and_signin(NAME2, EMAIL2, PASSWORD).await;
let (data, _creds, signin_resp) = register_and_signin(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(
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(
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]
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 = Data::new().await;
futures::join!(
delete_user(NAME, &data),
delete_user(NAME2, &data),
delete_user(NAME_CHANGE, &data)
);
}
let _ = register_and_signin(NAME2, EMAIL2, PASSWORD).await;
let (data, _creds, signin_resp) = register_and_signin(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(
NAME_CHANGE,
PASSWORD,
ROUTES.account.update_username,
&username_udpate,
ServiceError::UsernameTaken,
)
.await;
}

View File

@@ -1,110 +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::borrow::Cow;
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 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(resp)
}
}
#[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)?;
let res = sqlx::query!(
"UPDATE mcaptcha_users set name = $1
WHERE name = $2",
&processed_uname,
&username,
)
.execute(&data.db)
.await;
if res.is_err() {
if let Err(sqlx::Error::Database(err)) = res {
if err.code() == Some(Cow::from("23505"))
&& err.message().contains("mcaptcha_users_name_key")
{
return Err(ServiceError::UsernameTaken);
} else {
return Err(sqlx::Error::Database(err).into());
}
};
}
id.forget();
id.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,5 +1,5 @@
/*
* 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
* it under the terms of the GNU Affero General Public License as
@@ -14,71 +14,27 @@
* 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::http::header;
use actix_web::{web, HttpResponse, Responder};
use actix_web::{get, post, web, HttpResponse, Responder};
use log::debug;
use serde::{Deserialize, Serialize};
use super::mcaptcha::get_random;
use crate::errors::*;
use crate::AppData;
pub mod routes {
use actix_auth_middleware::GetLoginRoute;
pub struct Auth {
pub logout: &'static str,
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 {
use std::borrow::Cow;
use super::*;
use crate::Data;
#[derive(Clone, Debug, Deserialize, Serialize)]
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 username: String,
pub password: String,
}
@@ -87,79 +43,25 @@ pub mod runners {
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;
use sqlx::Error::RowNotFound;
let verify = |stored: &str, received: &str| {
if Config::verify(stored, received)? {
Ok(())
} else {
Err(ServiceError::WrongPassword)
}
};
if payload.login.contains('@') {
#[derive(Clone, Debug)]
struct EmailLogin {
name: String,
password: String,
}
let email_fut = sqlx::query_as!(
EmailLogin,
r#"SELECT name, password FROM mcaptcha_users WHERE email = ($1)"#,
&payload.login,
)
.fetch_one(&data.db)
.await;
match email_fut {
Ok(s) => {
verify(&s.password, &payload.password)?;
Ok(s.name)
}
Err(RowNotFound) => Err(ServiceError::AccountNotFound),
Err(_) => Err(ServiceError::InternalServerError),
}
} else {
let username_fut = sqlx::query_as!(
Password,
r#"SELECT password FROM mcaptcha_users WHERE name = ($1)"#,
&payload.login,
)
.fetch_one(&data.db)
.await;
match username_fut {
Ok(s) => {
verify(&s.password, &payload.password)?;
Ok(payload.login)
}
Err(RowNotFound) => Err(ServiceError::AccountNotFound),
Err(_) => Err(ServiceError::InternalServerError),
}
}
}
pub async fn register_runner(
payload: &Register,
data: &AppData,
) -> ServiceResult<()> {
if !crate::SETTINGS.allow_registration {
return Err(ServiceError::ClosedForRegistration);
}
if payload.password != payload.confirm_password {
return Err(ServiceError::PasswordsDontMatch);
#[post("/api/v1/signup")]
pub async fn signup(
payload: web::Json<Register>,
data: web::Data<Data>,
) -> ServiceResult<impl Responder> {
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(email)?;
data.creds.email(Some(&email))?;
}
let mut secret;
@@ -169,8 +71,8 @@ pub mod runners {
let res;
if let Some(email) = &payload.email {
res = sqlx::query!(
"insert into mcaptcha_users
(name , password, email, secret) values ($1, $2, $3, $4)",
"INSERT INTO mcaptcha_users
(name , password, email, secret) VALUES ($1, $2, $3, $4)",
&username,
&hash,
&email,
@@ -191,71 +93,225 @@ pub mod runners {
}
if res.is_ok() {
break;
} else if let Err(sqlx::Error::Database(err)) = res {
} else {
if let Err(sqlx::Error::Database(err)) = res {
if err.code() == Some(Cow::from("23505")) {
let msg = err.message();
if msg.contains("mcaptcha_users_name_key") {
return Err(ServiceError::UsernameTaken);
} else if msg.contains("mcaptcha_users_email_key") {
return Err(ServiceError::EmailTaken);
Err(ServiceError::UsernameTaken)?;
} else if msg.contains("mcaptcha_users_secret_key") {
continue;
} else {
return Err(ServiceError::InternalServerError);
Err(ServiceError::InternalServerError)?;
}
} else {
return Err(sqlx::Error::Database(err).into());
Err(sqlx::Error::Database(err))?;
}
};
}
Ok(())
}
}
pub fn services(cfg: &mut web::ServiceConfig) {
cfg.service(register);
cfg.service(login);
cfg.service(signout);
}
#[my_codegen::post(path = "crate::V1_API_ROUTES.auth.register")]
async fn register(
payload: web::Json<runners::Register>,
data: AppData,
) -> ServiceResult<impl Responder> {
runners::register_runner(&payload, &data).await?;
Ok(HttpResponse::Ok())
}
#[my_codegen::post(path = "crate::V1_API_ROUTES.auth.login")]
async fn login(
#[post("/api/v1/signin")]
pub async fn signin(
id: Identity,
payload: web::Json<runners::Login>,
query: web::Query<super::RedirectQuery>,
data: AppData,
payload: web::Json<Login>,
data: web::Data<Data>,
) -> ServiceResult<impl Responder> {
let username = runners::login_runner(payload.into_inner(), &data).await?;
id.remember(username);
// Ok(HttpResponse::Ok())
use argon2_creds::Config;
use sqlx::Error::RowNotFound;
let query = query.into_inner();
if let Some(redirect_to) = query.redirect_to {
Ok(HttpResponse::Found()
.append_header((header::LOCATION, redirect_to))
.finish())
let rec = sqlx::query_as!(
Password,
r#"SELECT password FROM mcaptcha_users WHERE name = ($1)"#,
&payload.username,
)
.fetch_one(&data.db)
.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 {
Ok(HttpResponse::Ok().finish())
Err(ServiceError::WrongPassword)
}
}
Err(RowNotFound) => return Err(ServiceError::UsernameNotFound),
Err(_) => return Err(ServiceError::InternalServerError)?,
}
}
#[my_codegen::get(
path = "crate::V1_API_ROUTES.auth.logout",
wrap = "crate::api::v1::get_middleware()"
)]
async fn signout(id: Identity) -> impl Responder {
if id.identity().is_some() {
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct Secret {
pub secret: String,
}
#[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();
}
HttpResponse::Found()
.append_header((header::LOCATION, crate::PAGES.auth.login))
.finish()
HttpResponse::Ok()
}
/// 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,153 +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::borrow::Cow;
use actix_identity::Identity;
use actix_web::{web, HttpResponse, Responder};
use libmcaptcha::defense::Level;
use serde::{Deserialize, Serialize};
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 futures::future::try_join_all;
use libmcaptcha::DefenseBuilder;
use log::debug;
use super::*;
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()?;
debug!("creating config");
let mcaptcha_config =
// add_mcaptcha_util(payload.duration, &payload.description, &data, username).await?;
{
let mut key;
let resp;
loop {
key = get_random(32);
let res = sqlx::query!(
"INSERT INTO mcaptcha_config
(key, user_id, duration, name)
VALUES ($1, (SELECT ID FROM mcaptcha_users WHERE name = $2), $3, $4)",
&key,
&username,
payload.duration as i32,
&payload.description,
)
.execute(&data.db)
.await;
match res {
Err(sqlx::Error::Database(err)) => {
if err.code() == Some(Cow::from("23505"))
&& err.message().contains("mcaptcha_config_key_key")
{
continue;
} else {
return Err(sqlx::Error::Database(err).into());
}
}
Err(e) => return Err(e.into()),
Ok(_) => {
resp = MCaptchaDetails {
key,
name: payload.description.to_owned(),
};
break;
}
}
}
resp
};
debug!("config created");
let mut futs = Vec::with_capacity(payload.levels.len());
for level in payload.levels.iter() {
let difficulty_factor = level.difficulty_factor as i32;
let visitor_threshold = level.visitor_threshold as i32;
let fut = sqlx::query!(
"INSERT INTO mcaptcha_levels (
difficulty_factor,
visitor_threshold,
config_id) VALUES (
$1, $2, (
SELECT config_id FROM mcaptcha_config WHERE
key = ($3) AND user_id = (
SELECT ID FROM mcaptcha_users WHERE name = $4
)));",
difficulty_factor,
visitor_threshold,
&mcaptcha_config.key,
&username,
)
.execute(&data.db);
futs.push(fut);
}
try_join_all(futs).await?;
Ok(mcaptcha_config)
}
}

View File

@@ -1,95 +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 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;
use sqlx::Error::RowNotFound;
let username = id.identity().unwrap();
struct PasswordID {
password: String,
id: i32,
}
let rec = sqlx::query_as!(
PasswordID,
r#"SELECT ID, password FROM mcaptcha_users WHERE name = ($1)"#,
&username,
)
.fetch_one(&data.db)
.await;
match rec {
Ok(rec) => {
if Config::verify(&rec.password, &payload.password)? {
let payload = payload.into_inner();
sqlx::query!(
"DELETE FROM mcaptcha_levels
WHERE config_id = (
SELECT config_id FROM mcaptcha_config
WHERE key = $1 AND user_id = $2
);",
&payload.key,
&rec.id,
)
.execute(&data.db)
.await?;
sqlx::query!(
"DELETE FROM mcaptcha_config WHERE key = ($1) AND user_id = $2;",
&payload.key,
&rec.id,
)
.execute(&data.db)
.await?;
if let Err(err) = data.captcha.remove(RemoveCaptcha(payload.key)).await {
log::error!(
"Error while trying to remove captcha from cache {}",
err
);
}
Ok(HttpResponse::Ok())
} else {
Err(ServiceError::WrongPassword)
}
}
Err(RowNotFound) => Err(ServiceError::UsernameNotFound),
Err(_) => Err(ServiceError::InternalServerError),
}
}

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,425 +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 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(Serialize, Deserialize, Clone, Debug)]
pub struct TrafficPattern {
pub avg_traffic: u32,
pub peak_sustainable_traffic: u32,
pub broke_my_site_traffic: Option<u32>,
pub description: String,
}
impl TrafficPattern {
pub fn calculate(
&self,
strategy: &DefaultDifficultyStrategy,
) -> ServiceResult<Vec<Level>> {
let mut levels = vec![
LevelBuilder::default()
.difficulty_factor(strategy.avg_traffic_difficulty)?
.visitor_threshold(self.avg_traffic)
.build()?,
LevelBuilder::default()
.difficulty_factor(strategy.peak_sustainable_traffic_difficulty)?
.visitor_threshold(self.peak_sustainable_traffic)
.build()?,
];
let mut highest_level = LevelBuilder::default();
highest_level.difficulty_factor(strategy.broke_my_site_traffic_difficulty)?;
match self.broke_my_site_traffic {
Some(broke_my_site_traffic) => {
highest_level.visitor_threshold(broke_my_site_traffic)
}
None => match self
.peak_sustainable_traffic
.checked_add(self.peak_sustainable_traffic / 2)
{
Some(num) => highest_level.visitor_threshold(num),
// TODO check for overflow: database saves these values as i32, so this u32 is cast
// into i32. Should choose bigger number or casts properly
None => highest_level.visitor_threshold(u32::MAX),
},
};
levels.push(highest_level.build()?);
Ok(levels)
}
}
#[my_codegen::post(
path = "crate::V1_API_ROUTES.captcha.easy.create",
wrap = "crate::api::v1::get_middleware()"
)]
async fn create(
payload: web::Json<TrafficPattern>,
data: AppData,
id: Identity,
) -> ServiceResult<impl Responder> {
let username = id.identity().unwrap();
let payload = payload.into_inner();
let levels =
payload.calculate(&crate::SETTINGS.captcha.default_difficulty_strategy)?;
let msg = CreateCaptcha {
levels,
duration: crate::SETTINGS.captcha.default_difficulty_strategy.duration,
description: payload.description,
};
let broke_my_site_traffic = payload.broke_my_site_traffic.map(|n| n as i32);
let mcaptcha_config = create_runner(&msg, &data, &username).await?;
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,
&mcaptcha_config.key,
&username,
payload.avg_traffic as i32,
payload.peak_sustainable_traffic as i32,
broke_my_site_traffic,
)
.execute(&data.db)
.await?;
Ok(HttpResponse::Ok().json(mcaptcha_config))
}
#[derive(Serialize, Deserialize, Clone, Debug)]
pub struct UpdateTrafficPattern {
pub pattern: TrafficPattern,
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 levels = payload
.pattern
.calculate(&crate::SETTINGS.captcha.default_difficulty_strategy)?;
let msg = UpdateCaptcha {
levels,
duration: crate::SETTINGS.captcha.default_difficulty_strategy.duration,
description: payload.pattern.description,
key: payload.key,
};
update_captcha_runner(&msg, &data, &username).await?;
sqlx::query!(
"DELETE FROM mcaptcha_sitekey_user_provided_avg_traffic
WHERE config_id = (
SELECT config_id
FROM
mcaptcha_config
WHERE
key = ($1)
AND
user_id = (SELECT ID FROM mcaptcha_users WHERE name = $2)
);",
&msg.key,
&username,
)
.execute(&data.db)
.await?;
let broke_my_site_traffic = payload.pattern.broke_my_site_traffic.map(|n| n as i32);
sqlx::query!(
"INSERT INTO mcaptcha_sitekey_user_provided_avg_traffic (
config_id,
avg_traffic,
peak_sustainable_traffic,
broke_my_site_traffic
) VALUES (
(SELECT config_id FROM mcaptcha_config
WHERE
key = ($1)
AND user_id = (SELECT ID FROM mcaptcha_users WHERE name = $2)
), $3, $4, $5)",
//payload.avg_traffic,
&msg.key,
&username,
payload.pattern.avg_traffic as i32,
payload.pattern.peak_sustainable_traffic as i32,
broke_my_site_traffic,
)
.execute(&data.db)
.await?;
Ok(HttpResponse::Ok())
}
#[cfg(test)]
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::*;
#[cfg(test)]
mod isoloated_test {
use super::{LevelBuilder, TrafficPattern};
#[test]
fn easy_configuration_works() {
const NAME: &str = "defaultuserconfgworks";
let mut payload = TrafficPattern {
avg_traffic: 100_000,
peak_sustainable_traffic: 1_000_000,
broke_my_site_traffic: Some(10_000_000),
description: NAME.into(),
};
let strategy = &crate::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!(payload.calculate(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!(
payload.calculate(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!(
payload.calculate(strategy).unwrap(),
vec![l1, very_large_l2, lmax]
);
}
}
#[actix_rt::test]
async fn easy_works() {
const NAME: &str = "defaultuserconfgworks";
const PASSWORD: &str = "longpassworddomain";
const EMAIL: &str = "defaultuserconfgworks@a.com";
{
let data = Data::new().await;
delete_user(NAME, &data).await;
}
let (data, _creds, signin_resp) =
register_and_signin(NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
let app = get_app!(data).await;
let payload = TrafficPattern {
avg_traffic: 100_000,
peak_sustainable_traffic: 1_000_000,
broke_my_site_traffic: Some(10_000_000),
description: NAME.into(),
};
let default_levels = payload
.calculate(&crate::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 = TrafficPattern {
avg_traffic: 1_000,
peak_sustainable_traffic: 10_000,
broke_my_site_traffic: Some(1_000_000),
description: NAME.into(),
};
let updated_default_values = update_pattern
.calculate(&crate::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,76 +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 = runner::get_captcha(&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,
}
pub mod runner {
use super::*;
// TODO get metadata from mcaptcha_config table
pub async fn get_captcha(
key: &str,
username: &str,
data: &AppData,
) -> ServiceResult<Vec<I32Levels>> {
let levels = sqlx::query_as!(
I32Levels,
"SELECT difficulty_factor, visitor_threshold FROM mcaptcha_levels WHERE
config_id = (
SELECT config_id FROM mcaptcha_config WHERE key = ($1)
AND user_id = (SELECT ID from mcaptcha_users WHERE name = $2)
)
ORDER BY difficulty_factor ASC;",
key,
&username
)
.fetch_all(&data.db)
.await?;
Ok(levels)
}
}

View File

@@ -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,5 +1,5 @@
/*
* 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
* it under the terms of the GNU Affero General Public License as
@@ -15,14 +15,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
pub mod create;
pub mod delete;
pub mod easy;
pub mod get;
pub mod stats;
#[cfg(test)]
pub mod test;
pub mod update;
pub mod duration;
pub mod levels;
pub mod mcaptcha;
pub use super::auth::is_authenticated;
pub fn get_random(len: usize) -> String {
use std::iter;
@@ -37,42 +34,3 @@ pub fn get_random(len: usize) -> String {
.take(len)
.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(),
}
}
}
}

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/>.
*/
use actix_identity::Identity;
use actix_web::{web, HttpResponse, Responder};
use serde::{Deserialize, Serialize};
use crate::errors::*;
use crate::stats::fetch::{Stats, StatsUnixTimestamp};
use crate::AppData;
pub mod routes {
pub struct Stats {
pub get: &'static str,
}
impl Stats {
pub const fn new() -> Self {
Self {
get: "/api/v1/mcaptcha/stats",
}
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct StatsPayload {
pub key: String,
}
#[my_codegen::post(
path = "crate::V1_API_ROUTES.captcha.stats.get",
wrap = "crate::api::v1::get_middleware()"
)]
pub async fn get(
payload: web::Json<StatsPayload>,
data: AppData,
id: Identity,
) -> ServiceResult<impl Responder> {
let username = id.identity().unwrap();
let stats = Stats::new(&username, &payload.key, &data.db).await?;
let stats = StatsUnixTimestamp::from_stats(&stats);
Ok(HttpResponse::Ok().json(&stats))
}

View File

@@ -1,124 +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 crate::api::v1::mcaptcha::delete::DeleteCaptcha;
use libmcaptcha::defense::Level;
use crate::api::v1::mcaptcha::update::UpdateCaptcha;
use crate::api::v1::ROUTES;
use crate::data::Data;
use crate::errors::*;
use crate::tests::*;
use crate::*;
const L1: Level = Level {
difficulty_factor: 100,
visitor_threshold: 10,
};
const L2: Level = Level {
difficulty_factor: 1000,
visitor_threshold: 1000,
};
#[actix_rt::test]
async fn level_routes_work() {
const NAME: &str = "testuserlevelroutes";
const PASSWORD: &str = "longpassworddomain";
const EMAIL: &str = "testuserlevelrouts@a.com";
{
let data = Data::new().await;
delete_user(NAME, &data).await;
}
register_and_signin(NAME, EMAIL, PASSWORD).await;
// create captcha
let (data, _, signin_resp, key) = add_levels_util(NAME, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
let app = get_app!(data).await;
// 2. get captcha
let add_level = get_level_data();
let get_level_resp = test::call_service(
&app,
post_request!(&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, add_level.levels);
// 3. update captcha
let levels = vec![L1, L2];
let update_level = UpdateCaptcha {
key: key.key.clone(),
levels: levels.clone(),
description: add_level.description,
duration: add_level.duration,
};
let add_token_resp = test::call_service(
&app,
post_request!(&update_level, ROUTES.captcha.update)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(add_token_resp.status(), StatusCode::OK);
let get_level_resp = test::call_service(
&app,
post_request!(&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, levels);
// 4. delete captcha
let mut delete_payload = DeleteCaptcha {
key: key.key,
password: format!("worongpass{}", PASSWORD),
};
bad_post_req_test(
NAME,
PASSWORD,
ROUTES.captcha.delete,
&delete_payload,
ServiceError::WrongPassword,
)
.await;
delete_payload.password = PASSWORD.into();
let del_resp = test::call_service(
&app,
post_request!(&delete_payload, ROUTES.captcha.delete)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(del_resp.status(), StatusCode::OK);
}

View File

@@ -1,264 +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::borrow::Cow;
use actix_identity::Identity;
use actix_web::{web, HttpResponse, Responder};
use libmcaptcha::defense::Level;
use libmcaptcha::master::messages::RenameBuilder;
use serde::{Deserialize, Serialize};
use super::create::MCaptchaDetails;
use super::get_random;
use crate::errors::*;
use crate::AppData;
#[my_codegen::post(
path = "crate::V1_API_ROUTES.captcha.update_key",
wrap = "crate::api::v1::get_middleware()"
)]
pub async fn update_key(
payload: web::Json<MCaptchaDetails>,
data: AppData,
id: Identity,
) -> ServiceResult<impl Responder> {
let username = id.identity().unwrap();
let mut key;
loop {
key = get_random(32);
let res = runner::update_key(&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 {
return Err(sqlx::Error::Database(err).into());
}
};
}
let payload = payload.into_inner();
let rename = RenameBuilder::default()
.name(payload.key)
.rename_to(key.clone())
.build()
.unwrap();
data.captcha.rename(rename).await?;
let resp = MCaptchaDetails {
key,
name: payload.name,
};
Ok(HttpResponse::Ok().json(resp))
}
#[derive(Serialize, Deserialize)]
pub struct UpdateCaptcha {
pub levels: Vec<Level>,
pub duration: u32,
pub description: String,
pub key: String,
}
#[my_codegen::post(
path = "crate::V1_API_ROUTES.captcha.update",
wrap = "crate::api::v1::get_middleware()"
)]
pub async fn update_captcha(
payload: web::Json<UpdateCaptcha>,
data: AppData,
id: Identity,
) -> ServiceResult<impl Responder> {
let username = id.identity().unwrap();
runner::update_captcha(&payload, &data, &username).await?;
Ok(HttpResponse::Ok())
}
pub mod runner {
use futures::future::try_join_all;
use libmcaptcha::{master::messages::RemoveCaptcha, DefenseBuilder};
use super::*;
pub async fn update_key(
key: &str,
old_key: &str,
username: &str,
data: &AppData,
) -> Result<(), sqlx::Error> {
sqlx::query!(
"UPDATE mcaptcha_config SET key = $1
WHERE key = $2 AND user_id = (SELECT ID FROM mcaptcha_users WHERE name = $3)",
&key,
&old_key,
&username,
)
.execute(&data.db)
.await?;
Ok(())
}
pub async fn update_captcha(
payload: &UpdateCaptcha,
data: &AppData,
username: &str,
) -> ServiceResult<()> {
let mut defense = DefenseBuilder::default();
for level in payload.levels.iter() {
defense.add_level(*level)?;
}
// 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()?;
let mut futs = Vec::with_capacity(payload.levels.len() + 2);
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?;
let update_fut = sqlx::query!(
"UPDATE mcaptcha_config SET name = $1, duration = $2
WHERE user_id = (SELECT ID FROM mcaptcha_users WHERE name = $3)
AND key = $4",
&payload.description,
payload.duration as i32,
&username,
&payload.key,
)
.execute(&data.db); //.await?;
futs.push(update_fut);
for level in payload.levels.iter() {
let difficulty_factor = level.difficulty_factor as i32;
let visitor_threshold = level.visitor_threshold as i32;
let fut = sqlx::query!(
"INSERT INTO mcaptcha_levels (
difficulty_factor,
visitor_threshold,
config_id) VALUES (
$1, $2, (
SELECT config_id FROM mcaptcha_config WHERE key = ($3) AND
user_id = (
SELECT ID from mcaptcha_users WHERE name = $4
)
));",
difficulty_factor,
visitor_threshold,
&payload.key,
&username,
)
.execute(&data.db); //.await?;
futs.push(fut);
}
try_join_all(futs).await?;
if let Err(ServiceError::CaptchaError(e)) = data
.captcha
.remove(RemoveCaptcha(payload.key.clone()))
.await
{
log::error!(
"Deleting captcha key {} while updating it, error: {:?}",
&payload.key,
e
);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
use actix_web::http::StatusCode;
use actix_web::test;
use crate::api::v1::mcaptcha::create::MCaptchaDetails;
use crate::api::v1::mcaptcha::stats::StatsPayload;
use crate::api::v1::ROUTES;
use crate::tests::*;
use crate::*;
#[actix_rt::test]
async fn update_and_get_mcaptcha_works() {
const NAME: &str = "updateusermcaptcha";
const PASSWORD: &str = "longpassworddomain";
const EMAIL: &str = "testupdateusermcaptcha@a.com";
{
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_levels_util(NAME, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
let app = get_app!(data).await;
// 2. update token key
let update_token_resp = test::call_service(
&app,
post_request!(&token_key, ROUTES.captcha.update_key)
.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 levels with udpated key
let get_token_resp = test::call_service(
&app,
post_request!(&updated_token, ROUTES.captcha.get)
.cookie(cookies.clone())
.to_request(),
)
.await;
// if updated key doesn't exist in databse, a non 200 result will bereturned
assert_eq!(get_token_resp.status(), StatusCode::OK);
// get stats
let paylod = StatsPayload { key: token_key.key };
let get_statis_resp = test::call_service(
&app,
post_request!(&paylod, ROUTES.captcha.stats.get)
.cookie(cookies.clone())
.to_request(),
)
.await;
// if updated key doesn't exist in databse, a non 200 result will bereturned
assert_eq!(get_statis_resp.status(), StatusCode::OK);
}
}

View File

@@ -1,5 +1,5 @@
/*
* 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
* it under the terms of the GNU Affero General Public License as
@@ -15,13 +15,11 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use actix_web::{web, HttpResponse, Responder};
use actix_web::{get, web, HttpResponse, Responder};
use derive_builder::Builder;
use libmcaptcha::redis::{Redis, RedisConfig};
use serde::{Deserialize, Serialize};
use crate::data::SystemGroup;
use crate::AppData;
use crate::Data;
use crate::{GIT_COMMIT_HASH, VERSION};
#[derive(Clone, Debug, Deserialize, Builder, Serialize)]
@@ -30,28 +28,12 @@ pub struct BuildDetails {
pub git_commit_hash: &'static str,
}
pub mod routes {
pub struct Meta {
pub build_details: &'static str,
pub health: &'static str,
}
impl Meta {
pub const fn new() -> Self {
Self {
build_details: "/api/v1/meta/build",
health: "/api/v1/meta/health",
}
}
}
}
#[get("/api/v1/meta/build")]
/// emmits build details of the bninary
#[my_codegen::get(path = "crate::V1_API_ROUTES.meta.build_details")]
async fn build_details() -> impl Responder {
pub async fn build_details() -> impl Responder {
let build = BuildDetails {
version: VERSION,
git_commit_hash: GIT_COMMIT_HASH,
git_commit_hash: &GIT_COMMIT_HASH,
};
HttpResponse::Ok().json(build)
}
@@ -60,91 +42,54 @@ async fn build_details() -> impl Responder {
/// Health check return datatype
pub struct Health {
db: bool,
#[serde(skip_serializing_if = "Self::is_redis")]
redis: Option<bool>,
}
impl Health {
fn is_redis(redis: &Option<bool>) -> bool {
redis.is_none()
}
}
#[get("/api/v1/meta/health")]
/// checks all components of the system
#[my_codegen::get(path = "crate::V1_API_ROUTES.meta.health")]
async fn health(data: AppData) -> impl Responder {
pub async fn health(data: web::Data<Data>) -> impl Responder {
use sqlx::Connection;
let mut resp_builder = HealthBuilder::default();
resp_builder.db(false);
resp_builder.redis = None;
if let Ok(mut con) = data.db.acquire().await {
if con.ping().await.is_ok() {
if let Ok(_) = con.ping().await {
resp_builder.db(true);
}
};
if let SystemGroup::Redis(_) = data.captcha {
if let Ok(r) = Redis::new(RedisConfig::Single(
crate::SETTINGS.redis.as_ref().unwrap().url.clone(),
))
.await
{
let status = r.get_client().ping().await;
resp_builder.redis = Some(Some(status));
} else {
resp_builder.redis = Some(Some(false));
}
};
HttpResponse::Ok().json(resp_builder.build().unwrap())
}
pub fn services(cfg: &mut web::ServiceConfig) {
cfg.service(build_details);
cfg.service(health);
}
#[cfg(test)]
mod tests {
use actix_web::{http::StatusCode, test, App};
use super::*;
use crate::api::v1::services;
use crate::api::v1::services as v1_services;
use crate::*;
#[actix_rt::test]
async fn build_details_works() {
let app = test::init_service(App::new().configure(services)).await;
const GET_URI: &str = "/api/v1/meta/build";
let mut app = test::init_service(App::new().configure(v1_services)).await;
let resp = test::call_service(
&app,
test::TestRequest::get()
.uri(V1_API_ROUTES.meta.build_details)
.to_request(),
)
.await;
let resp =
test::call_service(&mut app, test::TestRequest::get().uri(GET_URI).to_request()).await;
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn health_works() {
println!("{}", V1_API_ROUTES.meta.health);
let data = Data::new().await;
let app = get_app!(data).await;
const GET_URI: &str = "/api/v1/meta/health";
let resp = test::call_service(
&app,
test::TestRequest::get()
.uri(V1_API_ROUTES.meta.health)
.to_request(),
)
.await;
let data = Data::new().await;
let mut app = get_app!(data).await;
let resp =
test::call_service(&mut app, test::TestRequest::get().uri(GET_URI).to_request()).await;
assert_eq!(resp.status(), StatusCode::OK);
let health_resp: Health = test::read_body_json(resp).await;
assert!(health_resp.db);
assert_eq!(health_resp.redis, Some(true));
assert_eq!(health_resp.db, true);
}
}

View File

@@ -1,5 +1,5 @@
/*
* 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
* it under the terms of the GNU Affero General Public License as
@@ -15,36 +15,43 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use actix_auth_middleware::Authentication;
use actix_web::web::ServiceConfig;
use serde::Deserialize;
pub mod account;
pub mod auth;
pub mod mcaptcha;
pub mod meta;
pub mod notifications;
pub mod pow;
mod routes;
pub use routes::ROUTES;
pub fn services(cfg: &mut ServiceConfig) {
meta::services(cfg);
pow::services(cfg);
auth::services(cfg);
account::services(cfg);
mcaptcha::services(cfg);
notifications::services(cfg);
}
// meta
cfg.service(meta::build_details);
cfg.service(meta::health);
#[derive(Deserialize)]
pub struct RedirectQuery {
pub redirect_to: Option<String>,
}
// auth
cfg.service(auth::signout);
cfg.service(auth::signin);
cfg.service(auth::signup);
cfg.service(auth::delete_account);
cfg.service(auth::username_exists);
cfg.service(auth::email_exists);
cfg.service(auth::get_secret);
cfg.service(auth::update_user_secret);
pub fn get_middleware() -> Authentication<routes::Routes> {
Authentication::with_identity(ROUTES)
// mcaptcha
cfg.service(mcaptcha::mcaptcha::add_mcaptcha);
cfg.service(mcaptcha::mcaptcha::delete_mcaptcha);
cfg.service(mcaptcha::mcaptcha::update_token);
cfg.service(mcaptcha::mcaptcha::get_token);
// levels
cfg.service(mcaptcha::levels::add_levels);
cfg.service(mcaptcha::levels::update_levels);
cfg.service(mcaptcha::levels::delete_levels);
cfg.service(mcaptcha::levels::get_levels);
// duration
cfg.service(mcaptcha::duration::update_duration);
cfg.service(mcaptcha::duration::get_duration);
}
#[cfg(test)]

View File

@@ -1,106 +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 crate::errors::*;
use crate::AppData;
#[derive(Serialize, Deserialize)]
pub struct AddNotification {
pub to: String,
pub heading: String,
pub message: String,
}
/// route handler that adds a notification message
#[my_codegen::post(
path = "crate::V1_API_ROUTES.notifications.add",
wrap = "crate::api::v1::get_middleware()"
)]
pub async fn add_notification(
payload: web::Json<AddNotification>,
data: AppData,
id: Identity,
) -> ServiceResult<impl Responder> {
let sender = id.identity().unwrap();
// TODO handle error where payload.to doesnt exist
sqlx::query!(
"INSERT INTO mcaptcha_notifications (
heading, message, tx, rx)
VALUES (
$1, $2,
(SELECT ID FROM mcaptcha_users WHERE name = $3),
(SELECT ID FROM mcaptcha_users WHERE name = $4)
);",
&payload.heading,
&payload.message,
&sender,
&payload.to,
)
.execute(&data.db)
.await?;
Ok(HttpResponse::Ok())
}
#[cfg(test)]
mod tests {
use actix_web::http::StatusCode;
use actix_web::test;
use super::*;
use crate::tests::*;
use crate::*;
#[actix_rt::test]
async fn notification_works() {
const NAME1: &str = "notifuser1";
const NAME2: &str = "notiuser2";
const PASSWORD: &str = "longpassworddomain";
const EMAIL1: &str = "testnotification1@a.com";
const EMAIL2: &str = "testnotification2@a.com";
{
let data = Data::new().await;
delete_user(NAME1, &data).await;
delete_user(NAME2, &data).await;
}
register_and_signin(NAME1, EMAIL1, PASSWORD).await;
register_and_signin(NAME2, EMAIL2, PASSWORD).await;
let (data, _creds, signin_resp) = signin(NAME1, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
let app = get_app!(data).await;
let msg = AddNotification {
to: NAME2.into(),
heading: "Test notification".into(),
message: "Testeing notifications with a dummy message".into(),
};
let send_notification_resp = test::call_service(
&app,
post_request!(&msg, V1_API_ROUTES.notifications.add)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(send_notification_resp.status(), StatusCode::OK);
}
}

View File

@@ -1,165 +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 serde::{Deserialize, Serialize};
use sqlx::types::time::OffsetDateTime;
use crate::errors::*;
use crate::AppData;
pub struct Notification {
pub name: Option<String>,
pub heading: Option<String>,
pub message: Option<String>,
pub received: Option<OffsetDateTime>,
pub id: Option<i32>,
}
#[derive(Deserialize, Serialize)]
pub struct NotificationResp {
pub name: String,
pub heading: String,
pub message: String,
pub received: i64,
pub id: i32,
}
impl From<Notification> for NotificationResp {
fn from(n: Notification) -> Self {
NotificationResp {
name: n.name.unwrap(),
heading: n.heading.unwrap(),
received: n.received.unwrap().unix_timestamp(),
id: n.id.unwrap(),
message: n.message.unwrap(),
}
}
}
/// route handler that gets all unread notifications
#[my_codegen::get(
path = "crate::V1_API_ROUTES.notifications.get",
wrap = "crate::api::v1::get_middleware()"
)]
pub async fn get_notification(
data: AppData,
id: Identity,
) -> ServiceResult<impl Responder> {
let receiver = id.identity().unwrap();
// TODO handle error where payload.to doesnt exist
let mut notifications = runner::get_notification(&data, &receiver).await?;
let resp: Vec<NotificationResp> = notifications
.drain(0..)
.map(|x| {
let y: NotificationResp = x.into();
y
})
.collect();
Ok(HttpResponse::Ok().json(resp))
}
pub mod runner {
use super::*;
pub async fn get_notification(
data: &AppData,
receiver: &str,
) -> ServiceResult<Vec<Notification>> {
// TODO handle error where payload.to doesnt exist
let notifications = sqlx::query_file_as!(
Notification,
"src/api/v1/notifications/get_all_unread.sql",
&receiver
)
.fetch_all(&data.db)
.await?;
Ok(notifications)
}
}
#[cfg(test)]
mod tests {
use actix_web::http::StatusCode;
use actix_web::test;
use super::*;
use crate::api::v1::notifications::add::AddNotification;
use crate::tests::*;
use crate::*;
#[actix_rt::test]
async fn notification_get_works() {
const NAME1: &str = "notifuser12";
const NAME2: &str = "notiuser22";
const PASSWORD: &str = "longpassworddomain";
const EMAIL1: &str = "testnotification12@a.com";
const EMAIL2: &str = "testnotification22@a.com";
const HEADING: &str = "testing notifications get";
const MESSAGE: &str = "testing notifications get message";
{
let data = Data::new().await;
delete_user(NAME1, &data).await;
delete_user(NAME2, &data).await;
}
register_and_signin(NAME1, EMAIL1, PASSWORD).await;
register_and_signin(NAME2, EMAIL2, PASSWORD).await;
let (data, _creds, signin_resp) = signin(NAME1, PASSWORD).await;
let (_data, _creds2, signin_resp2) = signin(NAME2, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
let cookies2 = get_cookie!(signin_resp2);
let app = get_app!(data).await;
let msg = AddNotification {
to: NAME2.into(),
heading: HEADING.into(),
message: MESSAGE.into(),
};
let send_notification_resp = test::call_service(
&app,
post_request!(&msg, V1_API_ROUTES.notifications.add)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(send_notification_resp.status(), StatusCode::OK);
let get_notifications_resp = test::call_service(
&app,
test::TestRequest::get()
.uri(V1_API_ROUTES.notifications.get)
.cookie(cookies2.clone())
.to_request(),
)
.await;
assert_eq!(get_notifications_resp.status(), StatusCode::OK);
let mut notifications: Vec<NotificationResp> =
test::read_body_json(get_notifications_resp).await;
let notification = notifications.pop().unwrap();
assert_eq!(notification.name, NAME1);
assert_eq!(notification.message, MESSAGE);
assert_eq!(notification.heading, HEADING);
}
}

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,155 +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 crate::errors::*;
use crate::AppData;
#[derive(Deserialize, Serialize)]
pub struct MarkReadReq {
pub id: i32,
}
#[derive(Deserialize, Serialize)]
pub struct NotificationResp {
pub name: String,
pub heading: String,
pub message: String,
pub received: i64,
pub id: i32,
}
/// route handler that marks a notification read
#[my_codegen::post(
path = "crate::V1_API_ROUTES.notifications.mark_read",
wrap = "crate::api::v1::get_middleware()"
)]
pub async fn mark_read(
data: AppData,
payload: web::Json<MarkReadReq>,
id: Identity,
) -> ServiceResult<impl Responder> {
let receiver = id.identity().unwrap();
// TODO handle error where payload.to doesnt exist
sqlx::query_file_as!(
Notification,
"src/api/v1/notifications/mark_read.sql",
payload.id,
&receiver
)
.execute(&data.db)
.await?;
Ok(HttpResponse::Ok())
}
#[cfg(test)]
mod tests {
use actix_web::http::StatusCode;
use actix_web::test;
use super::*;
use crate::api::v1::notifications::add::AddNotification;
use crate::tests::*;
use crate::*;
#[actix_rt::test]
async fn notification_mark_read_works() {
const NAME1: &str = "notifuser122";
const NAME2: &str = "notiuser222";
const PASSWORD: &str = "longpassworddomain";
const EMAIL1: &str = "testnotification122@a.com";
const EMAIL2: &str = "testnotification222@a.com";
const HEADING: &str = "testing notifications get";
const MESSAGE: &str = "testing notifications get message";
{
let data = Data::new().await;
delete_user(NAME1, &data).await;
delete_user(NAME2, &data).await;
}
register_and_signin(NAME1, EMAIL1, PASSWORD).await;
register_and_signin(NAME2, EMAIL2, PASSWORD).await;
let (data, _creds, signin_resp) = signin(NAME1, PASSWORD).await;
let (_data, _creds2, signin_resp2) = signin(NAME2, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
let cookies2 = get_cookie!(signin_resp2);
let app = get_app!(data).await;
let msg = AddNotification {
to: NAME2.into(),
heading: HEADING.into(),
message: MESSAGE.into(),
};
let send_notification_resp = test::call_service(
&app,
post_request!(&msg, V1_API_ROUTES.notifications.add)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(send_notification_resp.status(), StatusCode::OK);
let get_notifications_resp = test::call_service(
&app,
test::TestRequest::get()
.uri(V1_API_ROUTES.notifications.get)
.cookie(cookies2.clone())
.to_request(),
)
.await;
assert_eq!(get_notifications_resp.status(), StatusCode::OK);
let mut notifications: Vec<NotificationResp> =
test::read_body_json(get_notifications_resp).await;
let notification = notifications.pop().unwrap();
assert_eq!(notification.name, NAME1);
assert_eq!(notification.message, MESSAGE);
assert_eq!(notification.heading, HEADING);
let mark_read_payload = MarkReadReq {
id: notification.id,
};
let mark_read_resp = test::call_service(
&app,
post_request!(&mark_read_payload, V1_API_ROUTES.notifications.mark_read)
.cookie(cookies2.clone())
.to_request(),
)
.await;
assert_eq!(mark_read_resp.status(), StatusCode::OK);
let get_notifications_resp = test::call_service(
&app,
test::TestRequest::get()
.uri(V1_API_ROUTES.notifications.get)
.cookie(cookies2.clone())
.to_request(),
)
.await;
assert_eq!(get_notifications_resp.status(), StatusCode::OK);
let mut notifications: Vec<NotificationResp> =
test::read_body_json(get_notifications_resp).await;
assert!(notifications.pop().is_none());
}
}

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,45 +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/>.
*/
pub mod add;
pub mod get;
pub mod mark_read;
pub mod routes {
pub struct Notifications {
pub add: &'static str,
pub mark_read: &'static str,
pub get: &'static str,
}
impl Notifications {
pub const fn new() -> Notifications {
Notifications {
add: "/api/v1/notifications/add",
mark_read: "/api/v1/notifications/read",
get: "/api/v1/notifications/get",
}
}
}
}
pub fn services(cfg: &mut actix_web::web::ServiceConfig) {
cfg.service(add::add_notification);
cfg.service(get::get_notification);
cfg.service(mark_read::mark_read);
}

View File

@@ -1,5 +1,5 @@
/*
* 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
* it under the terms of the GNU Affero General Public License as
@@ -15,19 +15,21 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
//use actix::prelude::*;
use actix_web::{web, HttpResponse, Responder};
use libmcaptcha::{
defense::LevelBuilder, master::messages::AddSiteBuilder, DefenseBuilder,
MCaptchaBuilder,
};
use actix::prelude::*;
use actix_web::{post, web, HttpResponse, Responder};
use m_captcha::{defense::LevelBuilder, master::AddSiteBuilder, DefenseBuilder, MCaptchaBuilder};
use serde::{Deserialize, Serialize};
use super::GetDurationResp;
use super::I32Levels;
use crate::errors::*;
use crate::stats::record::record_fetch;
use crate::AppData;
use crate::V1_API_ROUTES;
use crate::Data;
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct PoWConfig {
pub name: String,
pub domain: String,
}
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct GetConfigPayload {
@@ -36,11 +38,11 @@ pub struct GetConfigPayload {
// API keys are mcaptcha actor names
/// get PoW configuration for an mcaptcha key
#[my_codegen::post(path = "V1_API_ROUTES.pow.get_config()")]
#[post("/config")]
//#[post("/pow/config")]
pub async fn get_config(
payload: web::Json<GetConfigPayload>,
data: AppData,
data: web::Data<Data>,
) -> ServiceResult<impl Responder> {
let res = sqlx::query!(
"SELECT EXISTS (SELECT 1 from mcaptcha_config WHERE key = $1)",
@@ -55,24 +57,18 @@ pub async fn get_config(
let payload = payload.into_inner();
match res.exists {
Some(true) => {
();
match data.captcha.get_pow(payload.key.clone()).await {
Ok(Some(config)) => {
record_fetch(&payload.key, &data.db).await;
Ok(HttpResponse::Ok().json(config))
}
Ok(None) => {
Some(config) => Ok(HttpResponse::Ok().json(config)),
None => {
init_mcaptcha(&data, &payload.key).await?;
let config = data
.captcha
.get_pow(payload.key.clone())
.get_pow(payload.key)
.await
.expect("mcaptcha should be initialized and ready to go");
// background it. would require data::Data to be static
// to satidfy lifetime
record_fetch(&payload.key, &data.db).await;
Ok(HttpResponse::Ok().json(config))
}
Err(e) => Err(e.into()),
}
}
@@ -80,28 +76,21 @@ pub async fn get_config(
None => Err(ServiceError::TokenNotFound),
}
}
/// Call this when [MCaptcha][libmcaptcha::MCaptcha] is not in master.
///
/// This fn gets mcaptcha config from database, builds [Defense][libmcaptcha::Defense],
/// creates [MCaptcha][libmcaptcha::MCaptcha] and adds it to [Master][libmcaptcha::Defense]
async fn init_mcaptcha(data: &AppData, key: &str) -> ServiceResult<()> {
async fn init_mcaptcha(data: &Data, key: &str) -> ServiceResult<()> {
// get levels
let levels_fut = 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;",
);",
&key,
)
.fetch_all(&data.db);
struct DurationResp {
duration: i32,
}
// get duration
let duration_fut = sqlx::query_as!(
DurationResp,
GetDurationResp,
"SELECT duration FROM mcaptcha_config
WHERE key = $1",
&key,
@@ -132,34 +121,37 @@ async fn init_mcaptcha(data: &AppData, key: &str) -> ServiceResult<()> {
.duration(duration.duration as u64)
// .cache(cache)
.build()
.unwrap();
.unwrap()
.start();
// add captcha to master
let msg = AddSiteBuilder::default()
.id(key.into())
.mcaptcha(mcaptcha)
.addr(mcaptcha.clone())
.build()
.unwrap();
data.captcha.add_site(msg).await?;
data.captcha.master.send(msg).await.unwrap();
Ok(())
}
#[cfg(test)]
mod tests {
use libmcaptcha::pow::PoWConfig;
use actix_web::http::{header, StatusCode};
use actix_web::test;
use m_captcha::pow::PoWConfig;
#[actix_rt::test]
async fn get_pow_config_works() {
use super::*;
use crate::tests::*;
use crate::*;
use actix_web::test;
#[actix_rt::test]
async fn get_pow_config_works() {
const NAME: &str = "powusrworks";
const PASSWORD: &str = "testingpas";
const EMAIL: &str = "randomuser@a.com";
const GET_URL: &str = "/api/v1/pow/config";
// const UPDATE_URL: &str = "/api/v1/mcaptcha/domain/token/duration/update";
{
let data = Data::new().await;
@@ -167,8 +159,9 @@ mod tests {
}
register_and_signin(NAME, EMAIL, PASSWORD).await;
let (data, _, _signin_resp, token_key) = add_levels_util(NAME, PASSWORD).await;
let app = get_app!(data).await;
let (data, _, signin_resp, token_key) = add_levels_util(NAME, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
let mut app = get_app!(data).await;
let get_config_payload = GetConfigPayload {
key: token_key.key.clone(),
@@ -176,11 +169,10 @@ mod tests {
// update and check changes
let url = V1_API_ROUTES.pow.get_config;
println!("{}", &url);
let get_config_resp = test::call_service(
&app,
post_request!(&get_config_payload, V1_API_ROUTES.pow.get_config)
&mut app,
post_request!(&get_config_payload, GET_URL)
.cookie(cookies.clone())
.to_request(),
)
.await;

View File

@@ -1,5 +1,5 @@
/*
* 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
* it under the terms of the GNU Affero General Public License as
@@ -15,82 +15,64 @@
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use actix_cors::Cors;
use actix_web::web;
pub mod get_config;
pub mod verify_pow;
pub mod verify_token;
pub use super::mcaptcha::get::I32Levels;
pub use super::mcaptcha::duration::GetDurationResp;
pub use super::mcaptcha::is_authenticated;
pub use super::mcaptcha::levels::I32Levels;
//lazy_static! {
// pub static ref CORS: Cors = Cors::default()
// .allow_any_origin()
// .allowed_methods(vec!["POST"])
// .allow_any_header()
// .max_age(0)
// .send_wildcard();
//}
//pub fn services(cfg: &mut web::ServiceConfig) -> web::Scope<impl actix_service::ServiceFactory> {
// let captcha_api_cors = Cors::default()
// .allow_any_origin()
// .allowed_methods(vec!["POST"])
// .allow_any_header()
// .max_age(0)
// .send_wildcard();
//
// web::scope("/api/v1/pow/*")
// .wrap(captcha_api_cors)
// .configure(pow_services)
//
// // pow
//}
pub fn services(cfg: &mut web::ServiceConfig) {
let cors = actix_cors::Cors::default()
let captcha_api_cors = Cors::default()
.allow_any_origin()
.allowed_methods(vec!["POST", "GET"])
.allowed_methods(vec!["POST"])
.allow_any_header()
.max_age(3600)
.max_age(0)
.send_wildcard();
let routes = crate::V1_API_ROUTES.pow;
cfg.service(
web::scope(routes.scope)
.wrap(cors)
.service(verify_pow::verify_pow)
.service(get_config::get_config)
.service(verify_token::validate_captcha_token),
web::scope("/api/v1/pow/")
.wrap(captcha_api_cors)
.configure(intenral_services),
);
// cfg.service(
// cfg.service(get_config::get_config);
// cfg.service(verify_pow::verify_pow);
// cfg.service(verify_token::validate_captcha_token);
}
pub mod routes {
pub struct PoW {
pub get_config: &'static str,
pub verify_pow: &'static str,
pub validate_captcha_token: &'static str,
pub scope: &'static str,
}
macro_rules! rm_scope {
($name:ident) => {
/// remove scope for $name route
pub fn $name(&self) -> &str {
self.$name
//.strip_prefix(&self.scope[..self.scope.len() - 1])
.strip_prefix(self.scope)
.unwrap()
}
};
}
impl PoW {
pub const fn new() -> Self {
// date: 2021-11-29 16:31
// commit: 6eb75d7
// route 404s when scope contained trailing slash
//let scope = "/api/v1/pow/";
let scope = "/api/v1/pow";
PoW {
get_config: "/api/v1/pow/config",
verify_pow: "/api/v1/pow/verify",
validate_captcha_token: "/api/v1/pow/siteverify",
scope,
}
}
rm_scope!(get_config);
rm_scope!(verify_pow);
rm_scope!(validate_captcha_token);
}
}
#[cfg(test)]
mod tests {
use super::routes::PoW;
#[test]
fn scope_pow_works() {
let pow = PoW::new();
assert_eq!(pow.get_config(), "/config");
assert_eq!(pow.verify_pow(), "/verify");
assert_eq!(pow.validate_captcha_token(), "/siteverify");
}
fn intenral_services(cfg: &mut web::ServiceConfig) {
cfg.service(get_config::get_config);
cfg.service(verify_pow::verify_pow);
cfg.service(verify_token::validate_captcha_token);
}

View File

@@ -1,5 +1,5 @@
/*
* 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
* it under the terms of the GNU Affero General Public License as
@@ -14,45 +14,36 @@
* 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/>.
*/
//! PoW Verification module
use actix_web::{web, HttpResponse, Responder};
use libmcaptcha::pow::Work;
use actix_web::{post, web, HttpResponse, Responder};
use m_captcha::pow::Work;
use serde::{Deserialize, Serialize};
use crate::errors::*;
use crate::stats::record::record_solve;
use crate::AppData;
use crate::V1_API_ROUTES;
use crate::Data;
#[derive(Clone, Debug, Deserialize, Serialize)]
/// validation token that clients receive as proof for submiting
/// valid PoW
pub struct ValidationToken {
pub token: String,
}
// API keys are mcaptcha actor names
/// route handler that verifies PoW and issues a solution token
/// if verification is successful
#[my_codegen::post(path = "V1_API_ROUTES.pow.verify_pow()")]
#[post("/verify")]
pub async fn verify_pow(
payload: web::Json<Work>,
data: AppData,
data: web::Data<Data>,
) -> ServiceResult<impl Responder> {
let key = payload.key.clone();
let res = data.captcha.verify_pow(payload.into_inner()).await?;
record_solve(&key, &data.db).await;
let payload = ValidationToken { token: res };
Ok(HttpResponse::Ok().json(payload))
}
#[cfg(test)]
mod tests {
use actix_web::http::StatusCode;
use actix_web::http::{header, StatusCode};
use actix_web::test;
use libmcaptcha::pow::PoWConfig;
use m_captcha::pow::PoWConfig;
use super::*;
use crate::api::v1::pow::get_config::GetConfigPayload;
@@ -64,6 +55,9 @@ mod tests {
const NAME: &str = "powverifyusr";
const PASSWORD: &str = "testingpas";
const EMAIL: &str = "verifyuser@a.com";
const VERIFY_URL: &str = "/api/v1/pow/verify";
const GET_URL: &str = "/api/v1/pow/config";
// const UPDATE_URL: &str = "/api/v1/mcaptcha/domain/token/duration/update";
{
let data = Data::new().await;
@@ -72,7 +66,7 @@ mod tests {
register_and_signin(NAME, EMAIL, PASSWORD).await;
let (data, _, _signin_resp, token_key) = add_levels_util(NAME, PASSWORD).await;
let app = get_app!(data).await;
let mut app = get_app!(data).await;
let get_config_payload = GetConfigPayload {
key: token_key.key.clone(),
@@ -81,9 +75,8 @@ mod tests {
// update and check changes
let get_config_resp = test::call_service(
&app,
post_request!(&get_config_payload, V1_API_ROUTES.pow.get_config)
.to_request(),
&mut app,
post_request!(&get_config_payload, GET_URL).to_request(),
)
.await;
assert_eq!(get_config_resp.status(), StatusCode::OK);
@@ -104,32 +97,32 @@ mod tests {
key: token_key.key.clone(),
};
let pow_verify_resp = test::call_service(
&app,
post_request!(&work, V1_API_ROUTES.pow.verify_pow).to_request(),
)
.await;
let pow_verify_resp =
test::call_service(&mut app, post_request!(&work, VERIFY_URL).to_request()).await;
assert_eq!(pow_verify_resp.status(), StatusCode::OK);
let string_not_found = test::call_service(
&app,
post_request!(&work, V1_API_ROUTES.pow.verify_pow).to_request(),
)
.await;
let string_not_found =
test::call_service(&mut app, post_request!(&work, VERIFY_URL).to_request()).await;
assert_eq!(string_not_found.status(), StatusCode::BAD_REQUEST);
let err: ErrorToResponse = test::read_body_json(string_not_found).await;
assert_eq!(err.error, "Challenge: not found");
assert_eq!(
err.error,
format!(
"{}",
ServiceError::CaptchaError(m_captcha::errors::CaptchaError::StringNotFound)
)
);
// let pow_config_resp = test::call_service(
// &app,
// post_request!(&get_config_payload, V1_API_ROUTES.pow.get_config).to_request(),
// )
// .await;
// assert_eq!(pow_config_resp.status(), StatusCode::OK);
let pow_config_resp = test::call_service(
&mut app,
post_request!(&get_config_payload, GET_URL).to_request(),
)
.await;
assert_eq!(pow_config_resp.status(), StatusCode::OK);
// I'm not checking for errors because changing work.result triggered
// InssuficientDifficulty, which is possible becuase libmcaptcha calculates
// InssuficientDifficulty, which is possible becuase m_captcha calculates
// difficulty with the submitted result. Besides, this endpoint is merely
// propagating errors from libmcaptcha and libmcaptcha has tests covering the
// propagating errors from m_captcha and m_captcha has tests covering the
// pow aspects ¯\_(ツ)_/¯
}
}

View File

@@ -1,5 +1,5 @@
/*
* 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
* it under the terms of the GNU Affero General Public License as
@@ -14,16 +14,13 @@
* 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/>.
*/
//! PoW success token module
use actix_web::{web, HttpResponse, Responder};
use libmcaptcha::cache::messages::VerifyCaptchaResult;
use actix_web::{post, web, HttpResponse, Responder};
use m_captcha::cache::messages::VerifyCaptchaResult;
use serde::{Deserialize, Serialize};
use crate::errors::*;
use crate::stats::record::record_confirm;
use crate::AppData;
use crate::V1_API_ROUTES;
use crate::Data;
#[derive(Clone, Debug, Deserialize, Serialize)]
pub struct CaptchaValidateResp {
@@ -32,29 +29,26 @@ pub struct CaptchaValidateResp {
// API keys are mcaptcha actor names
/// route hander that validates a PoW solution token
#[my_codegen::post(path = "V1_API_ROUTES.pow.validate_captcha_token()")]
#[post("/siteverify")]
pub async fn validate_captcha_token(
payload: web::Json<VerifyCaptchaResult>,
data: AppData,
data: web::Data<Data>,
) -> ServiceResult<impl Responder> {
let key = payload.key.clone();
let res = data
.captcha
.validate_verification_tokens(payload.into_inner())
.await?;
let payload = CaptchaValidateResp { valid: res };
record_confirm(&key, &data.db).await;
//println!("{:?}", &payload);
println!("{:?}", &payload);
Ok(HttpResponse::Ok().json(payload))
}
#[cfg(test)]
mod tests {
use actix_web::http::StatusCode;
use actix_web::http::{header, StatusCode};
use actix_web::test;
use libmcaptcha::pow::PoWConfig;
use libmcaptcha::pow::Work;
use m_captcha::pow::PoWConfig;
use m_captcha::pow::Work;
use super::*;
use crate::api::v1::pow::get_config::GetConfigPayload;
@@ -79,7 +73,7 @@ mod tests {
register_and_signin(NAME, EMAIL, PASSWORD).await;
let (data, _, _signin_resp, token_key) = add_levels_util(NAME, PASSWORD).await;
let app = get_app!(data).await;
let mut app = get_app!(data).await;
let get_config_payload = GetConfigPayload {
key: token_key.key.clone(),
@@ -88,7 +82,7 @@ mod tests {
// update and check changes
let get_config_resp = test::call_service(
&app,
&mut app,
post_request!(&get_config_payload, GET_URL).to_request(),
)
.await;
@@ -111,7 +105,7 @@ mod tests {
};
let pow_verify_resp = test::call_service(
&app,
&mut app,
post_request!(&work, VERIFY_CAPTCHA_URL).to_request(),
)
.await;
@@ -124,18 +118,17 @@ mod tests {
};
let validate_client_token = test::call_service(
&app,
&mut app,
post_request!(&validate_payload, VERIFY_TOKEN_URL).to_request(),
)
.await;
assert_eq!(validate_client_token.status(), StatusCode::OK);
let resp: CaptchaValidateResp =
test::read_body_json(validate_client_token).await;
let resp: CaptchaValidateResp = test::read_body_json(validate_client_token).await;
assert!(resp.valid);
// string not found
let string_not_found = test::call_service(
&app,
&mut app,
post_request!(&validate_payload, VERIFY_TOKEN_URL).to_request(),
)
.await;
@@ -149,7 +142,7 @@ mod tests {
// key not found
let key_not_found = test::call_service(
&app,
&mut app,
post_request!(&validate_payload, VERIFY_TOKEN_URL).to_request(),
)
.await;

View File

@@ -1,54 +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_auth_middleware::GetLoginRoute;
use super::account::routes::Account;
use super::auth::routes::Auth;
use super::mcaptcha::routes::Captcha;
use super::meta::routes::Meta;
use super::notifications::routes::Notifications;
use super::pow::routes::PoW;
pub const ROUTES: Routes = Routes::new();
pub struct Routes {
pub auth: Auth,
pub account: Account,
pub captcha: Captcha,
pub meta: Meta,
pub pow: PoW,
pub notifications: Notifications,
}
impl Routes {
const fn new() -> Routes {
Routes {
auth: Auth::new(),
account: Account::new(),
captcha: Captcha::new(),
meta: Meta::new(),
pow: PoW::new(),
notifications: Notifications::new(),
}
}
}
impl GetLoginRoute for Routes {
fn get_login_route(&self, src: Option<&str>) -> String {
self.auth.get_login_route(src)
}
}

View File

@@ -1,5 +1,5 @@
/*
* 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
* it under the terms of the GNU Affero General Public License as
@@ -18,8 +18,7 @@
use actix_web::http::{header, StatusCode};
use actix_web::test;
use crate::api::v1::auth::runners::{Login, Register};
use crate::api::v1::ROUTES;
use crate::api::v1::auth::*;
use crate::data::Data;
use crate::errors::*;
use crate::*;
@@ -32,8 +31,11 @@ async fn auth_works() {
const NAME: &str = "testuser";
const PASSWORD: &str = "longpassword";
const EMAIL: &str = "testuser1@a.com";
const SIGNIN: &str = "/api/v1/signin";
const SIGNUP: &str = "/api/v1/signup";
const GET_SECRET: &str = "/api/v1/account/secret/";
let app = get_app!(data).await;
let mut app = get_app!(data).await;
delete_user(NAME, &data).await;
@@ -41,12 +43,9 @@ async fn auth_works() {
let msg = Register {
username: NAME.into(),
password: PASSWORD.into(),
confirm_password: PASSWORD.into(),
email: None,
};
let resp =
test::call_service(&app, post_request!(&msg, ROUTES.auth.register).to_request())
.await;
let resp = test::call_service(&mut app, post_request!(&msg, SIGNUP).to_request()).await;
assert_eq!(resp.status(), StatusCode::OK);
// delete user
delete_user(NAME, &data).await;
@@ -55,111 +54,179 @@ async fn auth_works() {
let (_, _, signin_resp) = register_and_signin(NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
// Sign in with email
signin(EMAIL, PASSWORD).await;
// chech if get user secret works
let resp = test::call_service(
&mut app,
test::TestRequest::get()
.cookie(cookies.clone())
.uri(GET_SECRET)
.to_request(),
)
.await;
assert_eq!(resp.status(), StatusCode::OK);
// check if update user secret works
let resp = test::call_service(
&mut app,
test::TestRequest::post()
.cookie(cookies.clone())
.uri(GET_SECRET)
.to_request(),
)
.await;
assert_eq!(resp.status(), StatusCode::OK);
// 2. check if duplicate username is allowed
let mut msg = Register {
let msg = Register {
username: NAME.into(),
password: PASSWORD.into(),
confirm_password: PASSWORD.into(),
email: Some(EMAIL.into()),
};
bad_post_req_test(
NAME,
PASSWORD,
ROUTES.auth.register,
SIGNUP,
&msg,
ServiceError::UsernameTaken,
)
.await;
let name = format!("{}dupemail", NAME);
msg.username = name;
bad_post_req_test(
NAME,
PASSWORD,
ROUTES.auth.register,
&msg,
ServiceError::EmailTaken,
StatusCode::BAD_REQUEST,
)
.await;
// 3. sigining in with non-existent user
let mut creds = Login {
login: "nonexistantuser".into(),
let mut login = Login {
username: "nonexistantuser".into(),
password: msg.password.clone(),
};
bad_post_req_test(
NAME,
PASSWORD,
ROUTES.auth.login,
&creds,
ServiceError::AccountNotFound,
)
.await;
creds.login = "nonexistantuser@example.com".into();
bad_post_req_test(
NAME,
PASSWORD,
ROUTES.auth.login,
&creds,
ServiceError::AccountNotFound,
SIGNIN,
&login,
ServiceError::UsernameNotFound,
StatusCode::NOT_FOUND,
)
.await;
// 4. trying to signin with wrong password
creds.login = NAME.into();
creds.password = NAME.into();
login.username = NAME.into();
login.password = NAME.into();
bad_post_req_test(
NAME,
PASSWORD,
ROUTES.auth.login,
&creds,
SIGNIN,
&login,
ServiceError::WrongPassword,
StatusCode::UNAUTHORIZED,
)
.await;
// 5. signout
let signout_resp = test::call_service(
&app,
test::TestRequest::get()
.uri(ROUTES.auth.logout)
&mut app,
test::TestRequest::post()
.uri("/api/v1/signout")
.cookie(cookies)
.to_request(),
)
.await;
assert_eq!(signout_resp.status(), StatusCode::FOUND);
let headers = signout_resp.headers();
assert_eq!(headers.get(header::LOCATION).unwrap(), PAGES.auth.login);
assert_eq!(signout_resp.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn serverside_password_validation_works() {
const NAME: &str = "testuser542";
async fn del_userworks() {
const NAME: &str = "testuser2";
const PASSWORD: &str = "longpassword2";
const EMAIL: &str = "testuser1@a.com2";
{
let data = Data::new().await;
delete_user(NAME, &data).await;
}
let app = get_app!(data).await;
let (data, creds, signin_resp) = register_and_signin(NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
let mut app = get_app!(data).await;
// checking to see if server-side password validation (password == password_config)
// works
let register_msg = Register {
username: NAME.into(),
password: PASSWORD.into(),
confirm_password: NAME.into(),
email: None,
let payload = Password {
password: creds.password,
};
let resp = test::call_service(
&app,
post_request!(&register_msg, ROUTES.auth.register).to_request(),
let delete_user_resp = test::call_service(
&mut app,
post_request!(&payload, "/api/v1/account/delete")
.cookie(cookies)
.to_request(),
)
.await;
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let txt: ErrorToResponse = test::read_body_json(resp).await;
assert_eq!(txt.error, format!("{}", ServiceError::PasswordsDontMatch));
assert_eq!(delete_user_resp.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn uname_email_exists_works() {
const NAME: &str = "testuserexists";
const PASSWORD: &str = "longpassword2";
const EMAIL: &str = "testuserexists@a.com2";
const UNAME_CHECK: &str = "/api/v1/account/username/exists";
const EMAIL_CHECK: &str = "/api/v1/account/email/exists";
{
let data = Data::new().await;
delete_user(NAME, &data).await;
}
let (data, _, signin_resp) = register_and_signin(NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
let mut app = get_app!(data).await;
let mut payload = AccountCheckPayload { val: NAME.into() };
let user_exists_resp = test::call_service(
&mut app,
post_request!(&payload, UNAME_CHECK)
.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(
&mut app,
post_request!(&payload, UNAME_CHECK)
.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(
&mut app,
post_request!(&payload, EMAIL_CHECK)
.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(
&mut app,
post_request!(&payload, EMAIL_CHECK)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(email_exist.status(), StatusCode::OK);
resp = test::read_body_json(email_exist).await;
assert!(resp.exists);
}

View File

@@ -1,5 +1,5 @@
/*
* 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
* it under the terms of the GNU Affero General Public License as
@@ -16,4 +16,3 @@
*/
mod auth;
mod protected;

View File

@@ -1,80 +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 crate::data::Data;
use crate::*;
use crate::tests::*;
#[actix_rt::test]
async fn protected_routes_work() {
const NAME: &str = "testuser619";
const PASSWORD: &str = "longpassword2";
const EMAIL: &str = "testuser119@a.com2";
let _post_protected_urls = [
"/api/v1/account/secret/",
"/api/v1/account/email/",
"/api/v1/account/delete",
"/api/v1/mcaptcha/levels/add",
"/api/v1/mcaptcha/levels/update",
"/api/v1/mcaptcha/levels/delete",
"/api/v1/mcaptcha/levels/get",
"/api/v1/mcaptcha/domain/token/duration/update",
"/api/v1/mcaptcha/domain/token/duration/get",
"/api/v1/mcaptcha/add",
"/api/v1/mcaptcha/update/key",
"/api/v1/mcaptcha/get",
"/api/v1/mcaptcha/delete",
];
let get_protected_urls = ["/logout"];
{
let data = Data::new().await;
delete_user(NAME, &data).await;
}
let (data, _, signin_resp) = register_and_signin(NAME, EMAIL, PASSWORD).await;
let cookies = get_cookie!(signin_resp);
let app = get_app!(data).await;
for url in get_protected_urls.iter() {
let resp =
test::call_service(&app, test::TestRequest::get().uri(url).to_request())
.await;
assert_eq!(resp.status(), StatusCode::FOUND);
let authenticated_resp = test::call_service(
&app,
test::TestRequest::get()
.uri(url)
.cookie(cookies.clone())
.to_request(),
)
.await;
if url == &V1_API_ROUTES.auth.logout {
assert_eq!(authenticated_resp.status(), StatusCode::FOUND);
} else {
assert_eq!(authenticated_resp.status(), StatusCode::OK);
}
}
}

View File

@@ -1,5 +1,5 @@
/*
* 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
* it under the terms of the GNU Affero General Public License as
@@ -10,214 +10,62 @@
* 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/>.
*/
//! App data: redis cache, database connections, etc.
use std::sync::Arc;
use std::thread;
use actix::prelude::*;
use argon2_creds::{Config, ConfigBuilder, PasswordPolicy};
use lettre::transport::smtp::authentication::Mechanism;
use lettre::{
transport::smtp::authentication::Credentials, AsyncSmtpTransport, Tokio1Executor,
};
use libmcaptcha::cache::hashcache::HashCache;
use libmcaptcha::cache::redis::RedisCache;
use libmcaptcha::master::redis::master::Master as RedisMaster;
use libmcaptcha::redis::RedisConfig;
use libmcaptcha::{
cache::messages::VerifyCaptchaResult,
cache::Save,
errors::CaptchaResult,
master::messages::{AddSite, RemoveCaptcha, Rename},
master::{embedded::master::Master as EmbeddedMaster, Master as MasterTrait},
use m_captcha::{
cache::HashCache,
master::Master,
pow::ConfigBuilder as PoWConfigBuilder,
pow::PoWConfig,
pow::Work,
system::{System, SystemBuilder},
};
use sqlx::postgres::PgPoolOptions;
use sqlx::PgPool;
use crate::errors::ServiceResult;
use crate::SETTINGS;
macro_rules! enum_system_actor {
($name:ident, $type:ident) => {
pub async fn $name(&self, msg: $type) -> ServiceResult<()> {
match self {
Self::Embedded(val) => val.master.send(msg).await?.await??,
Self::Redis(val) => val.master.send(msg).await?.await??,
};
Ok(())
}
};
}
macro_rules! enum_system_wrapper {
($name:ident, $type:ty, $return_type:ty) => {
pub async fn $name(&self, msg: $type) -> $return_type {
match self {
Self::Embedded(val) => val.$name(msg).await,
Self::Redis(val) => val.$name(msg).await,
}
}
};
}
/// Represents mCaptcha cache and master system.
/// When Redis is configured, [SystemGroup::Redis] is used and
/// in its absense, [SystemGroup::Embedded] is used
pub enum SystemGroup {
Embedded(System<HashCache, EmbeddedMaster>),
Redis(System<RedisCache, RedisMaster>),
}
#[allow(unused_doc_comments)]
impl SystemGroup {
// TODO find a way to document these methods
// utility function to get difficulty factor of site `id` and cache it
enum_system_wrapper!(get_pow, String, CaptchaResult<Option<PoWConfig>>);
// utility function to verify [Work]
enum_system_wrapper!(verify_pow, Work, CaptchaResult<String>);
// utility function to validate verification tokens
enum_system_wrapper!(
validate_verification_tokens,
VerifyCaptchaResult,
CaptchaResult<bool>
);
// utility function to AddSite
enum_system_actor!(add_site, AddSite);
// utility function to rename captcha
enum_system_actor!(rename, Rename);
// utility function to remove captcha
enum_system_actor!(remove, RemoveCaptcha);
fn new_system<A: Save, B: MasterTrait>(m: Addr<B>, c: Addr<A>) -> System<A, B> {
let pow = PoWConfigBuilder::default()
.salt(SETTINGS.captcha.salt.clone())
.build()
.unwrap();
SystemBuilder::default().pow(pow).cache(c).master(m).build()
}
// read settings, if Redis is configured then produce a Redis mCaptcha cache
// based SystemGroup
async fn new() -> Self {
match &SETTINGS.redis {
Some(val) => {
let master = RedisMaster::new(RedisConfig::Single(val.url.clone()))
.await
.unwrap()
.start();
let cache = RedisCache::new(RedisConfig::Single(val.url.clone()))
.await
.unwrap()
.start();
let captcha = Self::new_system(master, cache);
SystemGroup::Redis(captcha)
}
None => {
let master = EmbeddedMaster::new(SETTINGS.captcha.gc).start();
let cache = HashCache::default().start();
let captcha = Self::new_system(master, cache);
SystemGroup::Embedded(captcha)
}
}
}
}
/// App data
#[derive(Clone)]
pub struct Data {
/// databse pool
pub db: PgPool,
/// credential management configuration
pub creds: Config,
/// mCaptcha system: Redis cache, etc.
pub captcha: SystemGroup,
/// email client
pub mailer: Option<Mailer>,
pub captcha: System<HashCache>,
}
impl Data {
pub fn get_creds() -> Config {
ConfigBuilder::default()
.username_case_mapped(true)
.profanity(true)
.blacklist(true)
.password_policy(PasswordPolicy::default())
.build()
.unwrap()
}
#[cfg(not(tarpaulin_include))]
/// create new instance of app data
pub async fn new() -> Arc<Self> {
let creds = Self::get_creds();
let c = creds.clone();
#[allow(unused_variables)]
let init = thread::spawn(move || {
log::info!("Initializing credential manager");
c.init();
log::info!("Initialized credential manager");
});
pub async fn new() -> Self {
let db = PgPoolOptions::new()
.max_connections(SETTINGS.database.pool)
.connect(&SETTINGS.database.url)
.await
.expect("Unable to form database pool");
let data = Data {
creds,
db,
captcha: SystemGroup::new().await,
mailer: Self::get_mailer(),
};
let creds = ConfigBuilder::default()
.username_case_mapped(false)
.profanity(true)
.blacklist(false)
.password_policy(PasswordPolicy::default())
.build()
.unwrap();
#[cfg(not(debug_assertions))]
init.join().unwrap();
let master = Master::new(SETTINGS.pow.gc).start();
let cache = HashCache::default().start();
let pow = PoWConfigBuilder::default()
.salt(SETTINGS.pow.salt.clone())
.build()
.unwrap();
Arc::new(data)
}
let captcha = SystemBuilder::default()
.master(master)
.cache(cache)
.pow(pow)
.build()
.unwrap();
fn get_mailer() -> Option<Mailer> {
if let Some(smtp) = SETTINGS.smtp.as_ref() {
let creds =
Credentials::new(smtp.username.to_string(), smtp.password.to_string()); // "smtp_username".to_string(), "smtp_password".to_string());
let mailer: Mailer =
AsyncSmtpTransport::<Tokio1Executor>::builder_dangerous(&smtp.url)
.port(smtp.port)
.credentials(creds)
.authentication(vec![
Mechanism::Login,
Mechanism::Xoauth2,
Mechanism::Plain,
])
.build();
// let mailer: Mailer = AsyncSmtpTransport::<Tokio1Executor>::relay(&smtp.url) //"smtp.gmail.com")
// .unwrap()
// .credentials(creds)
// .build();
Some(mailer)
} else {
None
Data { creds, db, captcha }
}
}
}
/// Mailer data type AsyncSmtpTransport<Tokio1Executor>
pub type Mailer = AsyncSmtpTransport<Tokio1Executor>;

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