dump: wip archiver
This commit is contained in:
parent
cfd1f485c2
commit
fa4aa03baa
86 changed files with 11725 additions and 0 deletions
3
.dockerignore
Normal file
3
.dockerignore
Normal file
|
@ -0,0 +1,3 @@
|
|||
/target
|
||||
.env.local
|
||||
tarpaulin-report.html
|
6
.gitignore
vendored
Normal file
6
.gitignore
vendored
Normal file
|
@ -0,0 +1,6 @@
|
|||
/target
|
||||
.env.local
|
||||
tarpaulin-report.html
|
||||
tmp/
|
||||
node_modules/
|
||||
assets/
|
3692
Cargo.lock
generated
Normal file
3692
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load diff
104
Cargo.toml
Normal file
104
Cargo.toml
Normal file
|
@ -0,0 +1,104 @@
|
|||
[package]
|
||||
name = "pativu"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
repository = "https://git.batsense.net/realaravinth/pativu"
|
||||
homepage = "https://git.batsense.net/realaravinth/pativu"
|
||||
documentation = "https://git.batsense.net/realaravinth/pativu"
|
||||
readme = "README.md"
|
||||
license = "AGPLv3 or later version"
|
||||
authors = ["realaravinth <realaravinth@batsense.net>"]
|
||||
build = "build.rs"
|
||||
|
||||
|
||||
|
||||
#[dependencies]
|
||||
#actix-web = "4"
|
||||
#futures-util = { version = "0.3.17", default-features = false, features = ["std"] }
|
||||
#lazy_static = "1.4.0"
|
||||
#log = "0.4.17"
|
||||
#pretty_env_logger = "0.4.0"
|
||||
#serde = { version = "1", features=["derive"]}
|
||||
#actix-web-codegen-const-routes = { version = "0.1.0", tag = "0.1.0", git = "https://github.com/realaravinth/actix-web-codegen-const-routes" }
|
||||
#derive_builder = "0.11.2"
|
||||
#config = "0.11"
|
||||
#derive_more = "0.99.17"
|
||||
#url = { version = "2.2.2", features = ["serde"]}
|
||||
#serde_json = { version ="1", features = ["raw_value"]}
|
||||
#clap = { vesrion = "3.2.20", features = ["derive"]}
|
||||
|
||||
[build-dependencies]
|
||||
serde_json = "1"
|
||||
sqlx = { version = "0.6.1", features = [ "runtime-actix-rustls", "sqlite", "time", "offline"] }
|
||||
|
||||
#[dev-dependencies]
|
||||
#actix-rt = "2.7.0"
|
||||
#base64 = "0.13.0"
|
||||
|
||||
[dependencies]
|
||||
actix-web = "4.0.1"
|
||||
actix-http = "3.0.4"
|
||||
actix-identity = "0.4.0"
|
||||
actix-rt = "2"
|
||||
actix-web-codegen-const-routes = { version = "0.1.0", tag = "0.1.0", git = "https://github.com/realaravinth/actix-web-codegen-const-routes" }
|
||||
argon2-creds = { branch = "master", git = "https://github.com/realaravinth/argon2-creds"}
|
||||
sqlx = { version = "0.6.2", features = ["sqlite", "time", "offline", "runtime-actix-rustls", "uuid", "json"] }
|
||||
clap = { version = "3.2.20", features = ["derive"]}
|
||||
|
||||
config = "0.13"
|
||||
|
||||
serde = { version = "1", features = ["derive", "rc"]}
|
||||
serde_json = "1"
|
||||
|
||||
pretty_env_logger = "0.4"
|
||||
|
||||
lazy_static = "1.4"
|
||||
url = { version = "2.2", features = ["serde"] }
|
||||
urlencoding = "2.1.0"
|
||||
|
||||
derive_more = "0.99"
|
||||
|
||||
num_cpus = "1.13"
|
||||
|
||||
tokio = { version = "1", features=["sync", "fs"]}
|
||||
num_enum = "0.5.7"
|
||||
|
||||
mime_guess = "2.0.4"
|
||||
mime = "0.3.16"
|
||||
rust-embed = "6.3.0"
|
||||
rand = "0.8.5"
|
||||
tracing = { version = "0.1.37", features = ["log"]}
|
||||
tracing-actix-web = "0.6.2"
|
||||
toml = "0.5.9"
|
||||
serde_yaml = "0.9.14"
|
||||
uuid = { version = "1.2.2", features = ["serde"] }
|
||||
reqwest = { version = "0.11.13", features = ["rustls-tls-native-roots", "stream", "gzip", "deflate", "brotli", "json"]}
|
||||
futures-util = "0.3.25"
|
||||
lol_html = "0.3.1"
|
||||
scraper = "0.13.0"
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
[dependencies.cache-buster]
|
||||
git = "https://github.com/realaravinth/cache-buster"
|
||||
|
||||
[dependencies.tera]
|
||||
default-features = false
|
||||
version = "1.15.0"
|
||||
|
||||
[dependencies.actix-auth-middleware]
|
||||
branch = "v4"
|
||||
features = ["actix_identity_backend"]
|
||||
git = "https://github.com/realaravinth/actix-auth-middleware"
|
||||
version = "0.2"
|
||||
|
||||
|
||||
[dev-dependencies]
|
||||
futures = "0.3.24"
|
||||
mktemp = "0.4.1"
|
||||
|
||||
|
||||
[workspace]
|
||||
exclude = ["utils/cache-bust"]
|
72
Makefile
Normal file
72
Makefile
Normal file
|
@ -0,0 +1,72 @@
|
|||
define cache_bust ## run cache_busting program
|
||||
npm run sass
|
||||
cd utils/cache-bust && cargo run
|
||||
endef
|
||||
|
||||
default: ## Debug build
|
||||
$(call cache_bust)
|
||||
cargo run -- serve
|
||||
|
||||
cache-bust: ## Run cache buster on static assets
|
||||
$(call cache_bust)
|
||||
|
||||
check: ## Check for syntax errors on all workspaces
|
||||
cargo check --workspace --tests --all-features
|
||||
#cd utils/cache-bust && cargo check --tests --all-features
|
||||
|
||||
clean: ## Clean all build artifacts and dependencies
|
||||
@cargo clean
|
||||
|
||||
coverage: ## Generate HTML code coverage
|
||||
$(call cache_bust)
|
||||
cargo tarpaulin -t 1200 --out Html
|
||||
|
||||
dev-env: ## Download development dependencies
|
||||
npm install
|
||||
cargo fetch
|
||||
|
||||
doc: ## Prepare documentation
|
||||
cargo doc --no-deps --workspace --all-features
|
||||
|
||||
docker: ## Build docker images
|
||||
docker build \
|
||||
-t realaravinth/pativu:master \
|
||||
-t realaravinth/pativu:latest \
|
||||
-t realaravinth/pativu:0.1.0 .
|
||||
|
||||
docker-publish: docker ## Build and publish docker images
|
||||
docker push realaravinth/pativu:master
|
||||
docker push realaravinth/pativu:latest
|
||||
docker push realaravinth/pativu:0.1.0
|
||||
|
||||
lint: ## Lint codebase
|
||||
cargo fmt -v --all -- --emit files
|
||||
cargo clippy --workspace --tests --all-features
|
||||
|
||||
migrate: ## run migrations
|
||||
$(call cache_bust)
|
||||
unset DATABASE_URL && cargo build
|
||||
cargo run -- migrate
|
||||
|
||||
release: ## Release build
|
||||
$(call cache_bust)
|
||||
cargo build --release
|
||||
|
||||
run: default ## Run debug build
|
||||
cargo run -- serve
|
||||
|
||||
sqlx-offline-data: ## prepare sqlx offline data
|
||||
cargo sqlx prepare \
|
||||
--database-url=${DATABASE_URL} -- \
|
||||
--all-features
|
||||
|
||||
test: ## Run tests
|
||||
$(call cache_bust)
|
||||
cargo test --all-features --no-fail-fast
|
||||
|
||||
xml-test-coverage: ## Generate cobertura.xml test coverage
|
||||
$(call cache_bust)
|
||||
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}'
|
22
README.md
Normal file
22
README.md
Normal file
|
@ -0,0 +1,22 @@
|
|||
[![Docker](https://img.shields.io/docker/pulls/realaravinth/librepages-conductor)](https://hub.docker.com/r/realaravinth/librepages-conductor)
|
||||
[![status-badge](https://ci.batsense.net/api/badges/LibrePages/conductor/status.svg)](https://ci.batsense.net/LibrePages/conductor)
|
||||
|
||||
|
||||
<div align="center">
|
||||
<h1> Conductor </h1>
|
||||
<p>
|
||||
</div>
|
||||
|
||||
Enabling custom domain support(configuring reverse proxy, deploying TLS
|
||||
certificates, etc.) are environtment dependent. LibrePages will notify
|
||||
conductor when a new hostname should be deployed.
|
||||
|
||||
## Launch docker container
|
||||
|
||||
|
||||
```bash
|
||||
docker run -p 7000:7000 \
|
||||
-v $(pwd)/config/:/etc/lpconductor/ \
|
||||
-e LPCONDUCTOR_CONFIG=/etc/lpconductor/default.toml \
|
||||
realaravinth/librepages-conductor:latest
|
||||
```
|
26
build.rs
Normal file
26
build.rs
Normal file
|
@ -0,0 +1,26 @@
|
|||
/*
|
||||
* 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::process::Command;
|
||||
|
||||
fn main() {
|
||||
let output = Command::new("git")
|
||||
.args(["rev-parse", "HEAD"])
|
||||
.output()
|
||||
.expect("error in git command, is git installed?");
|
||||
let git_hash = String::from_utf8(output.stdout).unwrap();
|
||||
println!("cargo:rustc-env=GIT_HASH={}", git_hash);
|
||||
}
|
37
config/default.toml
Normal file
37
config/default.toml
Normal file
|
@ -0,0 +1,37 @@
|
|||
debug = true
|
||||
allow_registration = true
|
||||
# source code of your copy of pages server.
|
||||
source_code = "https://git.batsense.net/realaravinth/pativu"
|
||||
support_email = "support@librepages.example.org"
|
||||
|
||||
[server]
|
||||
# The port at which you want Pages to listen to
|
||||
port = 7000
|
||||
#IP address. Enter 0.0.0.0 to listen on all availale addresses
|
||||
ip= "0.0.0.0"
|
||||
# The number of worker threads that must be spun up by the Pages server.
|
||||
# Minimum of two threads are advisable for top async performance but can work
|
||||
# with one also.
|
||||
workers = 2
|
||||
domain = "localhost"
|
||||
cookie_secret = "94b2b2732626fdb7736229a7c777cb451e6304c147c4549f30"
|
||||
|
||||
|
||||
[pages]
|
||||
base_path = "/tmp/pativu-defualt-config/"
|
||||
|
||||
[database]
|
||||
# This section deals with the database location and how to access it
|
||||
# Please note that at the moment, we have support for only sqlite.
|
||||
# Example, if you are Batman, your config would be:
|
||||
# hostname = "batcave.org"
|
||||
# port = "5432"
|
||||
# username = "batman"
|
||||
# password = "somereallycomplicatedBatmanpassword"
|
||||
hostname = "localhost"
|
||||
port = "5432"
|
||||
username = "sqlite"
|
||||
password = "password"
|
||||
name = "sqlite"
|
||||
pool = 4
|
||||
database_type="sqlite"
|
7
migrations/20220910140647_librepages_users.sql
Normal file
7
migrations/20220910140647_librepages_users.sql
Normal file
|
@ -0,0 +1,7 @@
|
|||
CREATE TABLE IF NOT EXISTS librepages_users (
|
||||
name VARCHAR(100) NOT NULL UNIQUE,
|
||||
email VARCHAR(100) UNIQUE NOT NULL,
|
||||
email_verified BOOLEAN DEFAULT NULL,
|
||||
password TEXT NOT NULL,
|
||||
ID INTEGER PRIMARY KEY NOT NULL
|
||||
);
|
5
migrations/20220921122103_librepages_sites.sql
Normal file
5
migrations/20220921122103_librepages_sites.sql
Normal file
|
@ -0,0 +1,5 @@
|
|||
CREATE TABLE IF NOT EXISTS librepages_sites (
|
||||
url VARCHAR(3000) NOT NULL UNIQUE,
|
||||
ID INTEGER PRIMARY KEY NOT NULL,
|
||||
owned_by INTEGER NOT NULL references librepages_users(ID) ON DELETE CASCADE
|
||||
);
|
385
package-lock.json
generated
Normal file
385
package-lock.json
generated
Normal file
|
@ -0,0 +1,385 @@
|
|||
{
|
||||
"name": "librepages",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 2,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "librepages",
|
||||
"version": "1.0.0",
|
||||
"license": "ISC",
|
||||
"devDependencies": {
|
||||
"sass": "^1.54.9"
|
||||
}
|
||||
},
|
||||
"node_modules/anymatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
|
||||
"integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"normalize-path": "^3.0.0",
|
||||
"picomatch": "^2.0.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8"
|
||||
}
|
||||
},
|
||||
"node_modules/binary-extensions": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"fill-range": "^7.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/chokidar": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "individual",
|
||||
"url": "https://paulmillr.com/funding/"
|
||||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 8.10.0"
|
||||
},
|
||||
"optionalDependencies": {
|
||||
"fsevents": "~2.3.2"
|
||||
}
|
||||
},
|
||||
"node_modules/fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"optional": true,
|
||||
"os": [
|
||||
"darwin"
|
||||
],
|
||||
"engines": {
|
||||
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-glob": "^4.0.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/immutable": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz",
|
||||
"integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-extglob": "^2.1.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.12.0"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8.6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/jonschlinkert"
|
||||
}
|
||||
},
|
||||
"node_modules/readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"picomatch": "^2.2.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/sass": {
|
||||
"version": "1.54.9",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.54.9.tgz",
|
||||
"integrity": "sha512-xb1hjASzEH+0L0WI9oFjqhRi51t/gagWnxLiwUNMltA0Ab6jIDkAacgKiGYKM9Jhy109osM7woEEai6SXeJo5Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"chokidar": ">=3.0.0 <4.0.0",
|
||||
"immutable": "^4.0.0",
|
||||
"source-map-js": ">=0.6.2 <2.0.0"
|
||||
},
|
||||
"bin": {
|
||||
"sass": "sass.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=12.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/source-map-js": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
|
||||
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"is-number": "^7.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"dependencies": {
|
||||
"anymatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.2.tgz",
|
||||
"integrity": "sha512-P43ePfOAIupkguHUycrc4qJ9kz8ZiuOUijaETwX7THt0Y/GNK7v0aa8rY816xWjZ7rJdA5XdMcpVFTKMq+RvWg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"normalize-path": "^3.0.0",
|
||||
"picomatch": "^2.0.4"
|
||||
}
|
||||
},
|
||||
"binary-extensions": {
|
||||
"version": "2.2.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
|
||||
"integrity": "sha512-jDctJ/IVQbZoJykoeHbhXpOlNBqGNcwXJKJog42E5HDPUwQTSdjCHdihjj0DlnheQ7blbT6dHOafNAiS8ooQKA==",
|
||||
"dev": true
|
||||
},
|
||||
"braces": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz",
|
||||
"integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"fill-range": "^7.0.1"
|
||||
}
|
||||
},
|
||||
"chokidar": {
|
||||
"version": "3.5.3",
|
||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz",
|
||||
"integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"anymatch": "~3.1.2",
|
||||
"braces": "~3.0.2",
|
||||
"fsevents": "~2.3.2",
|
||||
"glob-parent": "~5.1.2",
|
||||
"is-binary-path": "~2.1.0",
|
||||
"is-glob": "~4.0.1",
|
||||
"normalize-path": "~3.0.0",
|
||||
"readdirp": "~3.6.0"
|
||||
}
|
||||
},
|
||||
"fill-range": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz",
|
||||
"integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"to-regex-range": "^5.0.1"
|
||||
}
|
||||
},
|
||||
"fsevents": {
|
||||
"version": "2.3.2",
|
||||
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz",
|
||||
"integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==",
|
||||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"glob-parent": {
|
||||
"version": "5.1.2",
|
||||
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz",
|
||||
"integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-glob": "^4.0.1"
|
||||
}
|
||||
},
|
||||
"immutable": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-4.1.0.tgz",
|
||||
"integrity": "sha512-oNkuqVTA8jqG1Q6c+UglTOD1xhC1BtjKI7XkCXRkZHrN5m18/XsnUp8Q89GkQO/z+0WjonSvl0FLhDYftp46nQ==",
|
||||
"dev": true
|
||||
},
|
||||
"is-binary-path": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz",
|
||||
"integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"binary-extensions": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"is-extglob": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||
"integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==",
|
||||
"dev": true
|
||||
},
|
||||
"is-glob": {
|
||||
"version": "4.0.3",
|
||||
"resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz",
|
||||
"integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-extglob": "^2.1.1"
|
||||
}
|
||||
},
|
||||
"is-number": {
|
||||
"version": "7.0.0",
|
||||
"resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz",
|
||||
"integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==",
|
||||
"dev": true
|
||||
},
|
||||
"normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
"integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==",
|
||||
"dev": true
|
||||
},
|
||||
"picomatch": {
|
||||
"version": "2.3.1",
|
||||
"resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz",
|
||||
"integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==",
|
||||
"dev": true
|
||||
},
|
||||
"readdirp": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz",
|
||||
"integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"picomatch": "^2.2.1"
|
||||
}
|
||||
},
|
||||
"sass": {
|
||||
"version": "1.54.9",
|
||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.54.9.tgz",
|
||||
"integrity": "sha512-xb1hjASzEH+0L0WI9oFjqhRi51t/gagWnxLiwUNMltA0Ab6jIDkAacgKiGYKM9Jhy109osM7woEEai6SXeJo5Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"chokidar": ">=3.0.0 <4.0.0",
|
||||
"immutable": "^4.0.0",
|
||||
"source-map-js": ">=0.6.2 <2.0.0"
|
||||
}
|
||||
},
|
||||
"source-map-js": {
|
||||
"version": "1.0.2",
|
||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.0.2.tgz",
|
||||
"integrity": "sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==",
|
||||
"dev": true
|
||||
},
|
||||
"to-regex-range": {
|
||||
"version": "5.0.1",
|
||||
"resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz",
|
||||
"integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"is-number": "^7.0.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
25
package.json
Normal file
25
package.json
Normal file
|
@ -0,0 +1,25 @@
|
|||
{
|
||||
"name": "librepages",
|
||||
"version": "1.0.0",
|
||||
"description": "<div align=\"center\"> <h1> Pages </h1> <p>",
|
||||
"main": "index.js",
|
||||
"directories": {
|
||||
"doc": "docs"
|
||||
},
|
||||
"scripts": {
|
||||
"sass": "rm -rf static/cache/css/*.css && sass templates/main.scss static/cache/css/main.css && sass templates/mobile.scss static/cache/css/mobile.css"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/realaravinth/librepages.git"
|
||||
},
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"bugs": {
|
||||
"url": "https://github.com/realaravinth/librepages/issues"
|
||||
},
|
||||
"homepage": "https://github.com/realaravinth/librepages#readme",
|
||||
"devDependencies": {
|
||||
"sass": "^1.54.9"
|
||||
}
|
||||
}
|
3
sqlx-data.json
Normal file
3
sqlx-data.json
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"db": "SQLite"
|
||||
}
|
17
src/api/mod.rs
Normal file
17
src/api/mod.rs
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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 v1;
|
144
src/api/v1/account/mod.rs
Normal file
144
src/api/v1/account/mod.rs
Normal file
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* 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::{web, HttpResponse, Responder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::ctx::api::v1::account::*;
|
||||
use crate::ctx::api::v1::auth::Password;
|
||||
use crate::errors::*;
|
||||
use crate::AppCtx;
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test;
|
||||
|
||||
pub use super::auth;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct AccountCheckPayload {
|
||||
pub val: String,
|
||||
}
|
||||
|
||||
pub fn services(cfg: &mut actix_web::web::ServiceConfig) {
|
||||
cfg.service(username_exists);
|
||||
cfg.service(set_username);
|
||||
cfg.service(email_exists);
|
||||
cfg.service(set_email);
|
||||
cfg.service(delete_account);
|
||||
cfg.service(update_user_password);
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct Email {
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct Username {
|
||||
pub username: String,
|
||||
}
|
||||
|
||||
/// update username
|
||||
#[actix_web_codegen_const_routes::post(
|
||||
path = "crate::V1_API_ROUTES.account.update_username",
|
||||
wrap = "super::get_auth_middleware()"
|
||||
)]
|
||||
#[tracing::instrument(name = "Update username", skip(ctx, payload, id))]
|
||||
async fn set_username(
|
||||
id: Identity,
|
||||
payload: web::Json<Username>,
|
||||
ctx: AppCtx,
|
||||
) -> ServiceResult<impl Responder> {
|
||||
let username = id.identity().unwrap();
|
||||
|
||||
let new_name = ctx.update_username(&username, &payload.username).await?;
|
||||
|
||||
id.forget();
|
||||
id.remember(new_name);
|
||||
|
||||
Ok(HttpResponse::Ok())
|
||||
}
|
||||
|
||||
#[actix_web_codegen_const_routes::post(path = "crate::V1_API_ROUTES.account.username_exists")]
|
||||
#[tracing::instrument(name = "Check if username exists", skip(ctx, payload))]
|
||||
async fn username_exists(
|
||||
payload: web::Json<AccountCheckPayload>,
|
||||
ctx: AppCtx,
|
||||
) -> ServiceResult<impl Responder> {
|
||||
Ok(HttpResponse::Ok().json(ctx.username_exists(&payload.val).await?))
|
||||
}
|
||||
|
||||
#[actix_web_codegen_const_routes::post(path = "crate::V1_API_ROUTES.account.email_exists")]
|
||||
#[tracing::instrument(name = "Check if email exists", skip(ctx, payload))]
|
||||
pub async fn email_exists(
|
||||
payload: web::Json<AccountCheckPayload>,
|
||||
ctx: AppCtx,
|
||||
) -> ServiceResult<impl Responder> {
|
||||
Ok(HttpResponse::Ok().json(ctx.email_exists(&payload.val).await?))
|
||||
}
|
||||
|
||||
/// update email
|
||||
#[actix_web_codegen_const_routes::post(
|
||||
path = "crate::V1_API_ROUTES.account.update_email",
|
||||
wrap = "super::get_auth_middleware()"
|
||||
)]
|
||||
#[tracing::instrument(name = "Update email", skip(ctx, payload, id))]
|
||||
async fn set_email(
|
||||
id: Identity,
|
||||
payload: web::Json<Email>,
|
||||
ctx: AppCtx,
|
||||
) -> ServiceResult<impl Responder> {
|
||||
let username = id.identity().unwrap();
|
||||
ctx.set_email(&username, &payload.email).await?;
|
||||
Ok(HttpResponse::Ok())
|
||||
}
|
||||
|
||||
#[actix_web_codegen_const_routes::post(
|
||||
path = "crate::V1_API_ROUTES.account.delete",
|
||||
wrap = "super::get_auth_middleware()"
|
||||
)]
|
||||
#[tracing::instrument(name = "Delete account", skip(ctx, payload, id))]
|
||||
async fn delete_account(
|
||||
id: Identity,
|
||||
payload: web::Json<Password>,
|
||||
ctx: AppCtx,
|
||||
) -> ServiceResult<impl Responder> {
|
||||
let username = id.identity().unwrap();
|
||||
|
||||
ctx.delete_user(&username, &payload.password).await?;
|
||||
id.forget();
|
||||
Ok(HttpResponse::Ok())
|
||||
}
|
||||
|
||||
#[actix_web_codegen_const_routes::post(
|
||||
path = "crate::V1_API_ROUTES.account.update_password",
|
||||
wrap = "super::get_auth_middleware()"
|
||||
)]
|
||||
#[tracing::instrument(name = "Update user password", skip(ctx, payload, id))]
|
||||
async fn update_user_password(
|
||||
id: Identity,
|
||||
ctx: AppCtx,
|
||||
|
||||
payload: web::Json<ChangePasswordReqest>,
|
||||
) -> ServiceResult<impl Responder> {
|
||||
let username = id.identity().unwrap();
|
||||
let payload = payload.into_inner();
|
||||
ctx.change_password(&username, &payload).await?;
|
||||
|
||||
Ok(HttpResponse::Ok())
|
||||
}
|
296
src/api/v1/account/test.rs
Normal file
296
src/api/v1/account/test.rs
Normal file
|
@ -0,0 +1,296 @@
|
|||
/*
|
||||
* 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::sync::Arc;
|
||||
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::test;
|
||||
|
||||
use super::*;
|
||||
use crate::api::v1::ROUTES;
|
||||
use crate::ctx::api::v1::auth::Password;
|
||||
use crate::ctx::Ctx;
|
||||
use crate::*;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn postgrest_account_works() {
|
||||
let (_, ctx) = crate::tests::get_ctx().await;
|
||||
uname_email_exists_works(ctx.clone()).await;
|
||||
email_udpate_password_validation_del_userworks(ctx.clone()).await;
|
||||
username_update_works(ctx.clone()).await;
|
||||
update_password_works(ctx.clone()).await;
|
||||
}
|
||||
|
||||
async fn uname_email_exists_works(ctx: Arc<Ctx>) {
|
||||
const NAME: &str = "testuserexists";
|
||||
const PASSWORD: &str = "longpasswordasdfa2";
|
||||
const EMAIL: &str = "testuserexists2@a.com";
|
||||
|
||||
let _ = ctx.delete_user(NAME, PASSWORD).await;
|
||||
|
||||
let (_, signin_resp) = ctx.register_and_signin(NAME, EMAIL, PASSWORD).await;
|
||||
let cookies = get_cookie!(signin_resp);
|
||||
let app = get_app!(ctx).await;
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
async fn email_udpate_password_validation_del_userworks(ctx: Arc<Ctx>) {
|
||||
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 _ = ctx.delete_user(NAME, PASSWORD).await;
|
||||
let _ = ctx.delete_user(NAME2, PASSWORD).await;
|
||||
|
||||
let _ = ctx.register_and_signin(NAME2, EMAIL2, PASSWORD).await;
|
||||
let (_creds, signin_resp) = ctx.register_and_signin(NAME, EMAIL, PASSWORD).await;
|
||||
let cookies = get_cookie!(signin_resp);
|
||||
let app = get_app!(ctx).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();
|
||||
// ctx.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(),
|
||||
};
|
||||
ctx.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));
|
||||
}
|
||||
|
||||
async fn username_update_works(ctx: Arc<Ctx>) {
|
||||
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 _ = futures::join!(
|
||||
ctx.delete_user(NAME, PASSWORD),
|
||||
ctx.delete_user(NAME2, PASSWORD),
|
||||
ctx.delete_user(NAME_CHANGE, PASSWORD)
|
||||
);
|
||||
|
||||
let _ = ctx.register_and_signin(NAME2, EMAIL2, PASSWORD).await;
|
||||
let (_creds, signin_resp) = ctx.register_and_signin(NAME, EMAIL, PASSWORD).await;
|
||||
let cookies = get_cookie!(signin_resp);
|
||||
let app = get_app!(ctx).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();
|
||||
// ctx.bad_post_req_test(
|
||||
// NAME_CHANGE,
|
||||
// PASSWORD,
|
||||
// ROUTES.account.update_username,
|
||||
// &username_udpate,
|
||||
// ServiceError::UsernameTaken,
|
||||
// )
|
||||
// .await;
|
||||
}
|
||||
|
||||
async fn update_password_works(ctx: Arc<Ctx>) {
|
||||
const NAME: &str = "updatepassuser";
|
||||
const PASSWORD: &str = "longpassword2";
|
||||
const EMAIL: &str = "updatepassuser@a.com";
|
||||
|
||||
let _ = ctx.delete_user(NAME, PASSWORD).await;
|
||||
|
||||
let (_, signin_resp) = ctx.register_and_signin(NAME, EMAIL, PASSWORD).await;
|
||||
let cookies = get_cookie!(signin_resp);
|
||||
let app = get_app!(ctx).await;
|
||||
|
||||
let new_password = "newpassword";
|
||||
|
||||
let update_password = ChangePasswordReqest {
|
||||
password: PASSWORD.into(),
|
||||
new_password: new_password.into(),
|
||||
confirm_new_password: PASSWORD.into(),
|
||||
};
|
||||
|
||||
let res = ctx.change_password(NAME, &update_password).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!(ctx.change_password(NAME, &update_password).await.is_ok());
|
||||
|
||||
let update_password = ChangePasswordReqest {
|
||||
password: new_password.into(),
|
||||
new_password: new_password.into(),
|
||||
confirm_new_password: PASSWORD.into(),
|
||||
};
|
||||
|
||||
ctx.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(),
|
||||
};
|
||||
|
||||
ctx.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);
|
||||
}
|
74
src/api/v1/auth.rs
Normal file
74
src/api/v1/auth.rs
Normal file
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* 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 crate::ctx::api::v1::auth::{Login, Register};
|
||||
use actix_identity::Identity;
|
||||
use actix_web::http::header;
|
||||
use actix_web::{web, HttpResponse, Responder};
|
||||
|
||||
use super::RedirectQuery;
|
||||
use crate::errors::*;
|
||||
use crate::AppCtx;
|
||||
|
||||
pub fn services(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(register);
|
||||
cfg.service(login);
|
||||
cfg.service(signout);
|
||||
}
|
||||
#[actix_web_codegen_const_routes::post(path = "crate::V1_API_ROUTES.auth.register")]
|
||||
#[tracing::instrument(name = "Register new user", skip(ctx, payload))]
|
||||
async fn register(payload: web::Json<Register>, ctx: AppCtx) -> ServiceResult<impl Responder> {
|
||||
ctx.register(&payload).await?;
|
||||
Ok(HttpResponse::Ok())
|
||||
}
|
||||
|
||||
#[actix_web_codegen_const_routes::post(path = "crate::V1_API_ROUTES.auth.login")]
|
||||
#[tracing::instrument(name = "Login", skip(ctx, payload, id, query))]
|
||||
async fn login(
|
||||
id: Identity,
|
||||
payload: web::Json<Login>,
|
||||
query: web::Query<RedirectQuery>,
|
||||
ctx: AppCtx,
|
||||
) -> ServiceResult<impl Responder> {
|
||||
let payload = payload.into_inner();
|
||||
let username = ctx.login(&payload).await?;
|
||||
id.remember(username);
|
||||
let query = query.into_inner();
|
||||
if let Some(redirect_to) = query.redirect_to {
|
||||
Ok(HttpResponse::Found()
|
||||
.insert_header((header::LOCATION, redirect_to))
|
||||
.finish())
|
||||
} else {
|
||||
Ok(HttpResponse::Ok().into())
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_web_codegen_const_routes::get(
|
||||
path = "crate::V1_API_ROUTES.auth.logout",
|
||||
wrap = "super::get_auth_middleware()"
|
||||
)]
|
||||
#[tracing::instrument(name = "Sign out", skip(id))]
|
||||
async fn signout(id: Identity) -> impl Responder {
|
||||
use actix_auth_middleware::GetLoginRoute;
|
||||
|
||||
if id.identity().is_some() {
|
||||
id.forget();
|
||||
}
|
||||
HttpResponse::Found()
|
||||
.append_header((header::LOCATION, crate::V1_API_ROUTES.get_login_route(None)))
|
||||
.finish()
|
||||
}
|
114
src/api/v1/meta.rs
Normal file
114
src/api/v1/meta.rs
Normal file
|
@ -0,0 +1,114 @@
|
|||
/*
|
||||
* 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::{web, HttpResponse, Responder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::{AppCtx, GIT_COMMIT_HASH, VERSION};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct BuildDetails<'a> {
|
||||
pub version: &'a str,
|
||||
pub git_commit_hash: &'a str,
|
||||
pub source_code: &'a 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",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// emits build details of the binary
|
||||
#[actix_web_codegen_const_routes::get(path = "crate::V1_API_ROUTES.meta.build_details")]
|
||||
#[tracing::instrument(name = "Fetch Build Details", skip(ctx))]
|
||||
async fn build_details(ctx: AppCtx) -> impl Responder {
|
||||
let build = BuildDetails {
|
||||
version: VERSION,
|
||||
git_commit_hash: GIT_COMMIT_HASH,
|
||||
source_code: &ctx.settings.source_code,
|
||||
};
|
||||
HttpResponse::Ok().json(build)
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
/// Health check return datatype
|
||||
pub struct Health {
|
||||
db: bool,
|
||||
}
|
||||
|
||||
/// checks all components of the system
|
||||
#[actix_web_codegen_const_routes::get(path = "crate::V1_API_ROUTES.meta.health")]
|
||||
#[tracing::instrument(name = "Fetch health", skip(ctx))]
|
||||
async fn health(ctx: crate::AppCtx) -> impl Responder {
|
||||
let res = Health {
|
||||
db: ctx.db.ping().await,
|
||||
};
|
||||
|
||||
HttpResponse::Ok().json(res)
|
||||
}
|
||||
|
||||
pub fn services(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(build_details);
|
||||
cfg.service(health);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use actix_web::{http::StatusCode, test};
|
||||
|
||||
use crate::*;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn build_details_works() {
|
||||
let (_dir, ctx) = tests::get_ctx().await;
|
||||
println!("[log] test configuration {:#?}", ctx.settings);
|
||||
let app = get_app!(ctx).await;
|
||||
|
||||
let resp = get_request!(app, V1_API_ROUTES.meta.build_details);
|
||||
check_status!(resp, StatusCode::OK);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn health_works() {
|
||||
use actix_web::test;
|
||||
|
||||
let (_dir, ctx) = tests::get_ctx().await;
|
||||
let app = get_app!(ctx).await;
|
||||
|
||||
let resp = test::call_service(
|
||||
&app,
|
||||
test::TestRequest::get()
|
||||
.uri(crate::V1_API_ROUTES.meta.health)
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
let health_resp: super::Health = test::read_body_json(resp).await;
|
||||
assert!(health_resp.db);
|
||||
}
|
||||
}
|
44
src/api/v1/mod.rs
Normal file
44
src/api/v1/mod.rs
Normal file
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* 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::Authentication;
|
||||
use actix_web::web::ServiceConfig;
|
||||
use serde::Deserialize;
|
||||
|
||||
pub mod account;
|
||||
pub mod auth;
|
||||
pub mod meta;
|
||||
pub mod routes;
|
||||
|
||||
pub use routes::ROUTES;
|
||||
|
||||
pub fn services(cfg: &mut ServiceConfig) {
|
||||
auth::services(cfg);
|
||||
account::services(cfg);
|
||||
meta::services(cfg);
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct RedirectQuery {
|
||||
pub redirect_to: Option<String>,
|
||||
}
|
||||
|
||||
pub fn get_auth_middleware() -> Authentication<routes::Routes> {
|
||||
Authentication::with_identity(ROUTES)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
100
src/api/v1/pages.rs
Normal file
100
src/api/v1/pages.rs
Normal file
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* 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::{web, HttpResponse, Responder};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::errors::*;
|
||||
use crate::AppCtx;
|
||||
|
||||
pub mod routes {
|
||||
pub struct Deploy {
|
||||
pub update: &'static str,
|
||||
}
|
||||
|
||||
impl Deploy {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
update: "/api/v1/update",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct DeployEvent {
|
||||
pub secret: String,
|
||||
pub branch: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct DeployEventResp {
|
||||
pub id: Uuid,
|
||||
}
|
||||
|
||||
pub fn services(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(update);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use actix_web::{http::StatusCode, test};
|
||||
|
||||
use crate::tests;
|
||||
use crate::*;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn deploy_update_works() {
|
||||
const NAME: &str = "dplyupdwrkuser";
|
||||
const PASSWORD: &str = "longpasswordasdfa2";
|
||||
const EMAIL: &str = "dplyupdwrkuser@a.com";
|
||||
|
||||
let (_dir, ctx) = tests::get_ctx().await;
|
||||
let _ = ctx.delete_user(NAME, PASSWORD).await;
|
||||
let (_, _signin_resp) = ctx.register_and_signin(NAME, EMAIL, PASSWORD).await;
|
||||
let page = ctx.add_test_site(NAME.into()).await;
|
||||
let app = get_app!(ctx).await;
|
||||
|
||||
let mut payload = DeployEvent {
|
||||
secret: page.secret.clone(),
|
||||
branch: page.branch.clone(),
|
||||
};
|
||||
|
||||
let resp = test::call_service(
|
||||
&app,
|
||||
post_request!(&payload, V1_API_ROUTES.deploy.update).to_request(),
|
||||
)
|
||||
.await;
|
||||
check_status!(resp, StatusCode::OK);
|
||||
let event_id: DeployEventResp = actix_web::test::read_body_json(resp).await;
|
||||
let update_event = ctx.db.get_event(&page.domain, &event_id.id).await.unwrap();
|
||||
assert_eq!(&update_event.site, &page.domain);
|
||||
assert_eq!(update_event.id, event_id.id);
|
||||
|
||||
payload.secret = page.branch.clone();
|
||||
|
||||
let resp = test::call_service(
|
||||
&app,
|
||||
post_request!(&payload, V1_API_ROUTES.deploy.update).to_request(),
|
||||
)
|
||||
.await;
|
||||
check_status!(resp, StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
}
|
117
src/api/v1/routes.rs
Normal file
117
src/api/v1/routes.rs
Normal file
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
//! V1 API Routes
|
||||
use actix_auth_middleware::GetLoginRoute;
|
||||
|
||||
use super::meta::routes::Meta;
|
||||
|
||||
/// constant [Routes](Routes) instance
|
||||
pub const ROUTES: Routes = Routes::new();
|
||||
|
||||
/// Authentication routes
|
||||
pub struct Auth {
|
||||
/// logout route
|
||||
pub logout: &'static str,
|
||||
/// login route
|
||||
pub login: &'static str,
|
||||
/// registration route
|
||||
pub register: &'static str,
|
||||
}
|
||||
impl Auth {
|
||||
/// create new instance of Authentication route
|
||||
pub const fn new() -> Auth {
|
||||
let login = "/api/v1/signin";
|
||||
let logout = "/api/v1/logout";
|
||||
let register = "/api/v1/signup";
|
||||
Auth {
|
||||
logout,
|
||||
login,
|
||||
register,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Account management routes
|
||||
pub struct Account {
|
||||
/// delete account route
|
||||
pub delete: &'static str,
|
||||
/// route to check if an email exists
|
||||
pub email_exists: &'static str,
|
||||
/// route to update a user's email
|
||||
pub update_email: &'static str,
|
||||
/// route to update password
|
||||
pub update_password: &'static str,
|
||||
/// route to check if a username is already registered
|
||||
pub username_exists: &'static str,
|
||||
/// route to change username
|
||||
pub update_username: &'static str,
|
||||
}
|
||||
|
||||
impl Account {
|
||||
/// create a new instance of [Account][Account] routes
|
||||
pub const fn new() -> Account {
|
||||
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,
|
||||
update_email,
|
||||
update_password,
|
||||
username_exists,
|
||||
update_username,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Top-level routes data structure for V1 AP1
|
||||
pub struct Routes {
|
||||
/// Authentication routes
|
||||
pub auth: Auth,
|
||||
/// Account routes
|
||||
pub account: Account,
|
||||
/// Meta routes
|
||||
pub meta: Meta,
|
||||
}
|
||||
|
||||
impl Routes {
|
||||
/// create new instance of Routes
|
||||
const fn new() -> Routes {
|
||||
Routes {
|
||||
auth: Auth::new(),
|
||||
account: Account::new(),
|
||||
meta: Meta::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl GetLoginRoute for Routes {
|
||||
fn get_login_route(&self, src: Option<&str>) -> String {
|
||||
if let Some(redirect_to) = src {
|
||||
format!(
|
||||
"{}?redirect_to={}",
|
||||
self.auth.login,
|
||||
urlencoding::encode(redirect_to)
|
||||
)
|
||||
} else {
|
||||
self.auth.register.to_string()
|
||||
}
|
||||
}
|
||||
}
|
174
src/api/v1/tests/auth.rs
Normal file
174
src/api/v1/tests/auth.rs
Normal file
|
@ -0,0 +1,174 @@
|
|||
/*
|
||||
* 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_auth_middleware::GetLoginRoute;
|
||||
use actix_web::http::{header, StatusCode};
|
||||
use actix_web::test;
|
||||
|
||||
use crate::api::v1::ROUTES;
|
||||
use crate::ctx::api::v1::auth::{Login, Register};
|
||||
use crate::ctx::ArcCtx;
|
||||
use crate::errors::*;
|
||||
use crate::*;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn postgrest_auth_works() {
|
||||
let (_, ctx) = crate::tests::get_ctx().await;
|
||||
auth_works(ctx.clone()).await;
|
||||
serverside_password_validation_works(ctx).await;
|
||||
}
|
||||
|
||||
async fn auth_works(ctx: ArcCtx) {
|
||||
const NAME: &str = "testuserfoo";
|
||||
const PASSWORD: &str = "longpassword";
|
||||
const EMAIL: &str = "testuser1foo@a.com";
|
||||
|
||||
let _ = ctx.delete_user(NAME, PASSWORD).await;
|
||||
let app = get_app!(ctx).await;
|
||||
|
||||
// 1. Register and signin
|
||||
let (_, signin_resp) = ctx.register_and_signin(NAME, EMAIL, PASSWORD).await;
|
||||
let cookies = get_cookie!(signin_resp);
|
||||
|
||||
// Sign in with email
|
||||
ctx.signin_test(EMAIL, PASSWORD).await;
|
||||
|
||||
// 2. check if duplicate username is allowed
|
||||
let mut msg = Register {
|
||||
username: NAME.into(),
|
||||
password: PASSWORD.into(),
|
||||
confirm_password: PASSWORD.into(),
|
||||
email: EMAIL.into(),
|
||||
};
|
||||
|
||||
msg.username = format!("asdfasd{}", msg.username);
|
||||
// ctx.bad_post_req_test(
|
||||
// NAME,
|
||||
// PASSWORD,
|
||||
// ROUTES.auth.register,
|
||||
// &msg,
|
||||
// ServiceError::EmailTaken,
|
||||
// )
|
||||
// .await;
|
||||
|
||||
msg.email = format!("asdfasd{}", msg.email);
|
||||
msg.username = NAME.into();
|
||||
// ctx.bad_post_req_test(
|
||||
// NAME,
|
||||
// PASSWORD,
|
||||
// ROUTES.auth.register,
|
||||
// &msg,
|
||||
// ServiceError::UsernameTaken,
|
||||
// )
|
||||
// .await;
|
||||
|
||||
// 3. sigining in with non-existent user
|
||||
let mut creds = Login {
|
||||
login: "nonexistantuser".into(),
|
||||
password: msg.password.clone(),
|
||||
};
|
||||
ctx.bad_post_req_test(
|
||||
NAME,
|
||||
PASSWORD,
|
||||
ROUTES.auth.login,
|
||||
&creds,
|
||||
ServiceError::AccountNotFound,
|
||||
)
|
||||
.await;
|
||||
|
||||
creds.login = "nonexistantuser@example.com".into();
|
||||
ctx.bad_post_req_test(
|
||||
NAME,
|
||||
PASSWORD,
|
||||
ROUTES.auth.login,
|
||||
&creds,
|
||||
ServiceError::AccountNotFound,
|
||||
)
|
||||
.await;
|
||||
|
||||
// 4. trying to signin with wrong password
|
||||
creds.login = NAME.into();
|
||||
creds.password = NAME.into();
|
||||
|
||||
ctx.bad_post_req_test(
|
||||
NAME,
|
||||
PASSWORD,
|
||||
ROUTES.auth.login,
|
||||
&creds,
|
||||
ServiceError::WrongPassword,
|
||||
)
|
||||
.await;
|
||||
|
||||
// 5. signout
|
||||
let signout_resp = test::call_service(
|
||||
&app,
|
||||
test::TestRequest::get()
|
||||
.uri(ROUTES.auth.logout)
|
||||
.cookie(cookies)
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(signout_resp.status(), StatusCode::FOUND);
|
||||
let headers = signout_resp.headers();
|
||||
assert_eq!(
|
||||
headers.get(header::LOCATION).unwrap(),
|
||||
&crate::V1_API_ROUTES.get_login_route(None)
|
||||
);
|
||||
|
||||
let creds = Login {
|
||||
login: NAME.into(),
|
||||
password: PASSWORD.into(),
|
||||
};
|
||||
|
||||
//6. sigin with redirect URL set
|
||||
let redirect_to = ROUTES.auth.logout;
|
||||
let resp = test::call_service(
|
||||
&app,
|
||||
post_request!(&creds, &ROUTES.get_login_route(Some(redirect_to))).to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(resp.status(), StatusCode::FOUND);
|
||||
let headers = resp.headers();
|
||||
assert_eq!(headers.get(header::LOCATION).unwrap(), &redirect_to);
|
||||
}
|
||||
|
||||
async fn serverside_password_validation_works(ctx: ArcCtx) {
|
||||
const NAME: &str = "testuser542";
|
||||
const PASSWORD: &str = "longpassword2";
|
||||
const EMAIL: &str = "testuser542@example.com";
|
||||
|
||||
let _ = ctx.delete_user(NAME, PASSWORD).await;
|
||||
|
||||
let app = get_app!(ctx).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: EMAIL.into(),
|
||||
};
|
||||
let resp = test::call_service(
|
||||
&app,
|
||||
post_request!(®ister_msg, ROUTES.auth.register).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));
|
||||
}
|
18
src/api/v1/tests/mod.rs
Normal file
18
src/api/v1/tests/mod.rs
Normal file
|
@ -0,0 +1,18 @@
|
|||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
mod auth;
|
||||
mod protected;
|
70
src/api/v1/tests/protected.rs
Normal file
70
src/api/v1/tests/protected.rs
Normal file
|
@ -0,0 +1,70 @@
|
|||
/*
|
||||
* 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_web::http::StatusCode;
|
||||
use actix_web::test;
|
||||
|
||||
use crate::ctx::ArcCtx;
|
||||
//use crate::pages::PAGES;
|
||||
use crate::*;
|
||||
|
||||
use crate::tests::*;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn postgrest_protected_routes_work() {
|
||||
let (_, ctx) = get_ctx().await;
|
||||
protected_routes_work(ctx.clone()).await
|
||||
}
|
||||
|
||||
async fn protected_routes_work(ctx: ArcCtx) {
|
||||
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",
|
||||
];
|
||||
|
||||
let get_protected_urls = [
|
||||
V1_API_ROUTES.auth.logout,
|
||||
// PAGES.auth.logout,
|
||||
// PAGES.home,
|
||||
];
|
||||
|
||||
let _ = ctx.delete_user(NAME, PASSWORD).await;
|
||||
|
||||
let (_, signin_resp) = ctx.register_and_signin(NAME, EMAIL, PASSWORD).await;
|
||||
let cookies = get_cookie!(signin_resp);
|
||||
let app = get_app!(ctx).await;
|
||||
|
||||
for url in get_protected_urls.iter() {
|
||||
let resp = get_request!(&app, url);
|
||||
assert_eq!(resp.status(), StatusCode::FOUND);
|
||||
|
||||
let authenticated_resp = get_request!(&app, url, cookies.clone());
|
||||
|
||||
println!("{url}");
|
||||
if url == &V1_API_ROUTES.auth.logout {
|
||||
// || url == &PAGES.auth.logout {
|
||||
assert_eq!(authenticated_resp.status(), StatusCode::FOUND);
|
||||
} else {
|
||||
assert_eq!(authenticated_resp.status(), StatusCode::OK);
|
||||
}
|
||||
}
|
||||
}
|
1
src/cache_buster_data.json
Normal file
1
src/cache_buster_data.json
Normal file
|
@ -0,0 +1 @@
|
|||
{"map":{"./static/cache/css/main.css":"./assets/css/main.C5E0456C4A1FB573F1A5A95D4490E00C60355FBF576EE380B61FB3A5C9D8DBB9.css","./static/cache/css/mobile.css.map":"./assets/css/mobile.css.53B52AE67347949A7066AA3C490CCB02A83D37D6B98A315365C7BB9E21794CA8.map","./static/cache/css/main.css.map":"./assets/css/main.css.F6A1AB66BB2E32F0C4BB1C550A741B010F8FBD134AFF5C65CF9C286D252AE05A.map","./static/cache/css/mobile.css":"./assets/css/mobile.6A41051B957A206B1CD6CFE04FD161558EA3D3EC944CBC191E318F3A765DF75B.css"},"base_dir":"./assets"}
|
17
src/ctx/api/mod.rs
Normal file
17
src/ctx/api/mod.rs
Normal file
|
@ -0,0 +1,17 @@
|
|||
/*
|
||||
* 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 v1;
|
136
src/ctx/api/v1/account.rs
Normal file
136
src/ctx/api/v1/account.rs
Normal file
|
@ -0,0 +1,136 @@
|
|||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
//! Account management utility datastructures and methods
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub use super::auth;
|
||||
use crate::ctx::Ctx;
|
||||
use crate::db;
|
||||
use crate::errors::*;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
/// Data structure used in `*_exists` methods
|
||||
pub struct AccountCheckResp {
|
||||
/// set to true if the attribute in question exists
|
||||
pub exists: bool,
|
||||
}
|
||||
|
||||
/// Data structure used to change password of a registered user
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct ChangePasswordReqest {
|
||||
/// current password
|
||||
pub password: String,
|
||||
/// new password
|
||||
pub new_password: String,
|
||||
/// new password confirmation
|
||||
pub confirm_new_password: String,
|
||||
}
|
||||
|
||||
impl Ctx {
|
||||
/// check if email exists on database
|
||||
pub async fn email_exists(&self, email: &str) -> ServiceResult<AccountCheckResp> {
|
||||
let resp = AccountCheckResp {
|
||||
exists: self.db.email_exists(email).await?,
|
||||
};
|
||||
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
/// update email
|
||||
pub async fn set_email(&self, username: &str, new_email: &str) -> ServiceResult<()> {
|
||||
self.creds.email(new_email)?;
|
||||
|
||||
let username = self.creds.username(username)?;
|
||||
|
||||
let payload = db::UpdateEmail {
|
||||
username: &username,
|
||||
new_email,
|
||||
};
|
||||
self.db.update_email(&payload).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// check if email exists in database
|
||||
pub async fn username_exists(&self, username: &str) -> ServiceResult<AccountCheckResp> {
|
||||
let processed_uname = self.creds.username(username)?;
|
||||
let resp = AccountCheckResp {
|
||||
exists: self.db.username_exists(&processed_uname).await?,
|
||||
};
|
||||
Ok(resp)
|
||||
}
|
||||
|
||||
/// update username of a registered user
|
||||
pub async fn update_username(
|
||||
&self,
|
||||
current_username: &str,
|
||||
new_username: &str,
|
||||
) -> ServiceResult<String> {
|
||||
let processed_uname = self.creds.username(new_username)?;
|
||||
|
||||
self.db
|
||||
.update_username(current_username, &processed_uname)
|
||||
.await?;
|
||||
|
||||
Ok(processed_uname)
|
||||
}
|
||||
|
||||
// returns Ok(()) upon successful authentication
|
||||
async fn authenticate(&self, username: &str, password: &str) -> ServiceResult<()> {
|
||||
use argon2_creds::Config;
|
||||
let username = self.creds.username(username)?;
|
||||
let resp = self
|
||||
.db
|
||||
.get_password(&db::Login::Username(&username))
|
||||
.await?;
|
||||
if Config::verify(&resp.hash, password)? {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ServiceError::WrongPassword)
|
||||
}
|
||||
}
|
||||
|
||||
/// delete user
|
||||
pub async fn delete_user(&self, username: &str, password: &str) -> ServiceResult<()> {
|
||||
let username = self.creds.username(username)?;
|
||||
self.authenticate(&username, password).await?;
|
||||
self.db.delete_user(&username).await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// change password
|
||||
pub async fn change_password(
|
||||
&self,
|
||||
|
||||
username: &str,
|
||||
payload: &ChangePasswordReqest,
|
||||
) -> ServiceResult<()> {
|
||||
if payload.new_password != payload.confirm_new_password {
|
||||
return Err(ServiceError::PasswordsDontMatch);
|
||||
}
|
||||
|
||||
self.authenticate(username, &payload.password).await?;
|
||||
|
||||
let hash = self.creds.password(&payload.new_password)?;
|
||||
|
||||
let username = self.creds.username(username)?;
|
||||
let db_payload = db::NameHash { username, hash };
|
||||
|
||||
self.db.update_password(&db_payload).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
104
src/ctx/api/v1/auth.rs
Normal file
104
src/ctx/api/v1/auth.rs
Normal file
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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/>.
|
||||
*/
|
||||
//! Authentication helper methods and data structures
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::ctx::Ctx;
|
||||
use crate::db;
|
||||
use crate::errors::*;
|
||||
|
||||
/// Register payload
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
pub struct Register {
|
||||
/// username
|
||||
pub username: String,
|
||||
/// password
|
||||
pub password: String,
|
||||
/// password confirmation: `password` and `confirm_password` must match
|
||||
pub confirm_password: String,
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
/// Login payload
|
||||
#[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
|
||||
/// user identifier: either username or email
|
||||
/// an email is detected by checkinf for the existence of `@` character
|
||||
pub login: String,
|
||||
/// password
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize)]
|
||||
/// struct used to represent password
|
||||
pub struct Password {
|
||||
/// password
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
impl Ctx {
|
||||
/// Log in method. Returns `Ok(())` when user is authenticated and errors when authentication
|
||||
/// fails
|
||||
pub async fn login(&self, payload: &Login) -> ServiceResult<String> {
|
||||
use argon2_creds::Config;
|
||||
|
||||
let verify = |stored: &str, received: &str| {
|
||||
if Config::verify(stored, received)? {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(ServiceError::WrongPassword)
|
||||
}
|
||||
};
|
||||
|
||||
let creds = if payload.login.contains('@') {
|
||||
self.db
|
||||
.get_password(&db::Login::Email(&payload.login))
|
||||
.await?
|
||||
} else {
|
||||
self.db
|
||||
.get_password(&db::Login::Username(&payload.login))
|
||||
.await?
|
||||
};
|
||||
verify(&creds.hash, &payload.password)?;
|
||||
Ok(creds.username)
|
||||
}
|
||||
|
||||
/// register new user
|
||||
pub async fn register(&self, payload: &Register) -> ServiceResult<()> {
|
||||
if !self.settings.allow_registration {
|
||||
return Err(ServiceError::ClosedForRegistration);
|
||||
}
|
||||
|
||||
if payload.password != payload.confirm_password {
|
||||
return Err(ServiceError::PasswordsDontMatch);
|
||||
}
|
||||
let username = self.creds.username(&payload.username)?;
|
||||
let hash = self.creds.password(&payload.password)?;
|
||||
|
||||
self.creds.email(&payload.email)?;
|
||||
|
||||
let db_payload = db::Register {
|
||||
username: &username,
|
||||
hash: &hash,
|
||||
email: &payload.email,
|
||||
};
|
||||
|
||||
self.db.register(&db_payload).await
|
||||
}
|
||||
}
|
22
src/ctx/api/v1/mod.rs
Normal file
22
src/ctx/api/v1/mod.rs
Normal file
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* 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 account;
|
||||
pub mod auth;
|
||||
pub mod pages;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
53
src/ctx/api/v1/pages.rs
Normal file
53
src/ctx/api/v1/pages.rs
Normal file
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::Url;
|
||||
|
||||
use crate::ctx::Ctx;
|
||||
use crate::db;
|
||||
use crate::db::Site;
|
||||
use crate::errors::*;
|
||||
use crate::page::Page;
|
||||
use crate::settings::Settings;
|
||||
use crate::utils::get_random;
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
|
||||
/// Data required to add site
|
||||
pub struct AddSite {
|
||||
pub url: Url,
|
||||
pub owner: String,
|
||||
}
|
||||
|
||||
impl AddSite {
|
||||
fn to_site(self, s: &Settings) -> Site {
|
||||
let site_secret = get_random(32);
|
||||
Site {
|
||||
url: self.url,
|
||||
owner: self.owner,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Ctx {
|
||||
pub async fn add_site(&self, site: AddSite) -> ServiceResult<Page> {
|
||||
let db_site = site.to_site(&self.settings);
|
||||
self.db.add_site(&db_site).await.unwrap();
|
||||
let page = Page::from_site(&self.settings, db_site);
|
||||
page.archive(&self).await.unwrap();
|
||||
Ok(page)
|
||||
}
|
||||
}
|
227
src/ctx/api/v1/tests/accounts.rs
Normal file
227
src/ctx/api/v1/tests/accounts.rs
Normal file
|
@ -0,0 +1,227 @@
|
|||
/*
|
||||
* 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 crate::api::v1::account::{Email, Username};
|
||||
use crate::ctx::api::v1::account::ChangePasswordReqest;
|
||||
use crate::ctx::api::v1::auth::Password;
|
||||
use crate::ctx::api::v1::auth::Register;
|
||||
use crate::ctx::ArcCtx;
|
||||
use crate::errors::*;
|
||||
use crate::*;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn postgrest_account_works() {
|
||||
let (_dir, ctx) = crate::tests::get_ctx().await;
|
||||
uname_email_exists_works(ctx.clone()).await;
|
||||
email_udpate_password_validation_del_userworks(ctx.clone()).await;
|
||||
username_update_works(ctx).await;
|
||||
}
|
||||
|
||||
async fn uname_email_exists_works(ctx: ArcCtx) {
|
||||
const NAME: &str = "testuserexistsfoo";
|
||||
const NAME2: &str = "testuserexists22";
|
||||
const NAME3: &str = "testuserexists32";
|
||||
const PASSWORD: &str = "longpassword2";
|
||||
const EMAIL: &str = "accotestsuser22@a.com";
|
||||
const EMAIL2: &str = "accotestsuser222@a.com";
|
||||
const EMAIL3: &str = "accotestsuser322@a.com";
|
||||
|
||||
let _ = ctx.db.delete_user(NAME).await;
|
||||
let _ = ctx.db.delete_user(PASSWORD).await;
|
||||
let _ = ctx.db.delete_user(NAME2).await;
|
||||
let _ = ctx.db.delete_user(NAME3).await;
|
||||
|
||||
// check username exists for non existent account
|
||||
println!("{:?}", ctx.username_exists(NAME).await);
|
||||
assert!(!ctx.username_exists(NAME).await.unwrap().exists);
|
||||
// check username email for non existent account
|
||||
assert!(!ctx.email_exists(EMAIL).await.unwrap().exists);
|
||||
|
||||
let mut register_payload = Register {
|
||||
username: NAME.into(),
|
||||
password: PASSWORD.into(),
|
||||
confirm_password: PASSWORD.into(),
|
||||
email: EMAIL.into(),
|
||||
};
|
||||
ctx.register(®ister_payload).await.unwrap();
|
||||
register_payload.username = NAME2.into();
|
||||
register_payload.email = EMAIL2.into();
|
||||
ctx.register(®ister_payload).await.unwrap();
|
||||
|
||||
// check username exists
|
||||
assert!(ctx.username_exists(NAME).await.unwrap().exists);
|
||||
assert!(ctx.username_exists(NAME2).await.unwrap().exists);
|
||||
// check email exists
|
||||
assert!(ctx.email_exists(EMAIL).await.unwrap().exists);
|
||||
|
||||
// update username
|
||||
ctx.update_username(NAME2, NAME3).await.unwrap();
|
||||
assert!(!ctx.username_exists(NAME2).await.unwrap().exists);
|
||||
assert!(ctx.username_exists(NAME3).await.unwrap().exists);
|
||||
|
||||
// assert!(matches!(
|
||||
// ctx.update_username(NAME3, NAME).await.err(),
|
||||
// Some(ServiceError::UsernameTaken)
|
||||
// ));
|
||||
|
||||
// update email
|
||||
// assert_eq!(
|
||||
// ctx.set_email(NAME, EMAIL2).await.err(),
|
||||
// Some(ServiceError::EmailTaken)
|
||||
// );
|
||||
ctx.set_email(NAME, EMAIL3).await.unwrap();
|
||||
|
||||
// change password
|
||||
let mut change_password_req = ChangePasswordReqest {
|
||||
password: PASSWORD.into(),
|
||||
new_password: NAME.into(),
|
||||
confirm_new_password: PASSWORD.into(),
|
||||
};
|
||||
assert_eq!(
|
||||
ctx.change_password(NAME, &change_password_req).await.err(),
|
||||
Some(ServiceError::PasswordsDontMatch)
|
||||
);
|
||||
|
||||
change_password_req.confirm_new_password = NAME.into();
|
||||
ctx.change_password(NAME, &change_password_req)
|
||||
.await
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
async fn email_udpate_password_validation_del_userworks(ctx: ArcCtx) {
|
||||
const NAME: &str = "testuser32sd2";
|
||||
const PASSWORD: &str = "longpassword2";
|
||||
const EMAIL: &str = "testuser12232@a.com2";
|
||||
const NAME2: &str = "eupdauser22";
|
||||
const EMAIL2: &str = "eupdauser22@a.com";
|
||||
|
||||
let _ = ctx.delete_user(NAME, PASSWORD).await;
|
||||
let _ = ctx.delete_user(NAME2, PASSWORD).await;
|
||||
|
||||
let _ = ctx.register_and_signin(NAME2, EMAIL2, PASSWORD).await;
|
||||
let (_creds, signin_resp) = ctx.register_and_signin(NAME, EMAIL, PASSWORD).await;
|
||||
let cookies = get_cookie!(signin_resp);
|
||||
let app = get_app!(ctx).await;
|
||||
|
||||
// update email
|
||||
let mut email_payload = Email {
|
||||
email: EMAIL.into(),
|
||||
};
|
||||
let email_update_resp = actix_web::test::call_service(
|
||||
&app,
|
||||
post_request!(&email_payload, crate::V1_API_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();
|
||||
// ctx.bad_post_req_test(
|
||||
// NAME,
|
||||
// PASSWORD,
|
||||
// crate::V1_API_ROUTES.account.update_email,
|
||||
// &email_payload,
|
||||
// ServiceError::EmailTaken,
|
||||
// )
|
||||
// .await;
|
||||
|
||||
// wrong password while deleting account
|
||||
let mut payload = Password {
|
||||
password: NAME.into(),
|
||||
};
|
||||
ctx.bad_post_req_test(
|
||||
NAME,
|
||||
PASSWORD,
|
||||
V1_API_ROUTES.account.delete,
|
||||
&payload,
|
||||
ServiceError::WrongPassword,
|
||||
)
|
||||
.await;
|
||||
|
||||
// delete account
|
||||
payload.password = PASSWORD.into();
|
||||
let delete_user_resp = actix_web::test::call_service(
|
||||
&app,
|
||||
post_request!(&payload, crate::V1_API_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 = actix_web::test::call_service(
|
||||
&app,
|
||||
post_request!(&payload, crate::V1_API_ROUTES.account.delete)
|
||||
.cookie(cookies)
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(account_not_found_resp.status(), StatusCode::NOT_FOUND);
|
||||
let txt: ErrorToResponse = actix_web::test::read_body_json(account_not_found_resp).await;
|
||||
assert_eq!(txt.error, format!("{}", ServiceError::AccountNotFound));
|
||||
}
|
||||
|
||||
async fn username_update_works(ctx: ArcCtx) {
|
||||
const NAME: &str = "testuse23423rupda";
|
||||
const EMAIL: &str = "testu23423serupda@sss.com";
|
||||
const EMAIL2: &str = "testu234serupda2@sss.com";
|
||||
const PASSWORD: &str = "longpassword2";
|
||||
const NAME2: &str = "terstusrt23423ds";
|
||||
const NAME_CHANGE: &str = "terstu234234srtdsxx";
|
||||
|
||||
let _ = futures::join!(
|
||||
ctx.delete_user(NAME, PASSWORD),
|
||||
ctx.delete_user(NAME2, PASSWORD),
|
||||
ctx.delete_user(NAME_CHANGE, PASSWORD)
|
||||
);
|
||||
|
||||
let _ = ctx.register_and_signin(NAME2, EMAIL2, PASSWORD).await;
|
||||
let (_creds, signin_resp) = ctx.register_and_signin(NAME, EMAIL, PASSWORD).await;
|
||||
let cookies = get_cookie!(signin_resp);
|
||||
let app = get_app!(ctx).await;
|
||||
|
||||
// update username
|
||||
let mut username_udpate = Username {
|
||||
username: NAME_CHANGE.into(),
|
||||
};
|
||||
let username_update_resp = actix_web::test::call_service(
|
||||
&app,
|
||||
post_request!(
|
||||
&username_udpate,
|
||||
crate::V1_API_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();
|
||||
// ctx.bad_post_req_test(
|
||||
// NAME_CHANGE,
|
||||
// PASSWORD,
|
||||
// V1_API_ROUTES.account.update_username,
|
||||
// &username_udpate,
|
||||
// ServiceError::UsernameTaken,
|
||||
// )
|
||||
// .await;
|
||||
}
|
104
src/ctx/api/v1/tests/auth.rs
Normal file
104
src/ctx/api/v1/tests/auth.rs
Normal file
|
@ -0,0 +1,104 @@
|
|||
/*
|
||||
* 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::sync::Arc;
|
||||
|
||||
//use crate::api::v1::auth::{Login, Register};
|
||||
use crate::ctx::api::v1::auth::{Login, Register};
|
||||
use crate::ctx::Ctx;
|
||||
use crate::errors::*;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn postgrest_auth_works() {
|
||||
let (_dir, ctx) = crate::tests::get_ctx().await;
|
||||
auth_works(ctx).await;
|
||||
}
|
||||
|
||||
async fn auth_works(ctx: Arc<Ctx>) {
|
||||
const NAME: &str = "testuser";
|
||||
const PASSWORD: &str = "longpassword";
|
||||
const EMAIL: &str = "testuser1@a.com";
|
||||
|
||||
let _ = ctx.delete_user(NAME, PASSWORD).await;
|
||||
|
||||
// 1. Register with email == None
|
||||
let mut register_payload = Register {
|
||||
username: NAME.into(),
|
||||
password: PASSWORD.into(),
|
||||
confirm_password: PASSWORD.into(),
|
||||
email: EMAIL.into(),
|
||||
};
|
||||
|
||||
// registration: passwords don't match
|
||||
register_payload.confirm_password = NAME.into();
|
||||
assert!(matches!(
|
||||
ctx.register(®ister_payload).await.err(),
|
||||
Some(ServiceError::PasswordsDontMatch)
|
||||
));
|
||||
|
||||
register_payload.confirm_password = PASSWORD.into();
|
||||
|
||||
ctx.register(®ister_payload).await.unwrap();
|
||||
// check if duplicate username is allowed
|
||||
// assert!(matches!(
|
||||
// ctx.register(®ister_payload).await.err(),
|
||||
// Some(ServiceError::UsernameTaken)
|
||||
// ));
|
||||
|
||||
// check if duplicate email is allowed
|
||||
let name = format!("{}dupemail", NAME);
|
||||
register_payload.username = name;
|
||||
// assert!(matches!(
|
||||
// ctx.register(®ister_payload).await.err(),
|
||||
// Some(ServiceError::EmailTaken)
|
||||
// ));
|
||||
|
||||
// Sign in with email
|
||||
let mut creds = Login {
|
||||
login: EMAIL.into(),
|
||||
password: PASSWORD.into(),
|
||||
};
|
||||
ctx.login(&creds).await.unwrap();
|
||||
|
||||
// signin with username
|
||||
creds.login = NAME.into();
|
||||
ctx.login(&creds).await.unwrap();
|
||||
|
||||
// sigining in with non-existent username
|
||||
creds.login = "nonexistantuser".into();
|
||||
assert!(matches!(
|
||||
ctx.login(&creds).await.err(),
|
||||
Some(ServiceError::AccountNotFound)
|
||||
));
|
||||
|
||||
// sigining in with non-existent email
|
||||
creds.login = "nonexistantuser@example.com".into();
|
||||
assert!(matches!(
|
||||
ctx.login(&creds).await.err(),
|
||||
Some(ServiceError::AccountNotFound)
|
||||
));
|
||||
|
||||
// sign in with incorrect password
|
||||
creds.login = NAME.into();
|
||||
creds.password = NAME.into();
|
||||
assert!(matches!(
|
||||
ctx.login(&creds).await.err(),
|
||||
Some(ServiceError::WrongPassword)
|
||||
));
|
||||
|
||||
// delete user
|
||||
ctx.delete_user(NAME, PASSWORD).await.unwrap();
|
||||
}
|
2
src/ctx/api/v1/tests/mod.rs
Normal file
2
src/ctx/api/v1/tests/mod.rs
Normal file
|
@ -0,0 +1,2 @@
|
|||
mod accounts;
|
||||
mod auth;
|
73
src/ctx/mod.rs
Normal file
73
src/ctx/mod.rs
Normal file
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* 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::sync::Arc;
|
||||
use std::thread;
|
||||
|
||||
use crate::db::*;
|
||||
use crate::settings::Settings;
|
||||
use argon2_creds::{Config as ArgonConfig, ConfigBuilder as ArgonConfigBuilder, PasswordPolicy};
|
||||
use reqwest::Client;
|
||||
use tracing::info;
|
||||
|
||||
pub mod api;
|
||||
|
||||
pub type ArcCtx = Arc<Ctx>;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Ctx {
|
||||
pub settings: Settings,
|
||||
pub db: Database,
|
||||
/// credential-procession policy
|
||||
pub creds: ArgonConfig,
|
||||
pub client: Client,
|
||||
}
|
||||
|
||||
impl Ctx {
|
||||
/// Get credential-processing policy
|
||||
pub fn get_creds() -> ArgonConfig {
|
||||
ArgonConfigBuilder::default()
|
||||
.username_case_mapped(true)
|
||||
.profanity(true)
|
||||
.blacklist(true)
|
||||
.password_policy(PasswordPolicy::default())
|
||||
.build()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub async fn new(settings: Settings) -> Arc<Self> {
|
||||
let creds = Self::get_creds();
|
||||
let c = creds.clone();
|
||||
|
||||
#[allow(unused_variables)]
|
||||
let init = thread::spawn(move || {
|
||||
info!("Initializing credential manager");
|
||||
c.init();
|
||||
info!("Initialized credential manager");
|
||||
});
|
||||
let db = get_db(&settings).await;
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
init.join();
|
||||
|
||||
Arc::new(Self {
|
||||
settings,
|
||||
db,
|
||||
creds,
|
||||
client: Client::default(),
|
||||
})
|
||||
}
|
||||
}
|
624
src/db.rs
Normal file
624
src/db.rs
Normal file
|
@ -0,0 +1,624 @@
|
|||
/*
|
||||
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
use std::str::FromStr;
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use sqlx::sqlite::SqlitePoolOptions;
|
||||
use sqlx::types::time::OffsetDateTime;
|
||||
use sqlx::ConnectOptions;
|
||||
use sqlx::Error;
|
||||
use sqlx::SqlitePool;
|
||||
use tracing::error;
|
||||
use url::Url;
|
||||
use uuid::Uuid;
|
||||
|
||||
use crate::errors::*;
|
||||
|
||||
/// Connect to databse
|
||||
pub enum ConnectionOptions {
|
||||
/// fresh connection
|
||||
Fresh(Fresh),
|
||||
/// existing connection
|
||||
Existing(Conn),
|
||||
}
|
||||
|
||||
/// Use an existing database pool
|
||||
pub struct Conn(pub SqlitePool);
|
||||
|
||||
pub struct Fresh {
|
||||
pub pool_options: SqlitePoolOptions,
|
||||
pub disable_logging: bool,
|
||||
pub url: String,
|
||||
}
|
||||
|
||||
impl ConnectionOptions {
|
||||
async fn connect(self) -> ServiceResult<Database> {
|
||||
let pool = match self {
|
||||
Self::Fresh(fresh) => {
|
||||
println!("from db.rs: {}", fresh.url);
|
||||
let mut connect_options =
|
||||
sqlx::sqlite::SqliteConnectOptions::from_str(&fresh.url).unwrap();
|
||||
if fresh.disable_logging {
|
||||
connect_options.disable_statement_logging();
|
||||
}
|
||||
sqlx::sqlite::SqliteConnectOptions::from_str(&fresh.url)
|
||||
.unwrap()
|
||||
.disable_statement_logging();
|
||||
fresh
|
||||
.pool_options
|
||||
.connect_with(connect_options)
|
||||
.await
|
||||
.unwrap()
|
||||
//.map_err(|e| ServiceError::ServiceError(Box::new(e)))?
|
||||
}
|
||||
|
||||
Self::Existing(conn) => conn.0,
|
||||
};
|
||||
Ok(Database { pool })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Database {
|
||||
pub pool: SqlitePool,
|
||||
}
|
||||
|
||||
impl Database {
|
||||
pub async fn migrate(&self) -> ServiceResult<()> {
|
||||
sqlx::migrate!("./migrations/")
|
||||
.run(&self.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
//.map_err(|e| ServiceError::ServiceError(Box::new(e)))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn ping(&self) -> bool {
|
||||
use sqlx::Connection;
|
||||
|
||||
if let Ok(mut con) = self.pool.acquire().await {
|
||||
con.ping().await.is_ok()
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// register a new user
|
||||
pub async fn register(&self, p: &Register<'_>) -> ServiceResult<()> {
|
||||
sqlx::query!(
|
||||
"INSERT INTO librepages_users
|
||||
(name , password, email) VALUES ($1, $2, $3)",
|
||||
p.username,
|
||||
p.hash,
|
||||
p.email,
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(map_register_err)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// delete a user
|
||||
pub async fn delete_user(&self, username: &str) -> ServiceResult<()> {
|
||||
sqlx::query!("DELETE FROM librepages_users WHERE name = ($1)", username)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// check if username exists
|
||||
pub async fn username_exists(&self, username: &str) -> ServiceResult<bool> {
|
||||
let res = sqlx::query!("SELECT ID from librepages_users WHERE name = $1", username,)
|
||||
.fetch_one(&self.pool)
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(_) => Ok(true),
|
||||
Err(Error::RowNotFound) => Ok(false),
|
||||
Err(e) => Err(map_register_err(e)),
|
||||
}
|
||||
}
|
||||
|
||||
/// get user email
|
||||
pub async fn get_email(&self, username: &str) -> ServiceResult<String> {
|
||||
struct Email {
|
||||
email: String,
|
||||
}
|
||||
|
||||
let res = sqlx::query_as!(
|
||||
Email,
|
||||
"SELECT email FROM librepages_users WHERE name = $1",
|
||||
username
|
||||
)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?;
|
||||
Ok(res.email)
|
||||
}
|
||||
|
||||
/// check if email exists
|
||||
pub async fn email_exists(&self, email: &str) -> ServiceResult<bool> {
|
||||
let res = sqlx::query!("SELECT ID from librepages_users WHERE email = $1", email)
|
||||
.fetch_one(&self.pool)
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(_) => Ok(true),
|
||||
Err(Error::RowNotFound) => Ok(false),
|
||||
Err(e) => Err(map_register_err(e)),
|
||||
}
|
||||
}
|
||||
|
||||
/// update a user's email
|
||||
pub async fn update_email(&self, p: &UpdateEmail<'_>) -> ServiceResult<()> {
|
||||
sqlx::query!(
|
||||
"UPDATE librepages_users set email = $1
|
||||
WHERE name = $2",
|
||||
p.new_email,
|
||||
p.username,
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// get a user's password
|
||||
pub async fn get_password(&self, l: &Login<'_>) -> ServiceResult<NameHash> {
|
||||
struct Password {
|
||||
name: String,
|
||||
password: String,
|
||||
}
|
||||
|
||||
let rec = match l {
|
||||
Login::Username(u) => sqlx::query_as!(
|
||||
Password,
|
||||
r#"SELECT name, password FROM librepages_users WHERE name = ($1)"#,
|
||||
u,
|
||||
)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?,
|
||||
Login::Email(e) => sqlx::query_as!(
|
||||
Password,
|
||||
r#"SELECT name, password FROM librepages_users WHERE email = ($1)"#,
|
||||
e,
|
||||
)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?,
|
||||
};
|
||||
|
||||
let res = NameHash {
|
||||
hash: rec.password,
|
||||
username: rec.name,
|
||||
};
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// update user's password
|
||||
pub async fn update_password(&self, p: &NameHash) -> ServiceResult<()> {
|
||||
sqlx::query!(
|
||||
"UPDATE librepages_users set password = $1
|
||||
WHERE name = $2",
|
||||
p.hash,
|
||||
p.username,
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// update username
|
||||
pub async fn update_username(&self, current: &str, new: &str) -> ServiceResult<()> {
|
||||
sqlx::query!(
|
||||
"UPDATE librepages_users set name = $1
|
||||
WHERE name = $2",
|
||||
new,
|
||||
current,
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn add_site(&self, msg: &Site) -> ServiceResult<()> {
|
||||
let url = msg.url.as_str();
|
||||
sqlx::query!(
|
||||
"
|
||||
INSERT INTO librepages_sites
|
||||
(url, owned_by)
|
||||
VALUES ($1, ( SELECT ID FROM librepages_users WHERE name = $2 ));
|
||||
",
|
||||
url,
|
||||
msg.owner,
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.unwrap();
|
||||
// .map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn get_site(&self, owner: &str, url: &str) -> ServiceResult<Site> {
|
||||
let site = sqlx::query_as!(
|
||||
InnerSite,
|
||||
"SELECT url
|
||||
FROM librepages_sites
|
||||
WHERE owned_by = (SELECT ID FROM librepages_users WHERE name = $1 )
|
||||
AND url = $2;
|
||||
",
|
||||
owner,
|
||||
url
|
||||
)
|
||||
.fetch_one(&self.pool)
|
||||
.await
|
||||
.map_err(|e| map_row_not_found_err(e, ServiceError::WebsiteNotFound))?;
|
||||
|
||||
site.to_site(owner.into())
|
||||
}
|
||||
|
||||
pub async fn list_all_sites(&self, owner: &str) -> ServiceResult<Vec<Site>> {
|
||||
let mut sites = sqlx::query_as!(
|
||||
InnerSite,
|
||||
"SELECT url
|
||||
FROM librepages_sites
|
||||
WHERE owned_by = (SELECT ID FROM librepages_users WHERE name = $1 );
|
||||
",
|
||||
owner,
|
||||
)
|
||||
.fetch_all(&self.pool)
|
||||
.await
|
||||
.map_err(|e| map_row_not_found_err(e, ServiceError::AccountNotFound))?;
|
||||
|
||||
let mut res = Vec::with_capacity(sites.len());
|
||||
|
||||
for site in sites.drain(0..) {
|
||||
res.push(site.to_site(owner.into())?);
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
pub async fn delete_site(&self, owner: &str, url: &str) -> ServiceResult<()> {
|
||||
sqlx::query!(
|
||||
"DELETE FROM librepages_sites
|
||||
WHERE url = ($1)
|
||||
AND owned_by = ( SELECT ID FROM librepages_users WHERE name = $2);
|
||||
",
|
||||
url,
|
||||
owner
|
||||
)
|
||||
.execute(&self.pool)
|
||||
.await
|
||||
.map_err(|e| map_row_not_found_err(e, ServiceError::WebsiteNotFound))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// check if url exists
|
||||
pub async fn url_exists(&self, url: &str) -> ServiceResult<bool> {
|
||||
let res = sqlx::query!("SELECT ID from librepages_sites WHERE url = $1", url,)
|
||||
.fetch_one(&self.pool)
|
||||
.await;
|
||||
|
||||
match res {
|
||||
Ok(_) => Ok(true),
|
||||
Err(Error::RowNotFound) => Ok(false),
|
||||
Err(e) => Err(map_register_err(e)),
|
||||
}
|
||||
}
|
||||
}
|
||||
struct InnerSite {
|
||||
url: String,
|
||||
}
|
||||
|
||||
impl InnerSite {
|
||||
fn to_site(self, owner: String) -> ServiceResult<Site> {
|
||||
Ok(Site {
|
||||
url: Url::parse(&self.url)?,
|
||||
owner,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
|
||||
/// Data required to add a new site
|
||||
pub struct Site {
|
||||
pub url: Url,
|
||||
pub owner: String,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
|
||||
/// Data required to register a new user
|
||||
pub struct Register<'a> {
|
||||
/// username of new user
|
||||
pub username: &'a str,
|
||||
/// hashed password of new use
|
||||
pub hash: &'a str,
|
||||
/// Optionally, email of new use
|
||||
pub email: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
|
||||
/// data required to update them email of a user
|
||||
pub struct UpdateEmail<'a> {
|
||||
/// username of the user
|
||||
pub username: &'a str,
|
||||
/// new email address of the user
|
||||
pub new_email: &'a str,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
|
||||
/// types of credentials used as identifiers during login
|
||||
pub enum Login<'a> {
|
||||
/// username as login
|
||||
Username(&'a str),
|
||||
/// email as login
|
||||
Email(&'a str),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
|
||||
/// type encapsulating username and hashed password of a user
|
||||
pub struct NameHash {
|
||||
/// username
|
||||
pub username: String,
|
||||
/// hashed password
|
||||
pub hash: String,
|
||||
}
|
||||
fn now_unix_time_stamp() -> OffsetDateTime {
|
||||
OffsetDateTime::now_utc()
|
||||
}
|
||||
|
||||
pub async fn get_db(settings: &crate::settings::Settings) -> Database {
|
||||
let pool_options = SqlitePoolOptions::new().max_connections(settings.database.pool);
|
||||
ConnectionOptions::Fresh(Fresh {
|
||||
pool_options,
|
||||
url: settings.database.url.clone(),
|
||||
disable_logging: !settings.debug,
|
||||
})
|
||||
.connect()
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
/// map custom row not found error to DB error
|
||||
pub fn map_row_not_found_err(e: sqlx::Error, row_not_found: ServiceError) -> ServiceError {
|
||||
if let sqlx::Error::RowNotFound = e {
|
||||
row_not_found
|
||||
} else {
|
||||
map_register_err(e)
|
||||
}
|
||||
}
|
||||
|
||||
/// map sqlite errors to [ServiceError](ServiceError) types
|
||||
fn map_register_err(e: sqlx::Error) -> ServiceError {
|
||||
use sqlx::Error;
|
||||
use std::borrow::Cow;
|
||||
|
||||
if let Error::Database(err) = e {
|
||||
if err.code() == Some(Cow::from("23505")) {
|
||||
let msg = err.message();
|
||||
println!("{}", msg);
|
||||
if msg.contains("librepages_users_name_key") {
|
||||
ServiceError::UsernameTaken
|
||||
} else if msg.contains("librepages_users_email_key") {
|
||||
ServiceError::EmailTaken
|
||||
} else {
|
||||
error!("{}", msg);
|
||||
ServiceError::InternalServerError
|
||||
}
|
||||
} else {
|
||||
ServiceError::InternalServerError
|
||||
}
|
||||
} else {
|
||||
ServiceError::InternalServerError
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::collections::HashSet;
|
||||
|
||||
use super::*;
|
||||
use crate::settings::Settings;
|
||||
#[actix_rt::test]
|
||||
async fn db_works() {
|
||||
let settings = Settings::new().unwrap();
|
||||
let pool_options = SqlitePoolOptions::new().max_connections(1);
|
||||
let db = ConnectionOptions::Fresh(Fresh {
|
||||
pool_options,
|
||||
url: settings.database.url.clone(),
|
||||
disable_logging: !settings.debug,
|
||||
})
|
||||
.connect()
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(db.ping().await);
|
||||
|
||||
const EMAIL: &str = "sqliteuser@foo.com";
|
||||
const EMAIL2: &str = "sqliteuser2@foo.com";
|
||||
const NAME: &str = "sqliteuser";
|
||||
const PASSWORD: &str = "pasdfasdfasdfadf";
|
||||
|
||||
db.migrate().await.unwrap();
|
||||
let p = super::Register {
|
||||
username: NAME,
|
||||
email: EMAIL,
|
||||
hash: PASSWORD,
|
||||
};
|
||||
|
||||
if db.username_exists(p.username).await.unwrap() {
|
||||
db.delete_user(p.username).await.unwrap();
|
||||
assert!(
|
||||
!db.username_exists(p.username).await.unwrap(),
|
||||
"user is deleted so username shouldn't exist"
|
||||
);
|
||||
}
|
||||
|
||||
db.register(&p).await.unwrap();
|
||||
|
||||
// assert!(matches!(
|
||||
// db.register(&p).await,
|
||||
// Err(ServiceError::UsernameTaken)
|
||||
// ));
|
||||
|
||||
// testing get_password
|
||||
|
||||
// with username
|
||||
let name_hash = db.get_password(&Login::Username(p.username)).await.unwrap();
|
||||
assert_eq!(name_hash.hash, p.hash, "user password matches");
|
||||
|
||||
assert_eq!(name_hash.username, p.username, "username matches");
|
||||
|
||||
// with email
|
||||
let mut name_hash = db.get_password(&Login::Email(p.email)).await.unwrap();
|
||||
assert_eq!(name_hash.hash, p.hash, "user password matches");
|
||||
assert_eq!(name_hash.username, p.username, "username matches");
|
||||
|
||||
// testing get_email
|
||||
assert_eq!(db.get_email(p.username).await.unwrap(), p.email);
|
||||
|
||||
// testing email exists
|
||||
assert!(
|
||||
db.email_exists(p.email).await.unwrap(),
|
||||
"user is registered so email should exist"
|
||||
);
|
||||
assert!(
|
||||
db.username_exists(p.username).await.unwrap(),
|
||||
"user is registered so username should exist"
|
||||
);
|
||||
|
||||
// update password test. setting password = username
|
||||
name_hash.hash = name_hash.username.clone();
|
||||
db.update_password(&name_hash).await.unwrap();
|
||||
|
||||
let name_hash = db.get_password(&Login::Username(p.username)).await.unwrap();
|
||||
assert_eq!(
|
||||
name_hash.hash, p.username,
|
||||
"user password matches with changed value"
|
||||
);
|
||||
assert_eq!(name_hash.username, p.username, "username matches");
|
||||
|
||||
// update username to p.email
|
||||
assert!(
|
||||
!db.username_exists(p.email).await.unwrap(),
|
||||
"user with p.email doesn't exist. pre-check to update username to p.email"
|
||||
);
|
||||
db.update_username(p.username, p.email).await.unwrap();
|
||||
assert!(
|
||||
db.username_exists(p.email).await.unwrap(),
|
||||
"user with p.email exist post-update"
|
||||
);
|
||||
|
||||
// testing update email
|
||||
let update_email = UpdateEmail {
|
||||
username: p.username,
|
||||
new_email: EMAIL2,
|
||||
};
|
||||
db.update_email(&update_email).await.unwrap();
|
||||
println!(
|
||||
"null user email: {}",
|
||||
db.email_exists(p.email).await.unwrap()
|
||||
);
|
||||
assert!(
|
||||
db.email_exists(p.email).await.unwrap(),
|
||||
"user was with empty email but email is set; so email should exist"
|
||||
);
|
||||
|
||||
// deleting user
|
||||
db.delete_user(p.email).await.unwrap();
|
||||
assert!(
|
||||
!db.username_exists(p.email).await.unwrap(),
|
||||
"user is deleted so username shouldn't exist"
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
pub async fn test_db_sites() {
|
||||
let settings = Settings::new().unwrap();
|
||||
let pool_options = SqlitePoolOptions::new().max_connections(1);
|
||||
let db = ConnectionOptions::Fresh(Fresh {
|
||||
pool_options,
|
||||
url: settings.database.url.clone(),
|
||||
disable_logging: !settings.debug,
|
||||
})
|
||||
.connect()
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(db.ping().await);
|
||||
|
||||
const EMAIL: &str = "sqlitedbsiteuser@foo.com";
|
||||
const NAME: &str = "sqlitedbsiteuser";
|
||||
const PASSWORD: &str = "pasdfasdfasdfadf";
|
||||
|
||||
db.migrate().await.unwrap();
|
||||
|
||||
let p = super::Register {
|
||||
username: NAME,
|
||||
email: EMAIL,
|
||||
hash: PASSWORD,
|
||||
};
|
||||
|
||||
if db.username_exists(p.username).await.unwrap() {
|
||||
db.delete_user(p.username).await.unwrap();
|
||||
assert!(
|
||||
!db.username_exists(p.username).await.unwrap(),
|
||||
"user is deleted so username shouldn't exist"
|
||||
);
|
||||
}
|
||||
|
||||
db.register(&p).await.unwrap();
|
||||
|
||||
let site = Site {
|
||||
url: Url::parse("https://db_works.tests.librepages.librepages.org").unwrap(),
|
||||
owner: p.username.into(),
|
||||
};
|
||||
|
||||
// test if url exists. Should be false
|
||||
assert!(!db.url_exists(site.url.as_str()).await.unwrap());
|
||||
|
||||
// testing adding site
|
||||
db.add_site(&site).await.unwrap();
|
||||
|
||||
// test if url exists. Should be true
|
||||
assert!(db.url_exists(site.url.as_str()).await.unwrap());
|
||||
|
||||
// get site
|
||||
let db_site = db.get_site(p.username, site.url.as_str()).await.unwrap();
|
||||
assert_eq!(db_site, site);
|
||||
|
||||
// list all sites owned by user
|
||||
let db_sites = db.list_all_sites(p.username).await.unwrap();
|
||||
assert_eq!(db_sites.len(), 1);
|
||||
assert_eq!(db_sites, vec![site.clone()]);
|
||||
|
||||
// delete site
|
||||
db.delete_site(p.username, site.url.as_str()).await.unwrap();
|
||||
|
||||
// test if url exists. Should be false
|
||||
assert!(!db.url_exists(site.url.as_str()).await.unwrap());
|
||||
}
|
||||
}
|
268
src/errors.rs
Normal file
268
src/errors.rs
Normal file
|
@ -0,0 +1,268 @@
|
|||
/*
|
||||
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
//! Represents all the ways a trait can fail using this crate
|
||||
use std::convert::From;
|
||||
use std::io::Error as FSErrorInner;
|
||||
use std::sync::Arc;
|
||||
|
||||
use actix_web::{
|
||||
error::ResponseError,
|
||||
http::{header, StatusCode},
|
||||
HttpResponse, HttpResponseBuilder,
|
||||
};
|
||||
use argon2_creds::errors::CredsError;
|
||||
use config::ConfigError as ConfigErrorInner;
|
||||
use derive_more::{Display, Error};
|
||||
use reqwest::Error as ReqwestError;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use url::ParseError;
|
||||
|
||||
use crate::page::Page;
|
||||
|
||||
#[derive(Debug, Display, Error)]
|
||||
pub struct FSError(#[display(fmt = "File System Error {}", _0)] pub FSErrorInner);
|
||||
|
||||
#[derive(Debug, Display, Error)]
|
||||
pub struct ConfigError(#[display(fmt = "Configuration Error {}", _0)] pub ConfigErrorInner);
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl PartialEq for FSError {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0.kind() == other.0.kind()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl PartialEq for ConfigError {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.0.to_string().trim() == other.0.to_string().trim()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl From<FSErrorInner> for ServiceError {
|
||||
fn from(e: FSErrorInner) -> Self {
|
||||
Self::FSError(FSError(e))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl From<ConfigErrorInner> for ServiceError {
|
||||
fn from(e: ConfigErrorInner) -> Self {
|
||||
Self::ConfigError(ConfigError(e))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, PartialEq, Error)]
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
/// Error data structure grouping various error subtypes
|
||||
pub enum ServiceError {
|
||||
/// All non-specific errors are grouped under this category
|
||||
#[display(fmt = "internal server error")]
|
||||
InternalServerError,
|
||||
|
||||
#[display(fmt = "The value you entered for URL is not a URL")] //405j
|
||||
/// The value you entered for url is not url"
|
||||
NotAUrl,
|
||||
#[display(fmt = "URL too long, maximum length can't be greater then 2048 characters")] //405
|
||||
/// URL too long, maximum length can't be greater then 2048 characters
|
||||
URLTooLong,
|
||||
|
||||
#[display(fmt = "Website not found")]
|
||||
/// website not found
|
||||
WebsiteNotFound,
|
||||
|
||||
#[display(fmt = "File not found")]
|
||||
/// File not found
|
||||
FileNotFound,
|
||||
|
||||
/// when the a path configured for a page is already taken
|
||||
#[display(
|
||||
fmt = "Path already used for another website. lhs: {:?} rhs: {:?}",
|
||||
_0,
|
||||
_1
|
||||
)]
|
||||
PathTaken(Arc<Page>, Arc<Page>),
|
||||
|
||||
/// when the a Secret configured for a page is already taken
|
||||
#[display(
|
||||
fmt = "Secret already used for another website. lhs: {:?} rhs: {:?}",
|
||||
_0,
|
||||
_1
|
||||
)]
|
||||
SecretTaken(Arc<Page>, Arc<Page>),
|
||||
|
||||
/// when the a Repository URL configured for a page is already taken
|
||||
#[display(
|
||||
fmt = "Repository URL already configured for another website deployment. lhs: {:?} rhs: {:?}",
|
||||
_0,
|
||||
_1
|
||||
)]
|
||||
DuplicateRepositoryURL(Arc<Page>, Arc<Page>),
|
||||
|
||||
#[display(fmt = "File System Error {}", _0)]
|
||||
FSError(FSError),
|
||||
|
||||
#[display(fmt = "Unauthorized {}", _0)]
|
||||
UnauthorizedOperation(#[error(not(source))] String),
|
||||
|
||||
#[display(fmt = "Bad request: {}", _0)]
|
||||
BadRequest(#[error(not(source))] String),
|
||||
|
||||
#[display(fmt = "Configuration Error {}", _0)]
|
||||
ConfigError(ConfigError),
|
||||
#[display(fmt = "Branch {} not found", _0)]
|
||||
BranchNotFound(#[error(not(source))] String),
|
||||
|
||||
/// Username is taken
|
||||
#[display(fmt = "Username is taken")]
|
||||
UsernameTaken,
|
||||
/// Email is taken
|
||||
#[display(fmt = "Email is taken")]
|
||||
EmailTaken,
|
||||
/// Account not found
|
||||
#[display(fmt = "Account not found")]
|
||||
AccountNotFound,
|
||||
|
||||
#[display(
|
||||
fmt = "This server is is closed for registration. Contact admin if this is unexpecter"
|
||||
)]
|
||||
/// registration failure, server is is closed for registration
|
||||
ClosedForRegistration,
|
||||
|
||||
#[display(fmt = "The value you entered for email is not an email")] //405j
|
||||
/// The value you entered for email is not an email"
|
||||
NotAnEmail,
|
||||
|
||||
#[display(fmt = "Wrong password")]
|
||||
/// wrong password
|
||||
WrongPassword,
|
||||
|
||||
/// when the value passed contains profanity
|
||||
#[display(fmt = "Can't allow profanity in usernames")]
|
||||
ProfanityError,
|
||||
/// when the value passed contains blacklisted words
|
||||
/// see [blacklist](https://github.com/shuttlecraft/The-Big-Username-Blacklist)
|
||||
#[display(fmt = "Username contains blacklisted words")]
|
||||
BlacklistError,
|
||||
/// when the value passed contains characters not present
|
||||
/// in [UsernameCaseMapped](https://tools.ietf.org/html/rfc8265#page-7)
|
||||
/// profile
|
||||
#[display(fmt = "username_case_mapped violation")]
|
||||
UsernameCaseMappedError,
|
||||
|
||||
#[display(fmt = "Passsword too short")]
|
||||
/// password too short
|
||||
PasswordTooShort,
|
||||
#[display(fmt = "password too long")]
|
||||
/// password too long
|
||||
PasswordTooLong,
|
||||
#[display(fmt = "Passwords don't match")]
|
||||
/// passwords don't match
|
||||
PasswordsDontMatch,
|
||||
}
|
||||
|
||||
impl From<ReqwestError> for ServiceError {
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
fn from(e: ReqwestError) -> ServiceError {
|
||||
tracing::error!("{}", e);
|
||||
ServiceError::InternalServerError
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ParseError> for ServiceError {
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
fn from(_: ParseError) -> ServiceError {
|
||||
ServiceError::NotAUrl
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic result data structure
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
pub type ServiceResult<V> = std::result::Result<V, ServiceError>;
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
pub struct ErrorToResponse {
|
||||
pub error: String,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl ResponseError for ServiceError {
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
HttpResponseBuilder::new(self.status_code())
|
||||
.append_header((header::CONTENT_TYPE, "application/json; charset=UTF-8"))
|
||||
.body(
|
||||
serde_json::to_string(&ErrorToResponse {
|
||||
error: self.to_string(),
|
||||
})
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
ServiceError::InternalServerError => StatusCode::INTERNAL_SERVER_ERROR, // INTERNAL SERVER ERROR
|
||||
ServiceError::ConfigError(_) => StatusCode::INTERNAL_SERVER_ERROR, // INTERNAL SERVER ERROR
|
||||
ServiceError::NotAUrl => StatusCode::BAD_REQUEST, //BADREQUEST,
|
||||
ServiceError::URLTooLong => StatusCode::BAD_REQUEST, //BADREQUEST,
|
||||
ServiceError::WebsiteNotFound => StatusCode::NOT_FOUND, //NOT FOUND,
|
||||
|
||||
ServiceError::PathTaken(_, _) => StatusCode::BAD_REQUEST, //BADREQUEST,
|
||||
ServiceError::DuplicateRepositoryURL(_, _) => StatusCode::BAD_REQUEST, //BADREQUEST,
|
||||
ServiceError::SecretTaken(_, _) => StatusCode::BAD_REQUEST, //BADREQUEST,
|
||||
ServiceError::FSError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
|
||||
ServiceError::UnauthorizedOperation(_) => StatusCode::UNAUTHORIZED,
|
||||
ServiceError::BadRequest(_) => StatusCode::BAD_REQUEST,
|
||||
ServiceError::BranchNotFound(_) => StatusCode::CONFLICT,
|
||||
|
||||
ServiceError::EmailTaken => StatusCode::BAD_REQUEST,
|
||||
ServiceError::UsernameTaken => StatusCode::BAD_REQUEST,
|
||||
ServiceError::AccountNotFound => StatusCode::NOT_FOUND,
|
||||
ServiceError::FileNotFound => StatusCode::NOT_FOUND,
|
||||
|
||||
ServiceError::ProfanityError => StatusCode::BAD_REQUEST, //BADREQUEST,
|
||||
ServiceError::BlacklistError => StatusCode::BAD_REQUEST, //BADREQUEST,
|
||||
ServiceError::UsernameCaseMappedError => StatusCode::BAD_REQUEST, //BADREQUEST,
|
||||
|
||||
ServiceError::PasswordTooShort => StatusCode::BAD_REQUEST, //BADREQUEST,
|
||||
ServiceError::PasswordTooLong => StatusCode::BAD_REQUEST, //BADREQUEST,
|
||||
ServiceError::PasswordsDontMatch => StatusCode::BAD_REQUEST, //BADREQUEST,
|
||||
ServiceError::ClosedForRegistration => StatusCode::FORBIDDEN, //FORBIDDEN,
|
||||
ServiceError::NotAnEmail => StatusCode::BAD_REQUEST, //BADREQUEST,
|
||||
ServiceError::WrongPassword => StatusCode::UNAUTHORIZED, //UNAUTHORIZED,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<CredsError> for ServiceError {
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
fn from(e: CredsError) -> ServiceError {
|
||||
match e {
|
||||
CredsError::UsernameCaseMappedError => ServiceError::UsernameCaseMappedError,
|
||||
CredsError::ProfainityError => ServiceError::ProfanityError,
|
||||
CredsError::BlacklistError => ServiceError::BlacklistError,
|
||||
CredsError::NotAnEmail => ServiceError::NotAnEmail,
|
||||
CredsError::Argon2Error(_) => ServiceError::InternalServerError,
|
||||
CredsError::PasswordTooLong => ServiceError::PasswordTooLong,
|
||||
CredsError::PasswordTooShort => ServiceError::PasswordTooShort,
|
||||
}
|
||||
}
|
||||
}
|
159
src/main.rs
Normal file
159
src/main.rs
Normal file
|
@ -0,0 +1,159 @@
|
|||
/*
|
||||
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
use std::env;
|
||||
|
||||
use actix_identity::{CookieIdentityPolicy, IdentityService};
|
||||
use actix_web::{
|
||||
error::InternalError, http::StatusCode, middleware as actix_middleware, web::Data as WebData,
|
||||
web::JsonConfig, App, HttpServer,
|
||||
};
|
||||
use clap::{Parser, Subcommand};
|
||||
use static_assets::FileMap;
|
||||
use tracing::info;
|
||||
use tracing_actix_web::TracingLogger;
|
||||
|
||||
mod api;
|
||||
mod ctx;
|
||||
mod db;
|
||||
mod errors;
|
||||
mod page;
|
||||
mod pages;
|
||||
mod serve;
|
||||
mod settings;
|
||||
mod static_assets;
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
mod utils;
|
||||
|
||||
pub use crate::api::v1::ROUTES as V1_API_ROUTES;
|
||||
use ctx::Ctx;
|
||||
pub use settings::Settings;
|
||||
|
||||
pub const CACHE_AGE: u32 = 604800;
|
||||
|
||||
pub const GIT_COMMIT_HASH: &str = env!("GIT_HASH");
|
||||
pub const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
pub const PKG_NAME: &str = env!("CARGO_PKG_NAME");
|
||||
pub const PKG_DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION");
|
||||
pub const PKG_HOMEPAGE: &str = env!("CARGO_PKG_HOMEPAGE");
|
||||
|
||||
pub type AppCtx = WebData<ctx::ArcCtx>;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref FILES: FileMap = FileMap::new();
|
||||
}
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(author, version, about, long_about = None)]
|
||||
struct Cli {
|
||||
#[clap(subcommand)]
|
||||
command: Commands,
|
||||
}
|
||||
|
||||
#[derive(Subcommand)]
|
||||
enum Commands {
|
||||
/// run database migrations
|
||||
Migrate,
|
||||
|
||||
/// run server
|
||||
Serve,
|
||||
}
|
||||
|
||||
#[actix_web::main]
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
if env::var("RUST_LOG").is_err() {
|
||||
env::set_var("RUST_LOG", "info");
|
||||
}
|
||||
|
||||
pretty_env_logger::init();
|
||||
let cli = Cli::parse();
|
||||
|
||||
info!(
|
||||
"{}: {}.\nFor more information, see: {}\nBuild info:\nVersion: {} commit: {}",
|
||||
PKG_NAME, PKG_DESCRIPTION, PKG_HOMEPAGE, VERSION, GIT_COMMIT_HASH
|
||||
);
|
||||
|
||||
let settings = Settings::new().unwrap();
|
||||
|
||||
match &cli.command {
|
||||
Commands::Migrate => db::get_db(&settings).await.migrate().await.unwrap(),
|
||||
Commands::Serve => {
|
||||
let ctx = Ctx::new(settings.clone()).await;
|
||||
let ctx = actix_web::web::Data::new(ctx);
|
||||
serve(settings, ctx).await.unwrap();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn serve(settings: Settings, ctx: AppCtx) -> std::io::Result<()> {
|
||||
let ip = settings.server.get_ip();
|
||||
let workers = settings.server.workers.unwrap_or_else(num_cpus::get);
|
||||
|
||||
info!("Starting server on: http://{}", ip);
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.wrap(TracingLogger::default())
|
||||
.wrap(actix_middleware::Compress::default())
|
||||
.app_data(ctx.clone())
|
||||
.app_data(get_json_err())
|
||||
.wrap(get_identity_service(&(settings.clone())))
|
||||
.wrap(
|
||||
actix_middleware::DefaultHeaders::new()
|
||||
.add(("Permissions-Policy", "interest-cohort=()")),
|
||||
)
|
||||
.wrap(actix_middleware::NormalizePath::new(
|
||||
actix_middleware::TrailingSlash::Trim,
|
||||
))
|
||||
.configure(services)
|
||||
})
|
||||
.workers(workers)
|
||||
.bind(ip)
|
||||
.unwrap()
|
||||
.run()
|
||||
.await
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
pub fn get_json_err() -> JsonConfig {
|
||||
JsonConfig::default().error_handler(|err, _| {
|
||||
//debug!("JSON deserialization error: {:?}", &err);
|
||||
InternalError::new(err, StatusCode::BAD_REQUEST).into()
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
pub fn get_identity_service(settings: &Settings) -> IdentityService<CookieIdentityPolicy> {
|
||||
let cookie_secret = &settings.server.cookie_secret;
|
||||
IdentityService::new(
|
||||
CookieIdentityPolicy::new(cookie_secret.as_bytes())
|
||||
.path("/")
|
||||
.name("Authorization")
|
||||
//TODO change cookie age
|
||||
.max_age_secs(216000)
|
||||
.domain(&settings.server.domain)
|
||||
.secure(false),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn services(cfg: &mut actix_web::web::ServiceConfig) {
|
||||
crate::api::v1::services(cfg);
|
||||
crate::pages::services(cfg);
|
||||
crate::serve::services(cfg);
|
||||
crate::static_assets::services(cfg);
|
||||
}
|
93
src/page.rs
Normal file
93
src/page.rs
Normal file
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* 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::path::Path;
|
||||
#[cfg(test)]
|
||||
use std::println as info;
|
||||
#[cfg(test)]
|
||||
use std::println as error;
|
||||
#[cfg(test)]
|
||||
use std::println as debug;
|
||||
|
||||
use futures_util::StreamExt;
|
||||
use reqwest::header::CONTENT_TYPE;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use tokio::fs;
|
||||
use tokio::io::{self, AsyncWriteExt, BufWriter};
|
||||
#[cfg(not(test))]
|
||||
use tracing::{debug, error, info};
|
||||
use url::Url;
|
||||
|
||||
use crate::ctx::Ctx;
|
||||
use crate::db::Site;
|
||||
use crate::errors::*;
|
||||
use crate::settings::Settings;
|
||||
use crate::utils;
|
||||
|
||||
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
|
||||
pub struct Page {
|
||||
pub file_path: String,
|
||||
pub url: Url,
|
||||
}
|
||||
|
||||
impl Page {
|
||||
pub fn from_site(settings: &Settings, s: Site) -> Self {
|
||||
Self {
|
||||
file_path: utils::get_website_path(settings, &s.owner, &s.url)
|
||||
.to_str()
|
||||
.unwrap()
|
||||
.to_owned(),
|
||||
url: s.url,
|
||||
}
|
||||
}
|
||||
async fn create_parent_dir_all(&self, path: &str) -> ServiceResult<()> {
|
||||
if let Some(parent) = Path::new(path).parent() {
|
||||
fs::create_dir_all(parent).await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn archive(&self, ctx: &Ctx) -> ServiceResult<()> {
|
||||
self.create_parent_dir_all(&self.file_path).await?;
|
||||
let res = ctx.client.get(self.url.as_str()).send().await?;
|
||||
let mut fetch_res = false;
|
||||
if let Some(content_type) = res.headers().get(CONTENT_TYPE) {
|
||||
if let Ok(content_type) = content_type.to_str() {
|
||||
if content_type.contains("text/html") {
|
||||
fetch_res = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let mut bytes = res.bytes_stream();
|
||||
let file = fs::OpenOptions::new()
|
||||
.read(true)
|
||||
.write(true)
|
||||
.truncate(true)
|
||||
.create(true)
|
||||
.open(&self.file_path)
|
||||
.await?;
|
||||
let mut writer = BufWriter::new(file);
|
||||
|
||||
while let Some(item) = bytes.next().await {
|
||||
let _ = writer.write(&item?).await?;
|
||||
}
|
||||
writer.flush().await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
121
src/pages/auth/login.rs
Normal file
121
src/pages/auth/login.rs
Normal file
|
@ -0,0 +1,121 @@
|
|||
/*
|
||||
* 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::cell::RefCell;
|
||||
|
||||
use actix_identity::Identity;
|
||||
use actix_web::http::header::ContentType;
|
||||
use tera::Context;
|
||||
|
||||
use crate::api::v1::RedirectQuery;
|
||||
use crate::ctx::api::v1::auth::Login as LoginPayload;
|
||||
use crate::pages::errors::*;
|
||||
use crate::settings::Settings;
|
||||
use crate::AppCtx;
|
||||
|
||||
pub use super::*;
|
||||
|
||||
pub struct Login {
|
||||
ctx: RefCell<Context>,
|
||||
}
|
||||
|
||||
pub const LOGIN: TemplateFile = TemplateFile::new("login", "pages/auth/login.html");
|
||||
|
||||
impl CtxError for Login {
|
||||
fn with_error(&self, e: &ReadableError) -> String {
|
||||
self.ctx.borrow_mut().insert(ERROR_KEY, e);
|
||||
self.render()
|
||||
}
|
||||
}
|
||||
|
||||
impl Login {
|
||||
pub fn new(settings: &Settings, payload: Option<&LoginPayload>) -> Self {
|
||||
let ctx = RefCell::new(context(settings));
|
||||
if let Some(payload) = payload {
|
||||
ctx.borrow_mut().insert(PAYLOAD_KEY, payload);
|
||||
}
|
||||
Self { ctx }
|
||||
}
|
||||
|
||||
pub fn render(&self) -> String {
|
||||
TEMPLATES.render(LOGIN.name, &self.ctx.borrow()).unwrap()
|
||||
}
|
||||
|
||||
pub fn page(s: &Settings) -> String {
|
||||
let p = Self::new(s, None);
|
||||
p.render()
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_web_codegen_const_routes::get(path = "PAGES.auth.login")]
|
||||
#[tracing::instrument(name = "Serve login page", skip(ctx))]
|
||||
pub async fn get_login(ctx: AppCtx) -> impl Responder {
|
||||
let login = Login::page(&ctx.settings);
|
||||
let html = ContentType::html();
|
||||
HttpResponse::Ok().content_type(html).body(login)
|
||||
}
|
||||
|
||||
pub fn services(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(get_login);
|
||||
cfg.service(login_submit);
|
||||
}
|
||||
|
||||
#[actix_web_codegen_const_routes::post(path = "PAGES.auth.login")]
|
||||
#[tracing::instrument(name = "Web UI Login", skip(id, payload, query, ctx))]
|
||||
pub async fn login_submit(
|
||||
id: Identity,
|
||||
payload: web::Form<LoginPayload>,
|
||||
query: web::Query<RedirectQuery>,
|
||||
ctx: AppCtx,
|
||||
) -> PageResult<impl Responder, Login> {
|
||||
let username = ctx
|
||||
.login(&payload)
|
||||
.await
|
||||
.map_err(|e| PageError::new(Login::new(&ctx.settings, Some(&payload)), e))?;
|
||||
id.remember(username);
|
||||
let query = query.into_inner();
|
||||
if let Some(redirect_to) = query.redirect_to {
|
||||
Ok(HttpResponse::Found()
|
||||
.insert_header((http::header::LOCATION, redirect_to))
|
||||
.finish())
|
||||
} else {
|
||||
Ok(HttpResponse::Found()
|
||||
.insert_header((http::header::LOCATION, PAGES.dash.home))
|
||||
.finish())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Login;
|
||||
use super::LoginPayload;
|
||||
use crate::errors::*;
|
||||
use crate::pages::errors::*;
|
||||
use crate::settings::Settings;
|
||||
|
||||
#[test]
|
||||
fn register_page_renders() {
|
||||
let settings = Settings::new().unwrap();
|
||||
Login::page(&settings);
|
||||
let payload = LoginPayload {
|
||||
login: "foo".into(),
|
||||
password: "foo".into(),
|
||||
};
|
||||
let page = Login::new(&settings, Some(&payload));
|
||||
page.with_error(&ReadableError::new(&ServiceError::WrongPassword));
|
||||
page.render();
|
||||
}
|
||||
}
|
162
src/pages/auth/mod.rs
Normal file
162
src/pages/auth/mod.rs
Normal file
|
@ -0,0 +1,162 @@
|
|||
/*
|
||||
* 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::*;
|
||||
|
||||
pub use super::{context, Footer, TemplateFile, PAGES, PAYLOAD_KEY, TEMPLATES};
|
||||
|
||||
pub mod login;
|
||||
pub mod register;
|
||||
#[cfg(test)]
|
||||
mod test;
|
||||
|
||||
pub const AUTH_BASE: TemplateFile = TemplateFile::new("authbase", "pages/auth/base.html");
|
||||
|
||||
pub fn register_templates(t: &mut tera::Tera) {
|
||||
for template in [AUTH_BASE, login::LOGIN, register::REGISTER].iter() {
|
||||
template.register(t).expect(template.name);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn services(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(signout);
|
||||
register::services(cfg);
|
||||
login::services(cfg);
|
||||
}
|
||||
|
||||
#[actix_web_codegen_const_routes::get(
|
||||
path = "PAGES.auth.logout",
|
||||
wrap = "super::get_auth_middleware()"
|
||||
)]
|
||||
#[tracing::instrument(name = "Sign out", skip(id))]
|
||||
async fn signout(id: Identity) -> impl Responder {
|
||||
use actix_auth_middleware::GetLoginRoute;
|
||||
|
||||
if id.identity().is_some() {
|
||||
id.forget();
|
||||
}
|
||||
HttpResponse::Found()
|
||||
.append_header((http::header::LOCATION, PAGES.get_login_route(None)))
|
||||
.finish()
|
||||
}
|
||||
|
||||
//#[post(path = "PAGES.auth.login")]
|
||||
//pub async fn login_submit(
|
||||
// id: Identity,
|
||||
// payload: web::Form<runners::Login>,
|
||||
// data: AppData,
|
||||
//) -> PageResult<impl Responder> {
|
||||
// let payload = payload.into_inner();
|
||||
// match runners::login_runner(&payload, &data).await {
|
||||
// Ok(username) => {
|
||||
// id.remember(username);
|
||||
// Ok(HttpResponse::Found()
|
||||
// .insert_header((header::LOCATION, PAGES.home))
|
||||
// .finish())
|
||||
// }
|
||||
// Err(e) => {
|
||||
// let status = e.status_code();
|
||||
// let heading = status.canonical_reason().unwrap_or("Error");
|
||||
//
|
||||
// Ok(HttpResponseBuilder::new(status)
|
||||
// .content_type("text/html; charset=utf-8")
|
||||
// .body(
|
||||
// IndexPage::new(heading, &format!("{}", e))
|
||||
// .render_once()
|
||||
// .unwrap(),
|
||||
// ))
|
||||
// }
|
||||
// }
|
||||
//}
|
||||
//
|
||||
//#[cfg(test)]
|
||||
//mod tests {
|
||||
// use actix_web::test;
|
||||
//
|
||||
// use super::*;
|
||||
//
|
||||
// use crate::api::v1::auth::runners::{Login, Register};
|
||||
// use crate::data::Data;
|
||||
// use crate::tests::*;
|
||||
// use crate::*;
|
||||
// use actix_web::http::StatusCode;
|
||||
//
|
||||
// #[actix_rt::test]
|
||||
// async fn auth_form_works() {
|
||||
// let data = Data::new().await;
|
||||
// const NAME: &str = "testuserform";
|
||||
// const PASSWORD: &str = "longpassword";
|
||||
//
|
||||
// let app = get_app!(data).await;
|
||||
//
|
||||
// delete_user(NAME, &data).await;
|
||||
//
|
||||
// // 1. Register with email == None
|
||||
// let msg = Register {
|
||||
// username: NAME.into(),
|
||||
// password: PASSWORD.into(),
|
||||
// confirm_password: PASSWORD.into(),
|
||||
// email: None,
|
||||
// };
|
||||
// let resp = test::call_service(
|
||||
// &app,
|
||||
// post_request!(&msg, V1_API_ROUTES.auth.register).to_request(),
|
||||
// )
|
||||
// .await;
|
||||
// assert_eq!(resp.status(), StatusCode::OK);
|
||||
//
|
||||
// // correct form login
|
||||
// let msg = Login {
|
||||
// login: NAME.into(),
|
||||
// password: PASSWORD.into(),
|
||||
// };
|
||||
//
|
||||
// let resp = test::call_service(
|
||||
// &app,
|
||||
// post_request!(&msg, PAGES.auth.login, FORM).to_request(),
|
||||
// )
|
||||
// .await;
|
||||
// assert_eq!(resp.status(), StatusCode::FOUND);
|
||||
// let headers = resp.headers();
|
||||
// assert_eq!(headers.get(header::LOCATION).unwrap(), PAGES.home,);
|
||||
//
|
||||
// // incorrect form login
|
||||
// let msg = Login {
|
||||
// login: NAME.into(),
|
||||
// password: NAME.into(),
|
||||
// };
|
||||
// let resp = test::call_service(
|
||||
// &app,
|
||||
// post_request!(&msg, PAGES.auth.login, FORM).to_request(),
|
||||
// )
|
||||
// .await;
|
||||
// assert_eq!(resp.status(), StatusCode::UNAUTHORIZED);
|
||||
//
|
||||
// // non-existent form login
|
||||
// let msg = Login {
|
||||
// login: PASSWORD.into(),
|
||||
// password: PASSWORD.into(),
|
||||
// };
|
||||
// let resp = test::call_service(
|
||||
// &app,
|
||||
// post_request!(&msg, PAGES.auth.login, FORM).to_request(),
|
||||
// )
|
||||
// .await;
|
||||
// assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||
// }
|
||||
//}
|
||||
//
|
109
src/pages/auth/register.rs
Normal file
109
src/pages/auth/register.rs
Normal file
|
@ -0,0 +1,109 @@
|
|||
/*
|
||||
* 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::header::ContentType;
|
||||
use std::cell::RefCell;
|
||||
use tera::Context;
|
||||
|
||||
use crate::ctx::api::v1::auth::Register as RegisterPayload;
|
||||
use crate::pages::errors::*;
|
||||
use crate::settings::Settings;
|
||||
use crate::AppCtx;
|
||||
|
||||
pub use super::*;
|
||||
|
||||
pub const REGISTER: TemplateFile = TemplateFile::new("register", "pages/auth/register.html");
|
||||
|
||||
pub struct Register {
|
||||
ctx: RefCell<Context>,
|
||||
}
|
||||
|
||||
impl CtxError for Register {
|
||||
fn with_error(&self, e: &ReadableError) -> String {
|
||||
self.ctx.borrow_mut().insert(ERROR_KEY, e);
|
||||
self.render()
|
||||
}
|
||||
}
|
||||
|
||||
impl Register {
|
||||
fn new(settings: &Settings, payload: Option<&RegisterPayload>) -> Self {
|
||||
let ctx = RefCell::new(context(settings));
|
||||
if let Some(payload) = payload {
|
||||
ctx.borrow_mut().insert(PAYLOAD_KEY, payload);
|
||||
}
|
||||
Self { ctx }
|
||||
}
|
||||
|
||||
pub fn render(&self) -> String {
|
||||
TEMPLATES.render(REGISTER.name, &self.ctx.borrow()).unwrap()
|
||||
}
|
||||
|
||||
pub fn page(s: &Settings) -> String {
|
||||
let p = Self::new(s, None);
|
||||
p.render()
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_web_codegen_const_routes::get(path = "PAGES.auth.register")]
|
||||
#[tracing::instrument(name = "Serve registration page", skip(ctx))]
|
||||
pub async fn get_register(ctx: AppCtx) -> impl Responder {
|
||||
let login = Register::page(&ctx.settings);
|
||||
let html = ContentType::html();
|
||||
HttpResponse::Ok().content_type(html).body(login)
|
||||
}
|
||||
|
||||
pub fn services(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(get_register);
|
||||
cfg.service(register_submit);
|
||||
}
|
||||
|
||||
#[actix_web_codegen_const_routes::post(path = "PAGES.auth.register")]
|
||||
#[tracing::instrument(name = "Process web UI registration", skip(ctx))]
|
||||
pub async fn register_submit(
|
||||
payload: web::Form<RegisterPayload>,
|
||||
ctx: AppCtx,
|
||||
) -> PageResult<impl Responder, Register> {
|
||||
ctx.register(&payload)
|
||||
.await
|
||||
.map_err(|e| PageError::new(Register::new(&ctx.settings, Some(&payload)), e))?;
|
||||
Ok(HttpResponse::Found()
|
||||
.insert_header((http::header::LOCATION, PAGES.auth.login))
|
||||
.finish())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::Register;
|
||||
use super::RegisterPayload;
|
||||
use crate::errors::*;
|
||||
use crate::pages::errors::*;
|
||||
use crate::settings::Settings;
|
||||
|
||||
#[test]
|
||||
fn register_page_renders() {
|
||||
let settings = Settings::new().unwrap();
|
||||
Register::page(&settings);
|
||||
let payload = RegisterPayload {
|
||||
username: "foo".into(),
|
||||
password: "foo".into(),
|
||||
confirm_password: "foo".into(),
|
||||
email: "foo".into(),
|
||||
};
|
||||
let page = Register::new(&settings, Some(&payload));
|
||||
page.with_error(&ReadableError::new(&ServiceError::WrongPassword));
|
||||
page.render();
|
||||
}
|
||||
}
|
144
src/pages/auth/test.rs
Normal file
144
src/pages/auth/test.rs
Normal file
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* 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 actix_web::http::header;
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::test;
|
||||
|
||||
use super::*;
|
||||
|
||||
use crate::ctx::api::v1::auth::{Login, Register};
|
||||
use crate::ctx::ArcCtx;
|
||||
use crate::errors::*;
|
||||
use crate::tests::*;
|
||||
use crate::*;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn postgrest_pages_auth_works() {
|
||||
let (_, ctx) = get_ctx().await;
|
||||
auth_works(ctx.clone()).await;
|
||||
serverside_password_validation_works(ctx.clone()).await;
|
||||
}
|
||||
|
||||
async fn auth_works(ctx: ArcCtx) {
|
||||
const NAME: &str = "testuserform";
|
||||
const EMAIL: &str = "testuserform@foo.com";
|
||||
const PASSWORD: &str = "longpassword";
|
||||
|
||||
let _ = ctx.delete_user(NAME, PASSWORD).await;
|
||||
let app = get_app!(ctx).await;
|
||||
|
||||
// 1. Register with email
|
||||
let msg = Register {
|
||||
username: NAME.into(),
|
||||
password: PASSWORD.into(),
|
||||
confirm_password: PASSWORD.into(),
|
||||
email: EMAIL.into(),
|
||||
};
|
||||
let resp = test::call_service(
|
||||
&app,
|
||||
post_request!(&msg, PAGES.auth.register, FORM).to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(resp.status(), StatusCode::FOUND);
|
||||
let headers = resp.headers();
|
||||
assert_eq!(headers.get(header::LOCATION).unwrap(), PAGES.auth.login);
|
||||
|
||||
// sign in
|
||||
let msg = Login {
|
||||
login: NAME.into(),
|
||||
password: PASSWORD.into(),
|
||||
};
|
||||
let resp = test::call_service(
|
||||
&app,
|
||||
post_request!(&msg, PAGES.auth.login, FORM).to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(resp.status(), StatusCode::FOUND);
|
||||
let headers = resp.headers();
|
||||
assert_eq!(headers.get(header::LOCATION).unwrap(), PAGES.dash.home);
|
||||
let cookies = get_cookie!(resp);
|
||||
|
||||
// redirect after signin
|
||||
let redirect = "/foo/bar/nonexistantuser";
|
||||
let url = PAGES.get_login_route(Some(redirect));
|
||||
let resp = test::call_service(&app, post_request!(&msg, &url, FORM).to_request()).await;
|
||||
assert_eq!(resp.status(), StatusCode::FOUND);
|
||||
let headers = resp.headers();
|
||||
assert_eq!(headers.get(header::LOCATION).unwrap(), &redirect);
|
||||
|
||||
// wrong password signin
|
||||
let msg = Login {
|
||||
login: NAME.into(),
|
||||
password: NAME.into(),
|
||||
};
|
||||
let resp = test::call_service(
|
||||
&app,
|
||||
post_request!(&msg, PAGES.auth.login, FORM).to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(resp.status(), ServiceError::WrongPassword.status_code());
|
||||
|
||||
// signout
|
||||
|
||||
println!("{}", PAGES.auth.logout);
|
||||
let signout_resp = test::call_service(
|
||||
&app,
|
||||
test::TestRequest::get()
|
||||
.uri(PAGES.auth.logout)
|
||||
.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.get_login_route(None)
|
||||
);
|
||||
|
||||
let _ = ctx.delete_user(NAME, PASSWORD).await;
|
||||
}
|
||||
|
||||
async fn serverside_password_validation_works(ctx: ArcCtx) {
|
||||
const NAME: &str = "pagetestuser542";
|
||||
const EMAIL: &str = "pagetestuser542@foo.com";
|
||||
const PASSWORD: &str = "longpassword2";
|
||||
|
||||
let _ = ctx.delete_user(NAME, PASSWORD).await;
|
||||
|
||||
let app = get_app!(ctx).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: EMAIL.into(),
|
||||
};
|
||||
let resp = test::call_service(
|
||||
&app,
|
||||
post_request!(®ister_msg, PAGES.auth.register, FORM).to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(
|
||||
resp.status(),
|
||||
ServiceError::PasswordsDontMatch.status_code()
|
||||
);
|
||||
}
|
76
src/pages/dash/home.rs
Normal file
76
src/pages/dash/home.rs
Normal file
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* 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::cell::RefCell;
|
||||
|
||||
use actix_identity::Identity;
|
||||
use actix_web::http::header::ContentType;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tera::Context;
|
||||
|
||||
use super::get_auth_middleware;
|
||||
use crate::db::Site;
|
||||
use crate::errors::ServiceResult;
|
||||
use crate::pages::errors::*;
|
||||
use crate::settings::Settings;
|
||||
use crate::AppCtx;
|
||||
|
||||
pub use super::*;
|
||||
|
||||
pub const DASH_HOME: TemplateFile = TemplateFile::new("dash_home", "pages/dash/index.html");
|
||||
|
||||
pub struct Home {
|
||||
ctx: RefCell<Context>,
|
||||
}
|
||||
|
||||
impl CtxError for Home {
|
||||
fn with_error(&self, e: &ReadableError) -> String {
|
||||
self.ctx.borrow_mut().insert(ERROR_KEY, e);
|
||||
self.render()
|
||||
}
|
||||
}
|
||||
|
||||
impl Home {
|
||||
pub fn new(settings: &Settings, sites: Option<&[Site]>) -> Self {
|
||||
let ctx = RefCell::new(context(settings));
|
||||
if let Some(sites) = sites {
|
||||
ctx.borrow_mut().insert(PAYLOAD_KEY, sites);
|
||||
}
|
||||
Self { ctx }
|
||||
}
|
||||
|
||||
pub fn render(&self) -> String {
|
||||
TEMPLATES
|
||||
.render(DASH_HOME.name, &self.ctx.borrow())
|
||||
.unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[actix_web_codegen_const_routes::get(path = "PAGES.dash.home", wrap = "get_auth_middleware()")]
|
||||
#[tracing::instrument(name = "Dashboard homepage", skip(ctx, id))]
|
||||
pub async fn get_home(ctx: AppCtx, id: Identity) -> PageResult<impl Responder, Home> {
|
||||
let db_sites = ctx.db.list_all_sites(&id.identity().unwrap())
|
||||
.await
|
||||
.map_err(|e| PageError::new(Home::new(&ctx.settings, None), e))?;
|
||||
let home = Home::new(&ctx.settings, Some(&db_sites)).render();
|
||||
let html = ContentType::html();
|
||||
Ok(HttpResponse::Ok().content_type(html).body(home))
|
||||
}
|
||||
|
||||
pub fn services(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(get_home);
|
||||
}
|
35
src/pages/dash/mod.rs
Normal file
35
src/pages/dash/mod.rs
Normal file
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* 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::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::get_auth_middleware;
|
||||
pub use super::{context, Footer, TemplateFile, PAGES, PAYLOAD_KEY, TEMPLATES};
|
||||
|
||||
pub mod home;
|
||||
pub mod sites;
|
||||
|
||||
pub fn register_templates(t: &mut tera::Tera) {
|
||||
home::DASH_HOME.register(t).expect(home::DASH_HOME.name);
|
||||
sites::ADD_SITE.register(t).expect(sites::ADD_SITE.name);
|
||||
}
|
||||
|
||||
pub fn services(cfg: &mut web::ServiceConfig) {
|
||||
home::services(cfg);
|
||||
sites::services(cfg);
|
||||
}
|
100
src/pages/dash/sites.rs
Normal file
100
src/pages/dash/sites.rs
Normal file
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* 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::cell::RefCell;
|
||||
|
||||
use actix_identity::Identity;
|
||||
use actix_web::http::header::ContentType;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use tera::Context;
|
||||
use url::Url;
|
||||
|
||||
use super::get_auth_middleware;
|
||||
use crate::ctx::api::v1::pages;
|
||||
use crate::errors::ServiceResult;
|
||||
use crate::pages::errors::*;
|
||||
use crate::settings::Settings;
|
||||
use crate::AppCtx;
|
||||
|
||||
pub use super::*;
|
||||
|
||||
pub const ADD_SITE: TemplateFile = TemplateFile::new("dash_add_site", "pages/dash/sites/add.html");
|
||||
|
||||
pub struct AddSite {
|
||||
ctx: RefCell<Context>,
|
||||
}
|
||||
|
||||
impl CtxError for AddSite {
|
||||
fn with_error(&self, e: &ReadableError) -> String {
|
||||
self.ctx.borrow_mut().insert(ERROR_KEY, e);
|
||||
self.render()
|
||||
}
|
||||
}
|
||||
|
||||
impl AddSite {
|
||||
pub fn new(settings: &Settings) -> Self {
|
||||
let ctx = RefCell::new(context(settings));
|
||||
Self { ctx }
|
||||
}
|
||||
|
||||
pub fn render(&self) -> String {
|
||||
TEMPLATES.render(ADD_SITE.name, &self.ctx.borrow()).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_web_codegen_const_routes::get(path = "PAGES.dash.add_site", wrap = "get_auth_middleware()")]
|
||||
#[tracing::instrument(name = "get add site", skip(ctx, id))]
|
||||
pub async fn get_add_site(ctx: AppCtx, id: Identity) -> PageResult<impl Responder, AddSite> {
|
||||
let home = AddSite::new(&ctx.settings).render();
|
||||
let html = ContentType::html();
|
||||
Ok(HttpResponse::Ok().content_type(html).body(home))
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
|
||||
/// Data required to add site
|
||||
pub struct TemplatePayloadAddSite {
|
||||
pub url: Url,
|
||||
}
|
||||
|
||||
#[actix_web_codegen_const_routes::post(
|
||||
path = "PAGES.dash.add_site",
|
||||
wrap = "get_auth_middleware()"
|
||||
)]
|
||||
#[tracing::instrument(name = "Post add site", skip(ctx, id))]
|
||||
pub async fn post_add_site(
|
||||
ctx: AppCtx,
|
||||
id: Identity,
|
||||
payload: web::Form<TemplatePayloadAddSite>,
|
||||
) -> PageResult<impl Responder, AddSite> {
|
||||
let payload = payload.into_inner();
|
||||
let owner = id.identity().unwrap();
|
||||
let location = format!("{}?url={}", PAGES.serve.catch_all, &payload.url);
|
||||
ctx.add_site(pages::AddSite {
|
||||
url: payload.url,
|
||||
owner,
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
Ok(HttpResponse::Found()
|
||||
.append_header((http::header::LOCATION, location))
|
||||
.finish())
|
||||
|
||||
}
|
||||
|
||||
pub fn services(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(get_add_site);
|
||||
cfg.service(post_add_site);
|
||||
}
|
105
src/pages/errors.rs
Normal file
105
src/pages/errors.rs
Normal file
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* 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::fmt;
|
||||
|
||||
use actix_web::{
|
||||
error::ResponseError,
|
||||
http::{header::ContentType, StatusCode},
|
||||
HttpResponse, HttpResponseBuilder,
|
||||
};
|
||||
use derive_more::Display;
|
||||
use derive_more::Error;
|
||||
use serde::*;
|
||||
|
||||
use super::TemplateFile;
|
||||
use crate::errors::ServiceError;
|
||||
|
||||
pub const ERROR_KEY: &str = "error";
|
||||
|
||||
pub const ERROR_TEMPLATE: TemplateFile = TemplateFile::new("error_comp", "components/error.html");
|
||||
pub fn register_templates(t: &mut tera::Tera) {
|
||||
ERROR_TEMPLATE.register(t).expect(ERROR_TEMPLATE.name);
|
||||
}
|
||||
|
||||
/// Render template with error context
|
||||
pub trait CtxError {
|
||||
fn with_error(&self, e: &ReadableError) -> String;
|
||||
}
|
||||
|
||||
#[derive(Serialize, Debug, Display, Clone)]
|
||||
#[display(fmt = "title: {} reason: {}", title, reason)]
|
||||
pub struct ReadableError {
|
||||
pub reason: String,
|
||||
pub title: String,
|
||||
}
|
||||
|
||||
impl ReadableError {
|
||||
pub fn new(e: &ServiceError) -> Self {
|
||||
let reason = format!("{}", e);
|
||||
let title = format!("{}", e.status_code());
|
||||
|
||||
Self { reason, title }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Error, Display)]
|
||||
#[display(fmt = "{}", readable)]
|
||||
pub struct PageError<T> {
|
||||
#[error(not(source))]
|
||||
template: T,
|
||||
readable: ReadableError,
|
||||
#[error(not(source))]
|
||||
error: ServiceError,
|
||||
}
|
||||
|
||||
impl<T> fmt::Debug for PageError<T> {
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("PageError")
|
||||
.field("readable", &self.readable)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: CtxError> PageError<T> {
|
||||
/// create new instance of [PageError] from a template and an error
|
||||
pub fn new(template: T, error: ServiceError) -> Self {
|
||||
let readable = ReadableError::new(&error);
|
||||
Self {
|
||||
error,
|
||||
template,
|
||||
readable,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl<T: CtxError> ResponseError for PageError<T> {
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
HttpResponseBuilder::new(self.status_code())
|
||||
.content_type(ContentType::html())
|
||||
.body(self.template.with_error(&self.readable))
|
||||
}
|
||||
|
||||
fn status_code(&self) -> StatusCode {
|
||||
self.error.status_code()
|
||||
}
|
||||
}
|
||||
|
||||
/// Generic result data structure
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
pub type PageResult<V, T> = std::result::Result<V, PageError<T>>;
|
209
src/pages/mod.rs
Normal file
209
src/pages/mod.rs
Normal file
|
@ -0,0 +1,209 @@
|
|||
/*
|
||||
* 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::cell::RefCell;
|
||||
|
||||
use actix_identity::Identity;
|
||||
use actix_web::http::header;
|
||||
use actix_web::*;
|
||||
use lazy_static::lazy_static;
|
||||
use rust_embed::RustEmbed;
|
||||
use serde::*;
|
||||
use tera::*;
|
||||
|
||||
use crate::pages::errors::*;
|
||||
use crate::settings::Settings;
|
||||
use crate::static_assets::ASSETS;
|
||||
use crate::AppCtx;
|
||||
use crate::{GIT_COMMIT_HASH, VERSION};
|
||||
|
||||
pub mod auth;
|
||||
pub mod dash;
|
||||
pub mod errors;
|
||||
pub mod routes;
|
||||
|
||||
pub use routes::get_auth_middleware;
|
||||
pub use routes::PAGES;
|
||||
|
||||
pub struct TemplateFile {
|
||||
pub name: &'static str,
|
||||
pub path: &'static str,
|
||||
}
|
||||
|
||||
impl TemplateFile {
|
||||
pub const fn new(name: &'static str, path: &'static str) -> Self {
|
||||
Self { name, path }
|
||||
}
|
||||
|
||||
pub fn register(&self, t: &mut Tera) -> std::result::Result<(), tera::Error> {
|
||||
t.add_raw_template(self.name, &Templates::get_template(self).expect(self.name))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[allow(dead_code)]
|
||||
pub fn register_from_file(&self, t: &mut Tera) -> std::result::Result<(), tera::Error> {
|
||||
use std::path::Path;
|
||||
t.add_template_file(Path::new("templates/").join(self.path), Some(self.name))
|
||||
}
|
||||
}
|
||||
|
||||
pub const PAYLOAD_KEY: &str = "payload";
|
||||
|
||||
pub const BASE: TemplateFile = TemplateFile::new("base", "components/base.html");
|
||||
pub const FOOTER: TemplateFile = TemplateFile::new("footer", "components/footer.html");
|
||||
pub const PUB_NAV: TemplateFile = TemplateFile::new("pub_nav", "components/nav/pub.html");
|
||||
pub const AUTH_NAV: TemplateFile = TemplateFile::new("auth_nav", "components/nav/auth.html");
|
||||
|
||||
lazy_static! {
|
||||
pub static ref TEMPLATES: Tera = {
|
||||
let mut tera = Tera::default();
|
||||
for t in [BASE, FOOTER, PUB_NAV, AUTH_NAV].iter() {
|
||||
t.register(&mut tera).unwrap();
|
||||
}
|
||||
errors::register_templates(&mut tera);
|
||||
tera.autoescape_on(vec![".html", ".sql"]);
|
||||
auth::register_templates(&mut tera);
|
||||
dash::register_templates(&mut tera);
|
||||
tera
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "templates/"]
|
||||
pub struct Templates;
|
||||
|
||||
impl Templates {
|
||||
pub fn get_template(t: &TemplateFile) -> Option<String> {
|
||||
match Self::get(t.path) {
|
||||
Some(file) => Some(String::from_utf8_lossy(&file.data).into_owned()),
|
||||
None => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn context(s: &Settings) -> Context {
|
||||
let mut ctx = Context::new();
|
||||
let footer = Footer::new(s);
|
||||
ctx.insert("footer", &footer);
|
||||
ctx.insert("page", &PAGES);
|
||||
ctx.insert("assets", &*ASSETS);
|
||||
ctx
|
||||
}
|
||||
|
||||
pub fn auth_ctx(_username: Option<&str>, s: &Settings) -> Context {
|
||||
let mut ctx = Context::new();
|
||||
let footer = Footer::new(s);
|
||||
ctx.insert("footer", &footer);
|
||||
ctx.insert("page", &PAGES);
|
||||
ctx.insert("assets", &*ASSETS);
|
||||
// ctx.insert("loggedin_user", &profile_link);
|
||||
ctx
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Footer<'a> {
|
||||
version: &'a str,
|
||||
support_email: &'a str,
|
||||
source_code: &'a str,
|
||||
git_hash: &'a str,
|
||||
settings: &'a Settings,
|
||||
}
|
||||
|
||||
impl<'a> Footer<'a> {
|
||||
pub fn new(settings: &'a Settings) -> Self {
|
||||
Self {
|
||||
version: VERSION,
|
||||
source_code: &settings.source_code,
|
||||
support_email: &settings.support_email,
|
||||
git_hash: &GIT_COMMIT_HASH[..8],
|
||||
settings,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_web_codegen_const_routes::get(path = "PAGES.home")]
|
||||
#[tracing::instrument(name = "Serve index page", skip(ctx, id))]
|
||||
pub async fn home(ctx: AppCtx, id: Identity) -> HttpResponse {
|
||||
let location = if id.identity().is_some() {
|
||||
PAGES.auth.login
|
||||
} else {
|
||||
PAGES.dash.home
|
||||
};
|
||||
HttpResponse::Found()
|
||||
.append_header((header::LOCATION, location))
|
||||
.finish()
|
||||
}
|
||||
|
||||
pub fn services(cfg: &mut web::ServiceConfig) {
|
||||
dash::services(cfg);
|
||||
auth::services(cfg);
|
||||
cfg.service(home);
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
#[test]
|
||||
fn templates_work_basic() {
|
||||
use super::*;
|
||||
use tera::Tera;
|
||||
|
||||
let mut tera = Tera::default();
|
||||
let mut tera2 = Tera::default();
|
||||
for t in [
|
||||
BASE,
|
||||
FOOTER,
|
||||
PUB_NAV,
|
||||
AUTH_NAV,
|
||||
auth::AUTH_BASE,
|
||||
auth::login::LOGIN,
|
||||
auth::register::REGISTER,
|
||||
errors::ERROR_TEMPLATE,
|
||||
super::dash::home::DASH_HOME,
|
||||
]
|
||||
.iter()
|
||||
{
|
||||
t.register_from_file(&mut tera2).unwrap();
|
||||
t.register(&mut tera).unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod http_page_tests {
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::test;
|
||||
|
||||
use crate::ctx::ArcCtx;
|
||||
use crate::*;
|
||||
|
||||
use super::PAGES;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn postgrest_templates_work() {
|
||||
let (_, ctx) = crate::tests::get_ctx().await;
|
||||
templates_work(ctx).await;
|
||||
}
|
||||
|
||||
async fn templates_work(ctx: ArcCtx) {
|
||||
let app = get_app!(ctx).await;
|
||||
|
||||
for file in [PAGES.auth.login, PAGES.auth.register, PAGES.home].iter() {
|
||||
let resp = get_request!(&app, file);
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
}
|
||||
}
|
105
src/pages/routes.rs
Normal file
105
src/pages/routes.rs
Normal file
|
@ -0,0 +1,105 @@
|
|||
/*
|
||||
* 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::{Authentication, GetLoginRoute};
|
||||
use serde::*;
|
||||
|
||||
use crate::serve::routes::Serve;
|
||||
|
||||
/// constant [Pages](Pages) instance
|
||||
pub const PAGES: Pages = Pages::new();
|
||||
|
||||
#[derive(Serialize)]
|
||||
/// Top-level routes data structure for V1 AP1
|
||||
pub struct Pages {
|
||||
/// Authentication routes
|
||||
pub auth: Auth,
|
||||
/// home page
|
||||
pub home: &'static str,
|
||||
pub dash: Dash,
|
||||
pub serve: Serve,
|
||||
}
|
||||
|
||||
impl Pages {
|
||||
/// create new instance of Routes
|
||||
const fn new() -> Pages {
|
||||
let auth = Auth::new();
|
||||
let dash = Dash::new();
|
||||
let serve = Serve::new();
|
||||
let home = "/";
|
||||
Pages { auth, home, dash, serve }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
/// Authentication routes
|
||||
pub struct Auth {
|
||||
/// logout route
|
||||
pub logout: &'static str,
|
||||
/// login route
|
||||
pub login: &'static str,
|
||||
/// registration route
|
||||
pub register: &'static str,
|
||||
}
|
||||
|
||||
impl Auth {
|
||||
/// create new instance of Authentication route
|
||||
pub const fn new() -> Auth {
|
||||
let login = "/login";
|
||||
let logout = "/logout";
|
||||
let register = "/join";
|
||||
Auth {
|
||||
logout,
|
||||
login,
|
||||
register,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
/// Dashboard routes
|
||||
pub struct Dash {
|
||||
/// home route
|
||||
pub home: &'static str,
|
||||
pub add_site: &'static str,
|
||||
}
|
||||
|
||||
impl Dash {
|
||||
/// create new instance of Dash route
|
||||
pub const fn new() -> Dash {
|
||||
let home = "/dash";
|
||||
let add_site = "/dash/sites/add";
|
||||
Dash { home, add_site }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_auth_middleware() -> Authentication<Pages> {
|
||||
Authentication::with_identity(PAGES)
|
||||
}
|
||||
|
||||
impl GetLoginRoute for Pages {
|
||||
fn get_login_route(&self, src: Option<&str>) -> String {
|
||||
if let Some(redirect_to) = src {
|
||||
format!(
|
||||
"{}?redirect_to={}",
|
||||
self.auth.login,
|
||||
urlencoding::encode(redirect_to)
|
||||
)
|
||||
} else {
|
||||
self.auth.login.to_string()
|
||||
}
|
||||
}
|
||||
}
|
76
src/serve.rs
Normal file
76
src/serve.rs
Normal file
|
@ -0,0 +1,76 @@
|
|||
use std::convert::identity;
|
||||
|
||||
use actix_identity::Identity;
|
||||
/*
|
||||
* 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::header::ContentType, web, HttpRequest, HttpResponse, Responder};
|
||||
use serde::{Serialize, Deserialize};
|
||||
use tokio::fs;
|
||||
|
||||
use crate::errors::*;
|
||||
use crate::pages;
|
||||
use crate::AppCtx;
|
||||
|
||||
pub mod routes {
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct Serve {
|
||||
pub catch_all: &'static str,
|
||||
}
|
||||
|
||||
impl Serve {
|
||||
pub const fn new() -> Self {
|
||||
Self {
|
||||
catch_all: "/archive",
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct Q {
|
||||
url: url::Url,
|
||||
}
|
||||
|
||||
#[actix_web_codegen_const_routes::get(path = "crate::pages::PAGES.serve.catch_all")]
|
||||
#[tracing::instrument(name = "Serve webpages", skip(ctx, id, q))]
|
||||
async fn serve_webpage(q: web::Query<(Q)>, ctx: AppCtx, id: Identity) -> ServiceResult<impl Responder> {
|
||||
let url = q.into_inner().url;
|
||||
|
||||
|
||||
let id = id.identity().unwrap();
|
||||
if ctx.db.url_exists(url.as_str()).await? {
|
||||
let path = crate::utils::get_website_path(&ctx.settings, &id, &url);
|
||||
let content = fs::read(&path).await?;
|
||||
let mime = if let Some(mime) = mime_guess::from_path(&path).first_raw() {
|
||||
mime
|
||||
} else {
|
||||
"text/html; charset=utf-8"
|
||||
};
|
||||
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type(mime)
|
||||
.body(content))
|
||||
} else {
|
||||
Err(ServiceError::WebsiteNotFound)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn services(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(serve_webpage);
|
||||
}
|
170
src/settings.rs
Normal file
170
src/settings.rs
Normal file
|
@ -0,0 +1,170 @@
|
|||
/*
|
||||
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
use std::env;
|
||||
use std::path::Path;
|
||||
|
||||
use config::{Config, ConfigError, Environment, File};
|
||||
use derive_more::Display;
|
||||
#[cfg(not(test))]
|
||||
use tracing::warn;
|
||||
|
||||
#[cfg(test)]
|
||||
use std::println as warn;
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use url::Url;
|
||||
|
||||
use crate::errors::*;
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Server {
|
||||
pub port: u32,
|
||||
pub ip: String,
|
||||
pub workers: Option<usize>,
|
||||
pub cookie_secret: String,
|
||||
pub domain: String,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Display, Eq, PartialEq, Clone, Debug)]
|
||||
pub struct Pages {
|
||||
pub base_path: String,
|
||||
}
|
||||
|
||||
impl Server {
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
pub fn get_ip(&self) -> String {
|
||||
format!("{}:{}", self.ip, self.port)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Display, Eq, PartialEq, Clone, Debug)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
pub enum DBType {
|
||||
#[display(fmt = "sqlite")]
|
||||
Sqlite,
|
||||
// #[display(fmt = "maria")]
|
||||
// Maria,
|
||||
}
|
||||
|
||||
impl DBType {
|
||||
fn from_url(url: &str) -> Result<Self, ConfigError> {
|
||||
Ok(Self::Sqlite)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Database {
|
||||
pub url: String,
|
||||
pub pool: u32,
|
||||
pub database_type: DBType,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Settings {
|
||||
pub allow_registration: bool,
|
||||
pub support_email: String,
|
||||
pub debug: bool,
|
||||
pub server: Server,
|
||||
pub source_code: String,
|
||||
pub pages: Pages,
|
||||
pub database: Database,
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
impl Settings {
|
||||
pub fn new() -> ServiceResult<Self> {
|
||||
let mut s = Config::builder();
|
||||
|
||||
const CURRENT_DIR: &str = "./config/default.toml";
|
||||
const ETC: &str = "/etc/pativu/config.toml";
|
||||
|
||||
let mut read_file = false;
|
||||
|
||||
if Path::new(ETC).exists() {
|
||||
s = s.add_source(File::with_name(ETC));
|
||||
read_file = true;
|
||||
}
|
||||
if Path::new(CURRENT_DIR).exists() {
|
||||
// merging default config from file
|
||||
s = s.add_source(File::with_name(CURRENT_DIR));
|
||||
read_file = true;
|
||||
}
|
||||
|
||||
if let Ok(path) = env::var("PATIVU_CONFIG") {
|
||||
s = s.add_source(File::with_name(&path));
|
||||
read_file = true;
|
||||
}
|
||||
|
||||
if !read_file {
|
||||
warn!("configuration file not found");
|
||||
}
|
||||
|
||||
s = s.add_source(Environment::with_prefix("PATIVU").separator("__"));
|
||||
|
||||
match env::var("PORT") {
|
||||
Ok(val) => {
|
||||
s = s.set_override("server.port", val).unwrap();
|
||||
}
|
||||
Err(e) => warn!("couldn't interpret PORT: {}", e),
|
||||
}
|
||||
|
||||
let intermediate_config = s.build_cloned().unwrap();
|
||||
|
||||
s = s
|
||||
.set_override(
|
||||
"database.url",
|
||||
format!(
|
||||
r"sqlite://{}:{}@{}:{}/{}",
|
||||
intermediate_config
|
||||
.get::<String>("database.username")
|
||||
.expect("Couldn't access database username"),
|
||||
intermediate_config
|
||||
.get::<String>("database.password")
|
||||
.expect("Couldn't access database password"),
|
||||
intermediate_config
|
||||
.get::<String>("database.hostname")
|
||||
.expect("Couldn't access database hostname"),
|
||||
intermediate_config
|
||||
.get::<String>("database.port")
|
||||
.expect("Couldn't access database port"),
|
||||
intermediate_config
|
||||
.get::<String>("database.name")
|
||||
.expect("Couldn't access database name")
|
||||
),
|
||||
)
|
||||
.expect("Couldn't set database url");
|
||||
|
||||
if let Ok(val) = env::var("DATABASE_URL") {
|
||||
let database_type = DBType::from_url(&val).unwrap();
|
||||
s = s.set_override("database.url", val).unwrap();
|
||||
s = s
|
||||
.set_override("database.database_type", database_type.to_string())
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
let settings = s.build()?.try_deserialize::<Settings>()?;
|
||||
settings.check_url();
|
||||
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
#[cfg(not(tarpaulin_include))]
|
||||
fn check_url(&self) {
|
||||
Url::parse(&self.source_code).expect("Please enter a URL for source_code in settings");
|
||||
}
|
||||
}
|
46
src/static_assets/filemap.rs
Normal file
46
src/static_assets/filemap.rs
Normal file
|
@ -0,0 +1,46 @@
|
|||
/*
|
||||
* 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 cache_buster::Files;
|
||||
|
||||
pub struct FileMap {
|
||||
pub files: Files,
|
||||
}
|
||||
|
||||
impl FileMap {
|
||||
#[allow(clippy::new_without_default)]
|
||||
pub fn new() -> Self {
|
||||
let map = include_str!("../cache_buster_data.json");
|
||||
let files = Files::new(map);
|
||||
Self { files }
|
||||
}
|
||||
pub fn get(&self, path: impl AsRef<str>) -> Option<&str> {
|
||||
let file_path = self.files.get_full_path(path);
|
||||
file_path.map(|file_path| &file_path[1..])
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
#[test]
|
||||
fn filemap_works() {
|
||||
let files = super::FileMap::new();
|
||||
let css = files.get("./static/cache/css/main.css").unwrap();
|
||||
println!("{}", css);
|
||||
assert!(css.contains("/assets/css/main"));
|
||||
}
|
||||
}
|
56
src/static_assets/mod.rs
Normal file
56
src/static_assets/mod.rs
Normal file
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* 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::*;
|
||||
|
||||
pub mod filemap;
|
||||
pub mod static_files;
|
||||
|
||||
pub use filemap::FileMap;
|
||||
pub use routes::{Assets, ASSETS};
|
||||
|
||||
pub fn services(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(static_files::static_files);
|
||||
}
|
||||
|
||||
pub mod routes {
|
||||
use lazy_static::lazy_static;
|
||||
use serde::*;
|
||||
|
||||
use super::*;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref ASSETS: Assets = Assets::new();
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
/// Top-level routes data structure for V1 AP1
|
||||
pub struct Assets {
|
||||
/// Authentication routes
|
||||
pub css: &'static str,
|
||||
pub mobile_css: &'static str,
|
||||
}
|
||||
|
||||
impl Assets {
|
||||
/// create new instance of Routes
|
||||
pub fn new() -> Assets {
|
||||
Assets {
|
||||
css: &static_files::assets::CSS,
|
||||
mobile_css: &static_files::assets::CSS,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
94
src/static_assets/static_files.rs
Normal file
94
src/static_assets/static_files.rs
Normal file
|
@ -0,0 +1,94 @@
|
|||
/*
|
||||
* 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_web::body::BoxBody;
|
||||
use actix_web::{get, http::header, web, HttpResponse, Responder};
|
||||
use mime_guess::from_path;
|
||||
use rust_embed::RustEmbed;
|
||||
|
||||
use crate::CACHE_AGE;
|
||||
|
||||
pub mod assets {
|
||||
use crate::FILES;
|
||||
use lazy_static::lazy_static;
|
||||
|
||||
lazy_static! {
|
||||
pub static ref CSS: &'static str = FILES.get("./static/cache/css/main.css").unwrap();
|
||||
pub static ref MOBILE_CSS: &'static str =
|
||||
FILES.get("./static/cache/css/mobile.css").unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(RustEmbed)]
|
||||
#[folder = "assets/"]
|
||||
struct Asset;
|
||||
|
||||
fn handle_assets(path: &str) -> HttpResponse {
|
||||
match Asset::get(path) {
|
||||
Some(content) => {
|
||||
let body: BoxBody = match content.data {
|
||||
Cow::Borrowed(bytes) => BoxBody::new(bytes),
|
||||
Cow::Owned(bytes) => BoxBody::new(bytes),
|
||||
};
|
||||
|
||||
HttpResponse::Ok()
|
||||
.insert_header(header::CacheControl(vec![
|
||||
header::CacheDirective::Public,
|
||||
header::CacheDirective::Extension("immutable".into(), None),
|
||||
header::CacheDirective::MaxAge(CACHE_AGE),
|
||||
]))
|
||||
.content_type(from_path(path).first_or_octet_stream().as_ref())
|
||||
.body(body)
|
||||
}
|
||||
None => HttpResponse::NotFound().body("404 Not Found"),
|
||||
}
|
||||
}
|
||||
|
||||
#[get("/assets/{_:.*}")]
|
||||
pub async fn static_files(path: web::Path<String>) -> impl Responder {
|
||||
handle_assets(&path)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::test;
|
||||
|
||||
use crate::ctx::ArcCtx;
|
||||
use crate::tests::*;
|
||||
use crate::*;
|
||||
|
||||
use super::assets::CSS;
|
||||
use super::assets::MOBILE_CSS;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn postgrest_static_files_works() {
|
||||
let (_, ctx) = get_ctx().await;
|
||||
static_assets_work(ctx).await;
|
||||
}
|
||||
|
||||
async fn static_assets_work(ctx: ArcCtx) {
|
||||
let app = get_app!(ctx).await;
|
||||
|
||||
for file in [*CSS, *MOBILE_CSS].iter() {
|
||||
println!("testing file {file}");
|
||||
let resp = get_request!(&app, file);
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
}
|
||||
}
|
274
src/tests.rs
Normal file
274
src/tests.rs
Normal file
|
@ -0,0 +1,274 @@
|
|||
/*
|
||||
* 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::sync::Arc;
|
||||
|
||||
use actix_web::{
|
||||
body::{BoxBody, EitherBody},
|
||||
dev::ServiceResponse,
|
||||
error::ResponseError,
|
||||
http::StatusCode,
|
||||
};
|
||||
use mktemp::Temp;
|
||||
use serde::Serialize;
|
||||
|
||||
use crate::ctx::api::v1::auth::{Login, Register};
|
||||
use crate::ctx::api::v1::pages::AddSite;
|
||||
use crate::ctx::Ctx;
|
||||
use crate::errors::*;
|
||||
use crate::page::Page;
|
||||
use crate::settings::Settings;
|
||||
use crate::*;
|
||||
|
||||
pub async fn get_ctx() -> (Temp, Arc<Ctx>) {
|
||||
// mktemp::Temp is returned because the temp directory created
|
||||
// is removed once the variable goes out of scope
|
||||
let settings = Settings::new().unwrap();
|
||||
|
||||
let tmp_dir = Temp::new_dir().unwrap();
|
||||
println!("[log] Test temp directory: {}", tmp_dir.to_str().unwrap());
|
||||
let page_base_path = tmp_dir.as_path().join("base_path");
|
||||
println!("[log] Initialzing settings again with test config");
|
||||
|
||||
(tmp_dir, Ctx::new(settings).await)
|
||||
}
|
||||
|
||||
#[allow(dead_code, clippy::upper_case_acronyms)]
|
||||
pub struct FORM;
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! post_request {
|
||||
($uri:expr) => {
|
||||
actix_web::test::TestRequest::post().uri($uri)
|
||||
};
|
||||
|
||||
($serializable:expr, $uri:expr) => {
|
||||
actix_web::test::TestRequest::post()
|
||||
.uri($uri)
|
||||
.insert_header((actix_web::http::header::CONTENT_TYPE, "application/json"))
|
||||
.set_payload(serde_json::to_string($serializable).unwrap())
|
||||
};
|
||||
|
||||
($serializable:expr, $uri:expr, FORM) => {
|
||||
actix_web::test::TestRequest::post()
|
||||
.uri($uri)
|
||||
.set_form($serializable)
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! get_request {
|
||||
($app:expr,$route:expr ) => {
|
||||
test::call_service(&$app, test::TestRequest::get().uri($route).to_request()).await
|
||||
};
|
||||
|
||||
($app:expr, $route:expr, $cookies:expr) => {
|
||||
test::call_service(
|
||||
&$app,
|
||||
test::TestRequest::get()
|
||||
.uri($route)
|
||||
.cookie($cookies)
|
||||
.to_request(),
|
||||
)
|
||||
.await
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! delete_request {
|
||||
($app:expr,$route:expr ) => {
|
||||
test::call_service(&$app, test::TestRequest::delete().uri($route).to_request()).await
|
||||
};
|
||||
|
||||
($app:expr, $route:expr, $cookies:expr) => {
|
||||
test::call_service(
|
||||
&$app,
|
||||
test::TestRequest::delete()
|
||||
.uri($route)
|
||||
.cookie($cookies)
|
||||
.to_request(),
|
||||
)
|
||||
.await
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! get_app {
|
||||
($ctx:expr) => {
|
||||
actix_web::test::init_service(
|
||||
actix_web::App::new()
|
||||
.app_data($crate::get_json_err())
|
||||
.wrap($crate::get_identity_service(&$ctx.settings))
|
||||
.wrap(actix_web::middleware::NormalizePath::new(
|
||||
actix_web::middleware::TrailingSlash::Trim,
|
||||
))
|
||||
.configure($crate::services)
|
||||
.app_data($crate::WebData::new($ctx.clone())),
|
||||
)
|
||||
};
|
||||
}
|
||||
|
||||
/// Utility function to check for status of a test response, attempt response payload serialization
|
||||
/// and print payload if response status doesn't match expected status
|
||||
#[macro_export]
|
||||
macro_rules! check_status {
|
||||
($resp:expr, $expected:expr) => {
|
||||
let status = $resp.status();
|
||||
if status != $expected {
|
||||
eprintln!(
|
||||
"[error] Expected status code: {} received: {status}",
|
||||
$expected
|
||||
);
|
||||
let response: serde_json::Value = actix_web::test::read_body_json($resp).await;
|
||||
eprintln!("[error] Body:\n{:#?}", response);
|
||||
assert_eq!(status, $expected);
|
||||
panic!()
|
||||
}
|
||||
{
|
||||
assert_eq!(status, $expected);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! get_cookie {
|
||||
($resp:expr) => {
|
||||
$resp.response().cookies().next().unwrap().to_owned()
|
||||
};
|
||||
}
|
||||
|
||||
impl Ctx {
|
||||
/// register and signin utility
|
||||
pub async fn register_and_signin(
|
||||
&self,
|
||||
name: &str,
|
||||
email: &str,
|
||||
password: &str,
|
||||
) -> (Login, ServiceResponse<EitherBody<BoxBody>>) {
|
||||
self.register_test(name, email, password).await;
|
||||
self.signin_test(name, password).await
|
||||
}
|
||||
|
||||
pub fn to_arc(&self) -> Arc<Self> {
|
||||
Arc::new(self.clone())
|
||||
}
|
||||
|
||||
/// register utility
|
||||
pub async fn register_test(&self, name: &str, email: &str, password: &str) {
|
||||
let app = get_app!(self.to_arc()).await;
|
||||
|
||||
// 1. Register
|
||||
let msg = Register {
|
||||
username: name.into(),
|
||||
password: password.into(),
|
||||
confirm_password: password.into(),
|
||||
email: email.into(),
|
||||
};
|
||||
println!("{:?}", msg);
|
||||
let resp = actix_web::test::call_service(
|
||||
&app,
|
||||
post_request!(&msg, crate::V1_API_ROUTES.auth.register).to_request(),
|
||||
)
|
||||
.await;
|
||||
if resp.status() != StatusCode::OK {
|
||||
let resp_err: ErrorToResponse = actix_web::test::read_body_json(resp).await;
|
||||
panic!("{}", resp_err.error);
|
||||
}
|
||||
}
|
||||
|
||||
/// signin util
|
||||
pub async fn signin_test(
|
||||
&self,
|
||||
|
||||
name: &str,
|
||||
password: &str,
|
||||
) -> (Login, ServiceResponse<EitherBody<BoxBody>>) {
|
||||
let app = get_app!(self.to_arc()).await;
|
||||
|
||||
// 2. signin
|
||||
let creds = Login {
|
||||
login: name.into(),
|
||||
password: password.into(),
|
||||
};
|
||||
let signin_resp = actix_web::test::call_service(
|
||||
&app,
|
||||
post_request!(&creds, V1_API_ROUTES.auth.login).to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(signin_resp.status(), StatusCode::OK);
|
||||
(creds, signin_resp)
|
||||
}
|
||||
|
||||
/// pub duplicate test
|
||||
pub async fn bad_post_req_test<T: Serialize>(
|
||||
&self,
|
||||
|
||||
name: &str,
|
||||
password: &str,
|
||||
url: &str,
|
||||
payload: &T,
|
||||
err: ServiceError,
|
||||
) {
|
||||
let (_, signin_resp) = self.signin_test(name, password).await;
|
||||
let cookies = get_cookie!(signin_resp);
|
||||
let app = get_app!(self.to_arc()).await;
|
||||
|
||||
let resp = actix_web::test::call_service(
|
||||
&app,
|
||||
post_request!(&payload, url)
|
||||
.cookie(cookies.clone())
|
||||
.to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(resp.status(), err.status_code());
|
||||
let resp_err: ErrorToResponse = actix_web::test::read_body_json(resp).await;
|
||||
//println!("{}", txt.error);
|
||||
assert_eq!(resp_err.error, format!("{}", err));
|
||||
}
|
||||
|
||||
/// bad post req test without payload
|
||||
pub async fn bad_post_req_test_witout_payload(
|
||||
&self,
|
||||
name: &str,
|
||||
password: &str,
|
||||
url: &str,
|
||||
err: ServiceError,
|
||||
) {
|
||||
let (_, signin_resp) = self.signin_test(name, password).await;
|
||||
let app = get_app!(self.to_arc()).await;
|
||||
let cookies = get_cookie!(signin_resp);
|
||||
|
||||
let resp = actix_web::test::call_service(
|
||||
&app,
|
||||
post_request!(url).cookie(cookies.clone()).to_request(),
|
||||
)
|
||||
.await;
|
||||
assert_eq!(resp.status(), err.status_code());
|
||||
let resp_err: ErrorToResponse = actix_web::test::read_body_json(resp).await;
|
||||
//println!("{}", resp_err.error);
|
||||
assert_eq!(resp_err.error, format!("{}", err));
|
||||
}
|
||||
|
||||
pub async fn add_test_site(&self, owner: String) -> Page {
|
||||
unimplemented!()
|
||||
// let msg = AddSite {
|
||||
// repo_url: REPO_URL.into(),
|
||||
// branch: BRANCH.into(),
|
||||
// owner,
|
||||
// };
|
||||
// self.add_site(msg).await.unwrap()
|
||||
}
|
||||
}
|
42
src/utils.rs
Normal file
42
src/utils.rs
Normal file
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* 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 crate::Settings;
|
||||
use std::path::{Path, PathBuf};
|
||||
|
||||
use url::Url;
|
||||
|
||||
/// Get random string of specific length
|
||||
pub(crate) fn get_random(len: usize) -> String {
|
||||
use rand::{distributions::Alphanumeric, rngs::ThreadRng, thread_rng, Rng};
|
||||
use std::iter;
|
||||
|
||||
let mut rng: ThreadRng = thread_rng();
|
||||
|
||||
iter::repeat(())
|
||||
.map(|()| rng.sample(Alphanumeric))
|
||||
.map(char::from)
|
||||
.take(len)
|
||||
.collect::<String>()
|
||||
}
|
||||
|
||||
pub(crate) fn get_website_path(s: &Settings, username: &str, url: &Url) -> PathBuf {
|
||||
let path = url.as_str().replace('/', "-");
|
||||
|
||||
Path::new(&s.pages.base_path)
|
||||
.join(username)
|
||||
.join(path)
|
||||
}
|
423
static/cache/css/main.css
vendored
Normal file
423
static/cache/css/main.css
vendored
Normal file
|
@ -0,0 +1,423 @@
|
|||
* {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover, button:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a,
|
||||
a:visited {
|
||||
color: rgb(0, 86, 179);
|
||||
}
|
||||
|
||||
.base {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.main__content-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
justify-content: space-between;
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
p,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
li,
|
||||
ol,
|
||||
ul {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
main {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 0.3em solid rgba(55, 55, 55, 0.4);
|
||||
margin-bottom: 16px;
|
||||
padding: 0 1em;
|
||||
color: #707070;
|
||||
}
|
||||
blockquote p,
|
||||
blockquote h1,
|
||||
blockquote h2,
|
||||
blockquote h3,
|
||||
blockquote h4,
|
||||
blockquote li,
|
||||
blockquote ol,
|
||||
blockquote ul {
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.auth__body {
|
||||
display: flex;
|
||||
height: 100vh;
|
||||
min-height: 500px;
|
||||
max-height: 800px;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.index-banner__container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.index-banner {
|
||||
margin: auto;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.index-banner__logo-container {
|
||||
margin: auto;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
.index-banner__title {
|
||||
margin: auto;
|
||||
font-style: none;
|
||||
}
|
||||
|
||||
.index-banner__tagline {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.index-banner__title-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.index-banner__logo {
|
||||
width: 120px;
|
||||
margin: auto;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.index-banner__main-action-btn {
|
||||
display: block;
|
||||
display: block;
|
||||
font-weight: 400;
|
||||
padding: 15px;
|
||||
border: none;
|
||||
margin: 20px 0;
|
||||
background-color: green;
|
||||
}
|
||||
|
||||
.index-banner__main-action-link {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.index-banner__features-list {
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.index-banner__features {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.home__features {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.home__features-title {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.index__group-content .page__container {
|
||||
width: 80%;
|
||||
height: 100vh;
|
||||
min-height: 500px;
|
||||
max-height: 800px;
|
||||
height: 90vh !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.action-call__container {
|
||||
background: #1f5818;
|
||||
width: 100%;
|
||||
padding: 60px 0;
|
||||
}
|
||||
|
||||
.action-call__margin-container {
|
||||
display: flex;
|
||||
width: 80%;
|
||||
margin: auto;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.action-call__prompt {
|
||||
color: white;
|
||||
font-weight: 400;
|
||||
font-size: 1.7rem;
|
||||
}
|
||||
|
||||
.action-call__button {
|
||||
display: block;
|
||||
display: block;
|
||||
font-weight: 400;
|
||||
padding: 15px;
|
||||
border: none;
|
||||
margin: 20px 0;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.action-call__button:hover {
|
||||
background-color: lightgray;
|
||||
}
|
||||
|
||||
.action-call_link {
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
.action-call_link:hover {
|
||||
text-decoration: none !important;
|
||||
}
|
||||
|
||||
.auth-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 80%;
|
||||
margin: auto;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.auth-form__input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 10px 0;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.auth-form__submit {
|
||||
width: 100%;
|
||||
display: block;
|
||||
margin: 10px 0;
|
||||
background-color: green;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 5px 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.auth-form__submit:hover {
|
||||
background-color: green;
|
||||
}
|
||||
|
||||
footer {
|
||||
display: block;
|
||||
color: #333;
|
||||
font-size: 0.7rem;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.footer__container {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
justify-content: space-between;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.footer__column {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
margin: auto 50px;
|
||||
align-items: center;
|
||||
flex: 2.5;
|
||||
}
|
||||
|
||||
.footer__column--center {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
margin: auto 50px;
|
||||
align-items: center;
|
||||
flex: 2.5;
|
||||
margin: auto;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.footer__column:last-child {
|
||||
justify-content: flex-end;
|
||||
}
|
||||
.footer__column:last-child a {
|
||||
margin: 10px;
|
||||
}
|
||||
|
||||
.footer__link-container {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.footer__link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.license__link {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.license__link:hover {
|
||||
color: rgb(0, 86, 179);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.footer__column-divider,
|
||||
.footer__column-divider--mobile-visible,
|
||||
.footer__column-divider--mobile-only {
|
||||
font-weight: 500;
|
||||
opacity: 0.7;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.footer__column-divider--mobile-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.footer__icon {
|
||||
margin: auto 5px;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
header {
|
||||
z-index: 5;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.nav__container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding-top: 5px;
|
||||
border-bottom: 1px solid rgb(211, 211, 211);
|
||||
}
|
||||
|
||||
.nav__home-btn {
|
||||
font-weight: bold;
|
||||
margin: auto;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.nav__hamburger-menu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav__spacer--small {
|
||||
width: 100px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.nav__spacer {
|
||||
flex: 4;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.nav__logo-container {
|
||||
display: inline-flex;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nav__logo-container:hover {
|
||||
color: rgb(0, 86, 179);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.nav__toggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav__logo {
|
||||
display: inline-flex;
|
||||
margin: auto;
|
||||
padding: 5px;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.nav__link-group {
|
||||
flex: 1.5;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
align-self: center;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nav__link-group--small {
|
||||
flex: 1.5;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
align-self: center;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
flex: 0.5;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.nav__link-container {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
height: 100%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.nav__link-container--action {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
height: 100%;
|
||||
margin: auto;
|
||||
background-color: green;
|
||||
padding: 15px;
|
||||
}
|
||||
.nav__link-container--action .nav__link {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.nav__link {
|
||||
text-decoration: none;
|
||||
color: black !important;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.nav__link:hover {
|
||||
color: rgb(0, 86, 179);
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=main.css.map */
|
1
static/cache/css/main.css.map
vendored
Normal file
1
static/cache/css/main.css.map
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
{"version":3,"sourceRoot":"","sources":["../../../templates/defaults.scss","../../../templates/pages/auth/sass/main.scss","../../../templates/components/sass/_fullscreen.scss","../../../templates/pages/auth/sass/form/main.scss","../../../templates/components/sass/footer/main.scss","../../../templates/components/sass/_link.scss","../../../templates/components/nav/sass/main.scss"],"names":[],"mappings":"AAAA;EACC;EACA;EAGA;EACA;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;AAAA;EAEC;;;AAGD;EACC;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;;;AAGD;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAQC;;;AAGD;EACC;;;AAGD;EACC;EACA;EAEA;EACA;;AAEA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;EAQC;;;ACjEF;EACC;ECFA;EACA;EACA;EDEA;EACA;;;AAID;EACC;EACA;EAIA;EACA;;;AAGD;EACC;EACA;EAEA;;;AASD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;;;AAID;EACC;;;AAKD;EACC;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAOD;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;;;AAKA;EACC,OAHmB;EClGpB;EACA;EACA;EDqGC;EACA;EACA;EACA;;;AAIF;EACC;EACA;EACA;;;AAGD;EACC;EACA,OApBoB;EAqBpB;EACA;EACA;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AEtJD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;ACzBD;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;EACA;EACA;EACA;EACA;EACA;EACA;;;AAWD;EAPC;EACA;EACA;EACA;EACA;;;AAOD;EAXC;EACA;EACA;EACA;EACA;EASA;EACA;EACA;EACA;;;AAGD;EACC;;AACA;EACC;;;AAIF;EACC;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EC1DC;EACA;;;AD6DD;AAAA;AAAA;EAGC;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AE3ED;EACC;EACA;EACA;EACA;;;AAGD;EACC;EACA;EAEA;EACA;EACA;EACA;;;AAGD;EACC;EAEA;EACA;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;;;AAGD;ED5CC;EACA;;;AC+CD;EACC;;;AAGD;EACC;EACA;EACA;EACA;;;AAcD;EAVC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;;;AAQD;EAfC;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EAUA;EACA;;;AAUD;EANC;EACA;EACA;EACA;;;AAOD;EAVC;EACA;EACA;EACA;EASA;EACA;;AACA;EACC;;;AAIF;EACC;EACA;EACA;EACA;;;AAGD;ED5GC;EACA","file":"main.css"}
|
209
static/cache/css/mobile.css
vendored
Normal file
209
static/cache/css/mobile.css
vendored
Normal file
|
@ -0,0 +1,209 @@
|
|||
footer {
|
||||
font-size: 0.44rem;
|
||||
}
|
||||
|
||||
.footer__container {
|
||||
display: grid;
|
||||
grid-template-rows: repeat(3, 100%);
|
||||
align-items: center;
|
||||
margin: auto;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.footer__link {
|
||||
font-size: 0.5rem;
|
||||
}
|
||||
|
||||
.license__conatiner,
|
||||
.license__link {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.footer__column:first-child {
|
||||
grid-row-start: 3;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.footer__column:last-child {
|
||||
grid-row-start: 2;
|
||||
}
|
||||
|
||||
.footer__column {
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.footer__column--center {
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.footer__column-divider--mobile-only {
|
||||
margin: 0 3px;
|
||||
font-size: 9.9px;
|
||||
}
|
||||
|
||||
.home__container {
|
||||
max-height: 100vh;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.home__name {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.index-banner {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.index-banner__title {
|
||||
font-size: 2.5rem;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.index__group-content .page__container {
|
||||
width: 90%;
|
||||
}
|
||||
|
||||
.index-banner__logo-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.action-call__margin-container {
|
||||
flex-direction: column;
|
||||
width: 85%;
|
||||
}
|
||||
|
||||
.action-call__prompt {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nav__container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.nav__header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.nav__link-group,
|
||||
.nav__link-group--small {
|
||||
position: sticky;
|
||||
flex-direction: column;
|
||||
margin: auto;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.nav__link-container--action {
|
||||
background-color: #fff;
|
||||
}
|
||||
.nav__link-container--action .nav__link {
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
.nav__link-container {
|
||||
border-bottom: 1px dashed rgba(55, 55, 55, 0.4);
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.nav__link-container--action {
|
||||
border-bottom: 1px dashed rgba(55, 55, 55, 0.4);
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.nav__link-container:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.nav__link {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.nav__hamburger-menu {
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.nav__spacer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav__link-group {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.nav__toggle:not(:checked) ~ .nav__link-group, .nav__link-group--small {
|
||||
max-height: 0;
|
||||
transition: max-height 0.4s ease-out;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nav__toggle:checked ~ .nav__link-group, .nav__toggle:checked ~ .nav__link-group--small {
|
||||
max-height: 500px;
|
||||
transition: max-height 0.4s ease-out;
|
||||
}
|
||||
|
||||
.nav__toggle:checked ~ .nav__header .nav__hamburger-inner::after {
|
||||
width: 24px;
|
||||
bottom: 1.3px;
|
||||
transform: rotate(-90deg);
|
||||
transition: bottom 0.1s ease-out, transform 0.22s cubic-bezier(0.215, 0.61, 0.355, 1) 0.12s, width 0.1s ease-out;
|
||||
}
|
||||
.nav__toggle:checked ~ .nav__header .nav__hamburger-inner::before {
|
||||
top: 0;
|
||||
opacity: 0;
|
||||
transition: top 0.1s ease-out, opacity 0.1s ease-out 0.12s;
|
||||
}
|
||||
.nav__toggle:checked ~ .nav__header .nav__hamburger-inner {
|
||||
transform: rotate(225deg);
|
||||
transition-delay: 0.12s;
|
||||
transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
|
||||
}
|
||||
|
||||
.nav__hamburger-inner::after {
|
||||
bottom: -7px;
|
||||
transition: bottom 0.1s ease-in 0.25s, transform 0.22s cubic-bezier(0.55, 0.055, 0.675, 0.19), width 0.1s ease-in 0.25s;
|
||||
}
|
||||
|
||||
.nav__hamburger-inner::after,
|
||||
.nav__hamburger-inner::before {
|
||||
content: "";
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav__hamburger-inner::before {
|
||||
top: -7px;
|
||||
transition: top 0.1s ease-in 0.25s, opacity 0.1s ease-in;
|
||||
}
|
||||
|
||||
.nav__hamburger-inner {
|
||||
top: 50%;
|
||||
margin: auto;
|
||||
transition-duration: 0.22s;
|
||||
transition-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
|
||||
}
|
||||
|
||||
.nav__hamburger-inner,
|
||||
.nav__hamburger-inner::after,
|
||||
.nav__hamburger-inner::before {
|
||||
width: 24px;
|
||||
height: 1.3px;
|
||||
position: relative;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.nav__hamburger-menu,
|
||||
.nav__hamburger-inner {
|
||||
display: block;
|
||||
}
|
||||
|
||||
/*# sourceMappingURL=mobile.css.map */
|
1
static/cache/css/mobile.css.map
vendored
Normal file
1
static/cache/css/mobile.css.map
vendored
Normal file
|
@ -0,0 +1 @@
|
|||
{"version":3,"sourceRoot":"","sources":["../../../templates/components/sass/footer/mobile.scss","../../../templates/pages/auth/sass/mobile.scss","../../../templates/components/nav/sass/mobile.scss"],"names":[],"mappings":"AAEA;EACC,WAHkB;;;AAMnB;EACC;EACA;EACA;EACA;EACA;;;AAGD;EACC;;;AAGD;AAAA;EAEC;;;AASD;EACC;EACA;;;AAGD;EACC;;;AAGD;EAdC;EACA;EACA;EAcA;;;AAGD;EAnBC;EACA;EACA;EAmBA;;;AAGD;EACC;EACA;;;AClDD;EACC;EACA;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;;;AAIA;EACC;;;AAIF;EACC;;;AAGD;EACC;EACA;;;AAGD;EACC;;;AC7BD;EACC;;;AAGD;EACC;EACA;EACA;EACA;;;AAGD;AAAA;EAEC;EACA;EACA;EACA;EACA;;;AAID;EACC;;AACA;EACC;;;AASF;EAJC;EACA;;;AAOD;EARC;EACA;;;AAWD;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;;;AAGD;EACC;;;AAGD;EACC;EACA;EACA;;;AAGD;EACC;EACA;;;AAIA;EACC;EACA,QA/E4B;EAgF5B;EACA;;AAKD;EACC;EACA;EACA;;AAGD;EACC;EACA;EACA;;;AAIF;EACC;EACA;;;AAKD;AAAA;EAEC;EACA;;;AAGD;EACC;EACA;;;AAGD;EACC;EACA;EACA;EACA;;;AAGD;AAAA;AAAA;EAGC;EACA,QAhI6B;EAiI7B;EAEA;;;AAGD;AAAA;EAEC","file":"mobile.css"}
|
15
templates/components/base.html
Normal file
15
templates/components/base.html
Normal file
|
@ -0,0 +1,15 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="{{ assets.css }}" />
|
||||
<title>Pativu</title>
|
||||
<title>{% block title %} {% endblock %}</title>
|
||||
</head>
|
||||
<body>
|
||||
<header>{% block nav %} {% endblock %}</header>
|
||||
{% block main %} {% endblock %}
|
||||
{% include "footer" %}
|
||||
</body>
|
||||
</html>
|
6
templates/components/error.html
Normal file
6
templates/components/error.html
Normal file
|
@ -0,0 +1,6 @@
|
|||
{% if error %}
|
||||
<div class="error_container">
|
||||
<h3 class="error-title">ERROR: {{ error.title }}</h3>
|
||||
<p class="error-message">{{ error.reason }}</p>
|
||||
</div>
|
||||
{% endif %}
|
37
templates/components/footer.html
Normal file
37
templates/components/footer.html
Normal file
|
@ -0,0 +1,37 @@
|
|||
<footer>
|
||||
<div class="footer__container">
|
||||
<div class="footer__column">
|
||||
<span class="license__conatiner">
|
||||
<a class="license__link" rel="noreferrer" href="/docs" target="_blank"
|
||||
>Docs</a
|
||||
>
|
||||
</div>
|
||||
<div class="footer__column">
|
||||
<a
|
||||
href="https://librepages.org"
|
||||
class="footer__link"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
title="Project Homepage"
|
||||
>
|
||||
Home
|
||||
</a>
|
||||
|
||||
<div class="footer__column-divider">|</div>
|
||||
<a href="mailto:{{ footer.support_email }}" class="footer__link"
|
||||
>Support</a
|
||||
>
|
||||
|
||||
<div class="footer__column-divider">|</div>
|
||||
<a
|
||||
class="footer__link"
|
||||
href="{{ footer.source_code }}"
|
||||
target="_blank"
|
||||
rel="noopener"
|
||||
title="Source Code"
|
||||
>
|
||||
v{{ footer.version }}-{{ footer.git_hash }}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
28
templates/components/nav/auth.html
Normal file
28
templates/components/nav/auth.html
Normal file
|
@ -0,0 +1,28 @@
|
|||
<nav class="nav__container">
|
||||
<input type="checkbox" class="nav__toggle" id="nav__toggle" />
|
||||
|
||||
<div class="nav__header">
|
||||
<a class="nav__logo-container" href="/">
|
||||
<p class="nav__home-btn">Pativu</p>
|
||||
</a>
|
||||
<label class="nav__hamburger-menu" for="nav__toggle">
|
||||
<span class="nav__hamburger-inner"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="nav__spacer"></div>
|
||||
|
||||
<div class="nav__link-group">
|
||||
<div class="nav__link-container">
|
||||
<a class="nav__link" rel="noreferrer" href="{{ page.gist.new }}">New Paste</a>
|
||||
</div>
|
||||
{% if loggedin_user %}
|
||||
<div class="nav__link-container">
|
||||
<a class="nav__link" rel="noreferrer" href="{{ loggedin_user }}">Profile</a>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div class="nav__link-container">
|
||||
<a class="nav__link" rel="noreferrer" href="{{ page.auth.logout }}">Log out</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
18
templates/components/nav/base.html
Normal file
18
templates/components/nav/base.html
Normal file
|
@ -0,0 +1,18 @@
|
|||
<nav class="nav__container">
|
||||
<input type="checkbox" class="nav__toggle" id="nav__toggle" />
|
||||
|
||||
<div class="nav__header">
|
||||
<a class="nav__logo-container" href="/">
|
||||
<p class="nav__home-btn">Pativu</p>
|
||||
</a>
|
||||
<label class="nav__hamburger-menu" for="nav__toggle">
|
||||
<span class="nav__hamburger-inner"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="nav__spacer"></div>
|
||||
|
||||
<div class="nav__link-group">
|
||||
{% block nav_links %} {% endblock %}
|
||||
</div>
|
||||
</nav>
|
23
templates/components/nav/pub.html
Normal file
23
templates/components/nav/pub.html
Normal file
|
@ -0,0 +1,23 @@
|
|||
<nav class="nav__container">
|
||||
<input type="checkbox" class="nav__toggle" id="nav__toggle" />
|
||||
|
||||
<div class="nav__header">
|
||||
<a class="nav__logo-container" href="/">
|
||||
<p class="nav__home-btn">Pativu</p>
|
||||
</a>
|
||||
<label class="nav__hamburger-menu" for="nav__toggle">
|
||||
<span class="nav__hamburger-inner"></span>
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="nav__spacer"></div>
|
||||
|
||||
<div class="nav__link-group">
|
||||
<div class="nav__link-container">
|
||||
<a class="nav__link" rel="noreferrer" href="{{ page.auth.login }}">Login</a>
|
||||
</div>
|
||||
<div class="nav__link-container">
|
||||
<a class="nav__link" rel="noreferrer" href="{{ page.auth.register }}">Register</a>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
112
templates/components/nav/sass/main.scss
Normal file
112
templates/components/nav/sass/main.scss
Normal file
|
@ -0,0 +1,112 @@
|
|||
@import "../../sass/_link";
|
||||
|
||||
header {
|
||||
z-index: 5;
|
||||
position: sticky;
|
||||
top: 0;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.nav__container {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
|
||||
box-sizing: border-box;
|
||||
width: 100%;
|
||||
padding-top: 5px;
|
||||
border-bottom: 1px solid rgb(211, 211, 211);
|
||||
}
|
||||
|
||||
.nav__home-btn {
|
||||
font-weight: bold;
|
||||
// font-family: monospace, monospace;
|
||||
margin: auto;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
.nav__hamburger-menu {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav__spacer--small {
|
||||
width: 100px;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.nav__spacer {
|
||||
flex: 4;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.nav__logo-container {
|
||||
display: inline-flex;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.nav__logo-container:hover {
|
||||
@include a_hover;
|
||||
}
|
||||
|
||||
.nav__toggle {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav__logo {
|
||||
display: inline-flex;
|
||||
margin: auto;
|
||||
padding: 5px;
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
@mixin nav__link-group {
|
||||
flex: 1.5;
|
||||
list-style: none;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
align-self: center;
|
||||
margin: auto;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.nav__link-group {
|
||||
@include nav__link-group;
|
||||
|
||||
}
|
||||
|
||||
.nav__link-group--small {
|
||||
@include nav__link-group;
|
||||
flex: 0.5;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
@mixin nav__link-container {
|
||||
display: flex;
|
||||
padding: 10px;
|
||||
height: 100%;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.nav__link-container {
|
||||
@include nav__link-container;
|
||||
}
|
||||
|
||||
.nav__link-container--action {
|
||||
@include nav__link-container;
|
||||
background-color: green;
|
||||
padding: 15px;
|
||||
.nav__link {
|
||||
color: white !important;
|
||||
}
|
||||
}
|
||||
|
||||
.nav__link {
|
||||
text-decoration: none;
|
||||
color: black !important;
|
||||
font-weight: 600;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.nav__link:hover {
|
||||
@include a_hover;
|
||||
}
|
141
templates/components/nav/sass/mobile.scss
Normal file
141
templates/components/nav/sass/mobile.scss
Normal file
|
@ -0,0 +1,141 @@
|
|||
//@import '../_vars';
|
||||
|
||||
$hamburger-menu-animation: 0.4s ease-out;
|
||||
$nav__hamburger-inner-height: 1.3px;
|
||||
|
||||
.nav__container {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.nav__header {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
min-width: 100%;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.nav__link-group,
|
||||
.nav__link-group--small {
|
||||
position: sticky;
|
||||
flex-direction: column;
|
||||
margin: auto;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
// background-color: $light-blue;
|
||||
}
|
||||
|
||||
.nav__link-container--action {
|
||||
background-color: #fff;
|
||||
.nav__link {
|
||||
color: #000 !important;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin nav__link-container {
|
||||
border-bottom: 1px dashed rgba(55, 55, 55, 0.4);
|
||||
width: 70%;
|
||||
}
|
||||
|
||||
.nav__link-container {
|
||||
@include nav__link-container;
|
||||
}
|
||||
|
||||
.nav__link-container--action {
|
||||
@include nav__link-container;
|
||||
}
|
||||
|
||||
.nav__link-container:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.nav__link {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.nav__hamburger-menu {
|
||||
display: inline-block;
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
}
|
||||
|
||||
.nav__spacer {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav__link-group {
|
||||
margin-right: auto;
|
||||
}
|
||||
|
||||
.nav__toggle:not(:checked) ~ .nav__link-group, .nav__link-group--small {
|
||||
max-height: 0;
|
||||
transition: max-height $hamburger-menu-animation;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.nav__toggle:checked ~ .nav__link-group, .nav__toggle:checked ~ .nav__link-group--small {
|
||||
max-height: 500px;
|
||||
transition: max-height $hamburger-menu-animation;
|
||||
}
|
||||
|
||||
.nav__toggle:checked ~ .nav__header {
|
||||
.nav__hamburger-inner::after {
|
||||
width: 24px;
|
||||
bottom: $nav__hamburger-inner-height;
|
||||
transform: rotate(-90deg);
|
||||
transition: bottom 0.1s ease-out,
|
||||
transform 0.22s cubic-bezier(0.215, 0.61, 0.355, 1) 0.12s,
|
||||
width 0.1s ease-out;
|
||||
}
|
||||
|
||||
.nav__hamburger-inner::before {
|
||||
top: 0;
|
||||
opacity: 0;
|
||||
transition: top 0.1s ease-out, opacity 0.1s ease-out 0.12s;
|
||||
}
|
||||
|
||||
.nav__hamburger-inner {
|
||||
transform: rotate(225deg);
|
||||
transition-delay: 0.12s;
|
||||
transition-timing-function: cubic-bezier(0.215, 0.61, 0.355, 1);
|
||||
}
|
||||
}
|
||||
|
||||
.nav__hamburger-inner::after {
|
||||
bottom: -7px;
|
||||
transition: bottom 0.1s ease-in 0.25s,
|
||||
transform 0.22s cubic-bezier(0.55, 0.055, 0.675, 0.19),
|
||||
width 0.1s ease-in 0.25s;
|
||||
}
|
||||
|
||||
.nav__hamburger-inner::after,
|
||||
.nav__hamburger-inner::before {
|
||||
content: "";
|
||||
display: block;
|
||||
}
|
||||
|
||||
.nav__hamburger-inner::before {
|
||||
top: -7px;
|
||||
transition: top 0.1s ease-in 0.25s, opacity 0.1s ease-in;
|
||||
}
|
||||
|
||||
.nav__hamburger-inner {
|
||||
top: 50%;
|
||||
margin: auto;
|
||||
transition-duration: 0.22s;
|
||||
transition-timing-function: cubic-bezier(0.55, 0.055, 0.675, 0.19);
|
||||
}
|
||||
|
||||
.nav__hamburger-inner,
|
||||
.nav__hamburger-inner::after,
|
||||
.nav__hamburger-inner::before {
|
||||
width: 24px;
|
||||
height: $nav__hamburger-inner-height;
|
||||
position: relative;
|
||||
// background: $dark-black;
|
||||
background: #000;
|
||||
}
|
||||
|
||||
.nav__hamburger-menu,
|
||||
.nav__hamburger-inner {
|
||||
display: block;
|
||||
}
|
5
templates/components/sass/_fullscreen.scss
Normal file
5
templates/components/sass/_fullscreen.scss
Normal file
|
@ -0,0 +1,5 @@
|
|||
@mixin fullscreen {
|
||||
height: 100vh;
|
||||
min-height: 500px;
|
||||
max-height: 800px;
|
||||
}
|
4
templates/components/sass/_link.scss
Normal file
4
templates/components/sass/_link.scss
Normal file
|
@ -0,0 +1,4 @@
|
|||
@mixin a_hover {
|
||||
color: rgb(0, 86, 179);
|
||||
text-decoration: underline;
|
||||
}
|
79
templates/components/sass/footer/main.scss
Normal file
79
templates/components/sass/footer/main.scss
Normal file
|
@ -0,0 +1,79 @@
|
|||
@import "../_link";
|
||||
|
||||
footer {
|
||||
display: block;
|
||||
color: #333;
|
||||
font-size: 0.7rem;
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.footer__container {
|
||||
width: 100%;
|
||||
padding: 0;
|
||||
justify-content: space-between;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
@mixin footer__column-base {
|
||||
list-style: none;
|
||||
display: flex;
|
||||
margin: auto 50px;
|
||||
align-items: center;
|
||||
flex: 2.5;
|
||||
}
|
||||
|
||||
.footer__column {
|
||||
@include footer__column-base;
|
||||
}
|
||||
|
||||
.footer__column--center {
|
||||
@include footer__column-base;
|
||||
margin: auto;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
.footer__column:last-child {
|
||||
justify-content: flex-end;
|
||||
a {
|
||||
margin: 10px;
|
||||
}
|
||||
}
|
||||
|
||||
.footer__link-container {
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.footer__link {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.license__link {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.license__link:hover {
|
||||
@include a_hover;
|
||||
}
|
||||
|
||||
.footer__column-divider,
|
||||
.footer__column-divider--mobile-visible,
|
||||
.footer__column-divider--mobile-only {
|
||||
font-weight: 500;
|
||||
opacity: 0.7;
|
||||
margin: 0 5px;
|
||||
}
|
||||
|
||||
.footer__column-divider--mobile-only {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.footer__icon {
|
||||
margin: auto 5px;
|
||||
height: 20px;
|
||||
}
|
52
templates/components/sass/footer/mobile.scss
Normal file
52
templates/components/sass/footer/mobile.scss
Normal file
|
@ -0,0 +1,52 @@
|
|||
$footer-font-size: 0.44rem;
|
||||
|
||||
footer {
|
||||
font-size: $footer-font-size;
|
||||
}
|
||||
|
||||
.footer__container {
|
||||
display: grid;
|
||||
grid-template-rows: repeat(3, 100%);
|
||||
align-items: center;
|
||||
margin: auto;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.footer__link {
|
||||
font-size: 0.5rem;
|
||||
}
|
||||
|
||||
.license__conatiner,
|
||||
.license__link {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
@mixin footer__column-base {
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.footer__column:first-child {
|
||||
grid-row-start: 3;
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.footer__column:last-child {
|
||||
grid-row-start: 2;
|
||||
}
|
||||
|
||||
.footer__column {
|
||||
@include footer__column-base;
|
||||
align-self: flex-end;
|
||||
}
|
||||
|
||||
.footer__column--center {
|
||||
@include footer__column-base;
|
||||
align-self: flex-start;
|
||||
}
|
||||
|
||||
.footer__column-divider--mobile-only {
|
||||
margin: 0 3px;
|
||||
font-size: 9.9px;
|
||||
}
|
70
templates/defaults.scss
Normal file
70
templates/defaults.scss
Normal file
|
@ -0,0 +1,70 @@
|
|||
* {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
//font-family: "Inter UI", -apple-system, BlinkMacSystemFont, "Roboto",
|
||||
// "Segoe UI", Helvetica, Arial, sans-serif;
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
a:hover, button:hover {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a,
|
||||
a:visited {
|
||||
color: rgb(0, 86, 179);
|
||||
}
|
||||
|
||||
.base {
|
||||
min-height: 100vh;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.main__content-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 100%;
|
||||
justify-content: space-between;
|
||||
flex: 2;
|
||||
}
|
||||
|
||||
p,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
li,
|
||||
ol,
|
||||
ul {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
main {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
blockquote {
|
||||
border-left: 0.3em solid rgba(55, 55, 55, 0.4);
|
||||
margin-bottom: 16px;
|
||||
//padding-left: 20px;
|
||||
padding: 0 1em;
|
||||
color: #707070;
|
||||
|
||||
p,
|
||||
h1,
|
||||
h2,
|
||||
h3,
|
||||
h4,
|
||||
li,
|
||||
ol,
|
||||
ul {
|
||||
color: inherit;
|
||||
}
|
||||
}
|
5
templates/main.scss
Normal file
5
templates/main.scss
Normal file
|
@ -0,0 +1,5 @@
|
|||
@import "defaults.scss";
|
||||
@import "pages/auth/sass/main.scss";
|
||||
@import "pages/auth/sass/form/main.scss";
|
||||
@import "components/sass/footer/main.scss";
|
||||
@import "components/nav/sass/main.scss";
|
3
templates/mobile.scss
Normal file
3
templates/mobile.scss
Normal file
|
@ -0,0 +1,3 @@
|
|||
@import "components/sass/footer/mobile.scss";
|
||||
@import "pages/auth/sass/mobile.scss";
|
||||
@import "components/nav/sass/mobile.scss";
|
46
templates/pages/auth/base.html
Normal file
46
templates/pages/auth/base.html
Normal file
|
@ -0,0 +1,46 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="{{ assets.css }}" />
|
||||
<title>Pativu</title>
|
||||
</head>
|
||||
<body class="auth__body">
|
||||
<header>{% include "pub_nav" %}</header>
|
||||
|
||||
<main class="index-banner__container">
|
||||
<section class="index-banner">
|
||||
<div class="index-banner__content-container">
|
||||
<h1 class="index-banner__title">Self-hosted internet archive</h1>
|
||||
<p class="index-banner__tagline">
|
||||
Personal internet archiving platform with focus on speed
|
||||
</p>
|
||||
<ul class="index-banner__features-list">
|
||||
<li class="index-banner__features">
|
||||
<b>Sanitize webpages</b> improving reader focus
|
||||
</li>
|
||||
<li class="index-banner__features">
|
||||
<b>Small, single-binary</b> making self-hosting easy
|
||||
</li>
|
||||
<li class="index-banner__features">
|
||||
<b>Crawl linked static assets</b> to completely archive a webpage
|
||||
</li>
|
||||
<li class="index-banner__features">
|
||||
<b>
|
||||
100%
|
||||
<a href="https://www.gnu.org/philosophy/free-sw.html"
|
||||
>Free Software</a
|
||||
> </b
|
||||
>: deploy your own instance
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
<section class="index-banner__logo-container">
|
||||
{% block login %} {% endblock %}
|
||||
</section>
|
||||
</main>
|
||||
{% include "footer" %}
|
||||
</body>
|
||||
</html>
|
44
templates/pages/auth/login.html
Normal file
44
templates/pages/auth/login.html
Normal file
|
@ -0,0 +1,44 @@
|
|||
{% extends 'authbase' %}
|
||||
{% block login %}
|
||||
<h2>Sign In</h2>
|
||||
<form action="{{ page.auth.login }}" method="POST" class="auth-form" accept-charset="utf-8">
|
||||
{% include "error_comp" %}
|
||||
<label class="auth-form__label" for="login">
|
||||
Username or Email
|
||||
<input
|
||||
class="auth-form__input"
|
||||
name="login"
|
||||
autofocus
|
||||
required
|
||||
id="login"
|
||||
type="text"
|
||||
{% if payload.username %}
|
||||
value={{ payload.username }}
|
||||
{% endif %}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="auth-form__label" for="password">
|
||||
Password
|
||||
<input
|
||||
class="auth-form__input"
|
||||
name="password"
|
||||
required
|
||||
id="password"
|
||||
type="password"
|
||||
{% if payload.password %}
|
||||
value={{ payload.password }}
|
||||
{% endif %}
|
||||
/>
|
||||
</label>
|
||||
<div class="auth-form__action-container">
|
||||
<a href="">Forgot password?</a>
|
||||
<button class="auth-form__submit" type="submit">Login</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p class="auth-form__alt-action">
|
||||
New to Pativu?
|
||||
<a href="{{ page.auth.register }}">Create an account</a>
|
||||
</p>
|
||||
{% endblock %}
|
73
templates/pages/auth/register.html
Normal file
73
templates/pages/auth/register.html
Normal file
|
@ -0,0 +1,73 @@
|
|||
{% extends 'authbase' %}
|
||||
{% block title_name %}Sign Up {% endblock %}
|
||||
{% block login %}
|
||||
<h2>Sign Up</h2>
|
||||
<form action="{{ page.auth.register }}" method="POST" class="auth-form" accept-charset="utf-8">
|
||||
{% include "error_comp" %}
|
||||
<label class="auth-form__label" for="username">
|
||||
Username
|
||||
<input
|
||||
class="auth-form__input"
|
||||
autofocus
|
||||
name="username"
|
||||
required
|
||||
id="username"
|
||||
type="text"
|
||||
{% if payload.username %}
|
||||
value={{ payload.username }}
|
||||
{% endif %}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="auth-form__label" for="email">
|
||||
Email
|
||||
<input
|
||||
class="auth-form__input"
|
||||
name="email"
|
||||
id="email"
|
||||
type="email"
|
||||
{% if payload.email %}
|
||||
value={{ payload.email }}
|
||||
{% endif %}
|
||||
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="auth-form__label" for="password">
|
||||
password
|
||||
<input
|
||||
class="auth-form__input"
|
||||
name="password"
|
||||
required
|
||||
id="password"
|
||||
type="password"
|
||||
{% if payload.password %}
|
||||
value={{ payload.password }}
|
||||
{% endif %}
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label class="auth-form__label" for="confirm_password">
|
||||
Re-enter Password
|
||||
<input
|
||||
class="auth-form__input"
|
||||
name="confirm_password"
|
||||
required
|
||||
id="confirm_password"
|
||||
type="password"
|
||||
{% if payload.confirm_password %}
|
||||
value={{ payload.confirm_password }}
|
||||
{% endif %}
|
||||
/>
|
||||
</label>
|
||||
<div class="auth-form__action-container">
|
||||
<a href="/forgot-password">Forgot password?</a>
|
||||
<button class="auth-form__submit" type="submit">Sign Up</button>
|
||||
</div>
|
||||
</form>
|
||||
|
||||
<p class="auth-form__alt-action">
|
||||
Already have an account?
|
||||
<a href="{{ page.auth.login }}"> Login </a>
|
||||
</p>
|
||||
{% endblock %}
|
29
templates/pages/auth/sass/form/main.scss
Normal file
29
templates/pages/auth/sass/form/main.scss
Normal file
|
@ -0,0 +1,29 @@
|
|||
.auth-form {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 80%;
|
||||
margin: auto;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
.auth-form__input {
|
||||
display: block;
|
||||
width: 100%;
|
||||
margin: 10px 0;
|
||||
padding: 5px 0;
|
||||
}
|
||||
|
||||
.auth-form__submit {
|
||||
width: 100%;
|
||||
display: block;
|
||||
margin: 10px 0;
|
||||
background-color: green;
|
||||
color: #fff;
|
||||
border: none;
|
||||
padding: 5px 0;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.auth-form__submit:hover {
|
||||
background-color: green;
|
||||
}
|
152
templates/pages/auth/sass/main.scss
Normal file
152
templates/pages/auth/sass/main.scss
Normal file
|
@ -0,0 +1,152 @@
|
|||
@import "../../../components/sass/fullscreen";
|
||||
|
||||
.auth__body {
|
||||
display: flex;
|
||||
@include fullscreen;
|
||||
flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
$heading-letter-spacing: 20px;
|
||||
.index-banner__container {
|
||||
width: 100%;
|
||||
display: flex;
|
||||
//background-color: #d1875a;
|
||||
// background-color: #3c3c3c;
|
||||
// background-color: #58181f;
|
||||
font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
.index-banner {
|
||||
margin: auto;
|
||||
display: flex;
|
||||
// flex-direction: column;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.index-banner__content-container {
|
||||
// height: 300px;
|
||||
li {
|
||||
// color: white;
|
||||
}
|
||||
}
|
||||
.index-banner__logo-container {
|
||||
margin: auto;
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 500px;
|
||||
}
|
||||
|
||||
.index-banner__title {
|
||||
margin: auto;
|
||||
font-style: none;
|
||||
//color: #fff;
|
||||
}
|
||||
|
||||
.index-banner__tagline {
|
||||
margin: auto;
|
||||
// color: #fff;
|
||||
// font-size: 1.4rem;
|
||||
}
|
||||
|
||||
.index-banner__title-container {
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.index-banner__logo {
|
||||
width: 120px;
|
||||
margin: auto;
|
||||
border-radius: 20px;
|
||||
}
|
||||
|
||||
.index-banner__main-action-btn {
|
||||
display: block;
|
||||
display: block;
|
||||
font-weight: 400;
|
||||
padding: 15px;
|
||||
border: none;
|
||||
margin: 20px 0;
|
||||
background-color: green;
|
||||
}
|
||||
|
||||
.index-banner__main-action-link {
|
||||
color: white !important;
|
||||
}
|
||||
|
||||
.index-banner__main-action-btn:hover {
|
||||
// background-color: lightgray;
|
||||
}
|
||||
|
||||
.index-banner__features-list {
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.index-banner__features {
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.home__features {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.home__features-title {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
$page-content-width: 80%;
|
||||
.index__group-content {
|
||||
.page__container {
|
||||
width: $page-content-width;
|
||||
@include fullscreen;
|
||||
height: 90vh !important;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
justify-content: space-around;
|
||||
}
|
||||
}
|
||||
|
||||
.action-call__container {
|
||||
background: #1f5818;
|
||||
width: 100%;
|
||||
padding: 60px 0;
|
||||
}
|
||||
|
||||
.action-call__margin-container {
|
||||
display: flex;
|
||||
width: $page-content-width;
|
||||
margin: auto;
|
||||
align-items: center;
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
.action-call__prompt {
|
||||
color: white;
|
||||
font-weight: 400;
|
||||
font-size: 1.7rem;
|
||||
}
|
||||
|
||||
.action-call__button {
|
||||
display: block;
|
||||
display: block;
|
||||
font-weight: 400;
|
||||
padding: 15px;
|
||||
border: none;
|
||||
margin: 20px 0;
|
||||
background-color: #fff;
|
||||
}
|
||||
|
||||
.action-call__button:hover {
|
||||
background-color: lightgray;
|
||||
}
|
||||
|
||||
.action-call_link {
|
||||
color: #000 !important;
|
||||
}
|
||||
|
||||
.action-call_link:hover {
|
||||
text-decoration: none !important;
|
||||
}
|
36
templates/pages/auth/sass/mobile.scss
Normal file
36
templates/pages/auth/sass/mobile.scss
Normal file
|
@ -0,0 +1,36 @@
|
|||
.home__container {
|
||||
max-height: 100vh;
|
||||
height: 100vh;
|
||||
}
|
||||
|
||||
.home__name {
|
||||
font-size: 2rem;
|
||||
}
|
||||
|
||||
.index-banner {
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.index-banner__title {
|
||||
font-size: 2.5rem;
|
||||
margin: auto;
|
||||
}
|
||||
|
||||
.index__group-content {
|
||||
.page__container {
|
||||
width: 90%;
|
||||
}
|
||||
}
|
||||
|
||||
.index-banner__logo-container {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.action-call__margin-container {
|
||||
flex-direction: column;
|
||||
width: 85%;
|
||||
}
|
||||
|
||||
.action-call__prompt {
|
||||
text-align: center;
|
||||
}
|
154
templates/pages/dash/index.html
Normal file
154
templates/pages/dash/index.html
Normal file
|
@ -0,0 +1,154 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="{{ assets.css }}" />
|
||||
<title>Pativu</title>
|
||||
</head>
|
||||
<body class="auth__body">
|
||||
<header>
|
||||
<nav>
|
||||
<p>Pativu</p>
|
||||
<span class="nav__spacer"></span>
|
||||
<ul class="nav__links">
|
||||
<li class="nav__item">Help</li>
|
||||
<li class="nav__item">Settings</li>
|
||||
<li class="nav__item">Logout</li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
<div class="sites__collection">
|
||||
<div class="sites__actions">
|
||||
<a class="sites__actions__new-site" href="{{ page.dash.add_site }}">
|
||||
<button>Add new site</button>
|
||||
</a>
|
||||
</div>
|
||||
{% for site in payload %}
|
||||
<a href="{{ page.serve.catch_all }}?url={{ site.url }}" class="site__container">
|
||||
<div class="site__info--head">
|
||||
<div class="site__info--column">
|
||||
<p><b></b>{{ site.url }}</b></p>
|
||||
</div>
|
||||
</div>
|
||||
</a>
|
||||
{% endfor %}
|
||||
</div>
|
||||
</main>
|
||||
{% include "footer" %}
|
||||
</body>
|
||||
|
||||
<style>
|
||||
header {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
nav {
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.nav__spacer {
|
||||
flex: 4;
|
||||
}
|
||||
|
||||
.nav__links {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.nav__item {
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
main {
|
||||
width: 100%;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.sites__collection {
|
||||
margin: auto;
|
||||
width: 70%;
|
||||
|
||||
border: 1px solid #e8ebed;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.sites__actions {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 0px 20px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.sites__actions__new-site {
|
||||
min-height: 36px;
|
||||
background: green;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0px 8px;
|
||||
}
|
||||
|
||||
.sites__actions__new-site > button {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
border: none;
|
||||
width: 100%;
|
||||
color: white;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.site__container {
|
||||
box-sizing: border-box;
|
||||
margin: 10px 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px 20px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.site__container:hover {
|
||||
background: #f7f8f8;
|
||||
}
|
||||
|
||||
.site__info--head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.site__info--column {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.site__info--column > p,
|
||||
.site__info--column > a {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.site__container:visited,
|
||||
.site__container {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.site__container--preview {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
</style>
|
||||
</html>
|
144
templates/pages/dash/sites/add.html
Normal file
144
templates/pages/dash/sites/add.html
Normal file
|
@ -0,0 +1,144 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<link rel="stylesheet" href="{{ assets.css }}" />
|
||||
<title>Pativu</title>
|
||||
</head>
|
||||
<body class="auth__body">
|
||||
<header>
|
||||
<nav>
|
||||
<p>Pativu</p>
|
||||
<span class="nav__spacer"></span>
|
||||
<ul class="nav__links">
|
||||
<li class="nav__item">Help</li>
|
||||
<li class="nav__item">Settings</li>
|
||||
<li class="nav__item"><a href="{{ page.auth.logout }}">Logout</a></li>
|
||||
</ul>
|
||||
</nav>
|
||||
</header>
|
||||
<main>
|
||||
</main>
|
||||
<form action="{{ page.dash.add_site }}" method="post">
|
||||
<label for="url">
|
||||
<input type="url" name="url" id="url">
|
||||
</label>
|
||||
<button type="submit">Archive</button>
|
||||
</form>
|
||||
{% include "footer" %}
|
||||
</body>
|
||||
|
||||
<style>
|
||||
header {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
nav {
|
||||
width: 100%;
|
||||
margin: auto;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.nav__spacer {
|
||||
flex: 4;
|
||||
}
|
||||
|
||||
.nav__links {
|
||||
display: flex;
|
||||
list-style: none;
|
||||
}
|
||||
|
||||
.nav__item {
|
||||
margin: 0 20px;
|
||||
}
|
||||
|
||||
body {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
main {
|
||||
width: 100%;
|
||||
margin: 20px;
|
||||
}
|
||||
|
||||
.sites__collection {
|
||||
margin: auto;
|
||||
width: 70%;
|
||||
|
||||
border: 1px solid #e8ebed;
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.sites__actions {
|
||||
width: 100%;
|
||||
height: 50px;
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
align-items: center;
|
||||
box-sizing: border-box;
|
||||
padding: 0px 20px;
|
||||
margin: 10px 0;
|
||||
}
|
||||
|
||||
.sites__actions__new-site {
|
||||
min-height: 36px;
|
||||
background: green;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 0px 8px;
|
||||
}
|
||||
|
||||
.sites__actions__new-site > button {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
height: 100%;
|
||||
border: none;
|
||||
width: 100%;
|
||||
color: white;
|
||||
background: none;
|
||||
}
|
||||
|
||||
.site__container {
|
||||
box-sizing: border-box;
|
||||
margin: 10px 0;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 10px 20px;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.site__container:hover {
|
||||
background: #f7f8f8;
|
||||
}
|
||||
|
||||
.site__info--head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.site__info--column {
|
||||
margin-left: 20px;
|
||||
}
|
||||
|
||||
.site__info--column > p,
|
||||
.site__info--column > a {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
.site__container:visited,
|
||||
.site__container {
|
||||
color: black;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.site__container--preview {
|
||||
width: 50px;
|
||||
height: 50px;
|
||||
border-radius: 50%;
|
||||
}
|
||||
</style>
|
||||
</html>
|
2
utils/cache-bust/.gitignore
vendored
Normal file
2
utils/cache-bust/.gitignore
vendored
Normal file
|
@ -0,0 +1,2 @@
|
|||
/target
|
||||
src/cache_buster_data.json
|
354
utils/cache-bust/Cargo.lock
generated
Normal file
354
utils/cache-bust/Cargo.lock
generated
Normal file
|
@ -0,0 +1,354 @@
|
|||
# This file is automatically @generated by Cargo.
|
||||
# It is not intended for manual editing.
|
||||
version = 3
|
||||
|
||||
[[package]]
|
||||
name = "block-buffer"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0bf7fe51849ea569fd452f37822f606a5cabb684dc918707a0193fd4664ff324"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cache-buster"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/realaravinth/cache-buster#7ca4545722fb99be30698a5e72c7d982a70fa11f"
|
||||
dependencies = [
|
||||
"data-encoding",
|
||||
"derive_builder",
|
||||
"mime",
|
||||
"mime_guess",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"sha2",
|
||||
"walkdir",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
version = "1.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "cpufeatures"
|
||||
version = "0.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "59a6001667ab124aebae2a495118e11d30984c3a653e99d86d58971708cf5e4b"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crypto-common"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "57952ca27b5e3606ff4dd79b0020231aaf9d6aa76dc05fd30137538c50bd3ce8"
|
||||
dependencies = [
|
||||
"generic-array",
|
||||
"typenum",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4529658bdda7fd6769b8614be250cdcfc3aeb0ee72fe66f9e41e5e5eb73eac02"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"darling_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_core"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "649c91bc01e8b1eac09fb91e8dbc7d517684ca6be8ebc75bb9cafc894f9fdb6f"
|
||||
dependencies = [
|
||||
"fnv",
|
||||
"ident_case",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"strsim",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "darling_macro"
|
||||
version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ddfc69c5bfcbd2fc09a0f38451d2daf0e372e367986a83906d1b0dbc88134fb5"
|
||||
dependencies = [
|
||||
"darling_core",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "data-encoding"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ee2393c4a91429dffb4bedf19f4d6abf27d8a732c8ce4980305d782e5426d57"
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d07adf7be193b71cc36b193d0f5fe60b918a3a9db4dad0449f57bcfd519704a3"
|
||||
dependencies = [
|
||||
"derive_builder_macro",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder_core"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f91d4cfa921f1c05904dc3c57b4a32c38aed3340cce209f3a6fd1478babafc4"
|
||||
dependencies = [
|
||||
"darling",
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "derive_builder_macro"
|
||||
version = "0.11.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f0314b72bed045f3a68671b3c86328386762c93f82d98c65c3cb5e5f573dd68"
|
||||
dependencies = [
|
||||
"derive_builder_core",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "digest"
|
||||
version = "0.10.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f2fb860ca6fafa5552fb6d0e816a69c8e49f0908bf524e30a90d97c85892d506"
|
||||
dependencies = [
|
||||
"block-buffer",
|
||||
"crypto-common",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fnv"
|
||||
version = "1.0.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
|
||||
|
||||
[[package]]
|
||||
name = "generic-array"
|
||||
version = "0.14.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fd48d33ec7f05fbfa152300fdad764757cbded343c1aa1cff2fbaf4134851803"
|
||||
dependencies = [
|
||||
"typenum",
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ident_case"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
|
||||
|
||||
[[package]]
|
||||
name = "itoa"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.125"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5916d2ae698f6de9bfb891ad7a8d65c09d232dc58cc4ac433c7da3b2fd84bc2b"
|
||||
|
||||
[[package]]
|
||||
name = "mime"
|
||||
version = "0.3.16"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2a60c7ce501c71e03a9c9c0d35b861413ae925bd979cc7a4e30d060069aaac8d"
|
||||
|
||||
[[package]]
|
||||
name = "mime_guess"
|
||||
version = "2.0.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4192263c238a5f0d0c6bfd21f336a313a4ce1c450542449ca191bb657b4642ef"
|
||||
dependencies = [
|
||||
"mime",
|
||||
"unicase",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "pativu-cachebust-util"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"cache-buster",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9027b48e9d4c9175fa2218adf3557f91c1137021739951d4932f5f8268ac48aa"
|
||||
dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.18"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1feb54ed693b93a84e14094943b84b7c4eae204c512b7ccb95ab0c66d278ad1"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ryu"
|
||||
version = "1.0.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
|
||||
|
||||
[[package]]
|
||||
name = "same-file"
|
||||
version = "1.0.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.137"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61ea8d54c77f8315140a05f4c7237403bf38b72704d031543aa1d16abbf517d1"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.137"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1f26faba0c3959972377d3b2d306ee9f71faee9714294e41bb777f83f88578be"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.81"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b7ce2b32a1aed03c558dc61a5cd328f15aff2dbc17daad8fb8af04d2100e15c"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha2"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "55deaec60f81eefe3cce0dc50bda92d6d8e88f2a27df7c5033b42afeb1ed2676"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"cpufeatures",
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "strsim"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "73473c0e59e6d5812c5dfe2a064a6444949f089e20eec9a2e5506596494e4623"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.94"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a07e33e919ebcd69113d5be0e4d70c5707004ff45188910106854f38b960df4a"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"unicode-xid",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "typenum"
|
||||
version = "1.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dcf81ac59edc17cc8697ff311e8f5ef2d99fcbd9817b34cec66f90b6c3dfd987"
|
||||
|
||||
[[package]]
|
||||
name = "unicase"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "50f37be617794602aabbeee0be4f259dc1778fabe05e2d67ee8f79326d5cb4f6"
|
||||
dependencies = [
|
||||
"version_check",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "unicode-xid"
|
||||
version = "0.2.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "957e51f3646910546462e67d5f7599b9e4fb8acdd304b087a6494730f9eebf04"
|
||||
|
||||
[[package]]
|
||||
name = "version_check"
|
||||
version = "0.9.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
|
||||
|
||||
[[package]]
|
||||
name = "walkdir"
|
||||
version = "2.3.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "808cf2735cd4b6866113f648b791c6adc5714537bc222d9347bb203386ffda56"
|
||||
dependencies = [
|
||||
"same-file",
|
||||
"winapi",
|
||||
"winapi-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi"
|
||||
version = "0.3.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419"
|
||||
dependencies = [
|
||||
"winapi-i686-pc-windows-gnu",
|
||||
"winapi-x86_64-pc-windows-gnu",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-i686-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
|
||||
|
||||
[[package]]
|
||||
name = "winapi-util"
|
||||
version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "winapi-x86_64-pc-windows-gnu"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
|
19
utils/cache-bust/Cargo.toml
Normal file
19
utils/cache-bust/Cargo.toml
Normal file
|
@ -0,0 +1,19 @@
|
|||
[package]
|
||||
name = "pativu-cachebust-util"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
homepage = "https://git.batsense.net/realaravinth/pativu"
|
||||
repository = "https://git.batsense.net/realaravinth/pativu"
|
||||
documentation = "https://git.batsense.net/realaravinth/pativu"
|
||||
readme = "https://git.batsense.net/realaravinth/pativu/blob/master/README.md"
|
||||
license = "AGPLv3 or later version"
|
||||
authors = ["realaravinth <realaravinth@batsense.net>"]
|
||||
|
||||
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
cache-buster = { version = "0.2.0", git = "https://github.com/realaravinth/cache-buster" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
78
utils/cache-bust/src/main.rs
Normal file
78
utils/cache-bust/src/main.rs
Normal file
|
@ -0,0 +1,78 @@
|
|||
/*
|
||||
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as
|
||||
* published by the Free Software Foundation, either version 3 of the
|
||||
* License, or (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
*/
|
||||
use std::fs;
|
||||
use std::path::Path;
|
||||
use std::collections::HashMap;
|
||||
|
||||
use cache_buster::{BusterBuilder, CACHE_BUSTER_DATA_FILE, NoHashCategory};
|
||||
use serde::{Serialize, Deserialize};
|
||||
|
||||
#[derive(Deserialize, Serialize)]
|
||||
struct FileMap {
|
||||
map: HashMap<String, String>,
|
||||
base_dir: String,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
cache_bust();
|
||||
process_file_map();
|
||||
}
|
||||
|
||||
fn cache_bust() {
|
||||
// until APPLICATION_WASM gets added to mime crate
|
||||
// PR: https://github.com/hyperium/mime/pull/138
|
||||
// let types = vec![
|
||||
// mime::IMAGE_PNG,
|
||||
// mime::IMAGE_SVG,
|
||||
// mime::IMAGE_JPEG,
|
||||
// mime::IMAGE_GIF,
|
||||
// mime::APPLICATION_JAVASCRIPT,
|
||||
// mime::TEXT_CSS,
|
||||
// ];
|
||||
|
||||
println!("[*] Cache busting");
|
||||
let no_hash = vec![NoHashCategory::FileExtentions(vec!["wasm"])];
|
||||
|
||||
let config = BusterBuilder::default()
|
||||
.source("../../static/cache/")
|
||||
.result("./../../assets")
|
||||
.no_hash(no_hash)
|
||||
.follow_links(true)
|
||||
.build()
|
||||
.unwrap();
|
||||
|
||||
config.process().unwrap();
|
||||
}
|
||||
|
||||
fn process_file_map() {
|
||||
let contents = fs::read_to_string(CACHE_BUSTER_DATA_FILE).unwrap();
|
||||
let files: FileMap = serde_json::from_str(&contents).unwrap();
|
||||
let mut map = HashMap::with_capacity(files.map.len());
|
||||
for (k, v) in files.map.iter() {
|
||||
map.insert(k.strip_prefix("../.").unwrap().to_owned(),
|
||||
v.strip_prefix("./../.").unwrap().to_owned()
|
||||
);
|
||||
}
|
||||
|
||||
let new_filemap = FileMap{
|
||||
map,
|
||||
base_dir: files.base_dir.strip_prefix("./../.").unwrap().to_owned(),
|
||||
};
|
||||
|
||||
let dest = Path::new("../../").join(CACHE_BUSTER_DATA_FILE);
|
||||
fs::write(&dest, serde_json::to_string(&new_filemap).unwrap()).unwrap();
|
||||
}
|
Loading…
Reference in a new issue