Compare commits

..

7 Commits

121 changed files with 684 additions and 15259 deletions

View File

@ -1,4 +1,4 @@
**/target/
/target
tarpaulin-report.html
.env
cobertura.xml

View File

@ -6,8 +6,6 @@ on:
push:
branches:
- master
- "*"
- '!gh-pages'
jobs:
fmt:

View File

@ -6,8 +6,6 @@ on:
push:
branches:
- master
- "*"
- '!gh-pages'
jobs:
build_and_test:
@ -20,22 +18,6 @@ jobs:
name: ${{ matrix.version }} - x86_64-unknown-linux-gnu
runs-on: ubuntu-latest
services:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: password
POSTGRES_USER: postgres
POSTGRES_DB: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v2
- name: ⚡ Cache
@ -47,14 +29,6 @@ jobs:
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- uses: actions/setup-node@v2
with:
node-version: "16.x"
- uses: actions/setup-node@v2
with:
node-version: "16.x"
- name: Install ${{ matrix.version }}
uses: actions-rs/toolchain@v1
with:
@ -62,28 +36,13 @@ jobs:
profile: minimal
override: true
- uses: actions/setup-node@v2
with:
node-version: "16.x"
- name: download deps
run: make dev-env
env:
LPCONDUCTOR_CREDS_USERNAME: "librepages_api"
LPCONDUCTOR_CREDS_PASSWORD: "longrandomlygeneratedpassword"
- name: Apply migrations
run: make migrate
env:
DATABASE_URL: "postgres://postgres:password@localhost:5432/postgres"
- name: Generate coverage file
if: matrix.version == 'stable' && (github.ref == 'refs/heads/master' || github.event_name == 'pull_request')
uses: actions-rs/tarpaulin@v0.1
with:
version: '0.18.2'
args: '-t 1200'
env:
DATABASE_URL: "postgres://postgres:password@localhost:5432/postgres"
# GIT_HASH is dummy value. I guess build.rs is skipped in tarpaulin
# execution so this value is required for preventing meta tests from
# panicking

View File

@ -6,8 +6,6 @@ on:
push:
branches:
- master
- "*"
- "!gh-pages"
jobs:
build_and_test:
@ -15,28 +13,12 @@ jobs:
fail-fast: false
matrix:
version:
#- 1.51.0
- stable
# - nightly
name: ${{ matrix.version }} - x86_64-unknown-linux-gnu
runs-on: ubuntu-latest
services:
postgres:
image: postgres
env:
POSTGRES_PASSWORD: password
POSTGRES_USER: postgres
POSTGRES_DB: postgres
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 5432:5432
steps:
- uses: actions/checkout@v2
- name: ⚡ Cache
@ -49,24 +31,8 @@ jobs:
target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- uses: actions/setup-node@v2
with:
node-version: "16.x"
- name: download deps
run: make dev-env
env:
LPCONDUCTOR_CREDS_USERNAME: "librepages_api"
LPCONDUCTOR_CREDS_PASSWORD: "longrandomlygeneratedpassword"
- name: configure GPG key
if: (github.ref == 'refs/heads/master' || github.event_name == 'push') && github.repository == 'realaravinth/librepages'
run: echo -n "$RELEASE_BOT_GPG_SIGNING_KEY" | gpg --batch --import --pinentry-mode loopback
env:
RELEASE_BOT_GPG_SIGNING_KEY: ${{ secrets.RELEASE_BOT_GPG_SIGNING_KEY }}
- name: Login to DockerHub
if: (github.ref == 'refs/heads/master' || github.event_name == 'push') && github.repository == 'realaravinth/librepages'
if: (github.ref == 'refs/heads/master' || github.event_name == 'push') && github.repository == 'realaravinth/pages'
uses: docker/login-action@v1
with:
username: ${{ secrets.DOCKERHUB_USERNAME }}
@ -79,39 +45,28 @@ jobs:
profile: minimal
override: true
- name: build and apply migrations
run: make migrate
env:
DATABASE_URL: "postgres://postgres:password@localhost:5432/postgres"
- name: build
run: make
- name: run tests
run: make test
env:
DATABASE_URL: "postgres://postgres:password@localhost:5432/postgres"
- name: make docker images
- name: build docker images
if: matrix.version == 'stable'
run: make docker
- name: publish docker images
if: (github.ref == 'refs/heads/master' && github.event_name == 'push') && github.repository == 'realaravinth/librepages'
if: matrix.version == 'stable' && (github.ref == 'refs/heads/master' || github.event_name == 'push') && github.repository == 'realaravinth/pages'
run: make docker-publish
- name: publish bins
if: (github.ref == 'refs/heads/master' && github.event_name == 'push') && github.repository == 'realaravinth/librepages'
run: ./scripts/bin-publish.sh publish master latest $DUMBSERVE_PASSWORD
env:
DUMBSERVE_PASSWORD: ${{ secrets.DUMBSERVE_PASSWORD }}
GPG_PASSWORD: ${{ secrets.GPG_PASSWORD }}
- name: generate documentation
if: matrix.version == 'stable' && (github.repository == 'realaravinth/librepages')
if: matrix.version == 'stable' && (github.ref == 'refs/heads/master' || github.event_name == 'push') && github.repository == 'realaravinth/pages'
run: make doc
env:
GIT_HASH: 8e77345f1597e40c2e266cb4e6dee74888918a61 # dummy value
DATABASE_URL: "postgres://postgres:password@localhost:5432/postgres"
- name: Deploy to GitHub Pages
if: matrix.version == 'stable' && (github.repository == 'realaravinth/librepages')
if: matrix.version == 'stable' && (github.ref == 'refs/heads/master' || github.event_name == 'push') && github.repository == 'realaravinth/pages'
uses: JamesIves/github-pages-deploy-action@3.7.1
with:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

8
.gitignore vendored
View File

@ -1,9 +1 @@
/target
tarpaulin-report.html
.env
.env.local
src/cache_buster_data.json
utils/cache-bust/src/cache_buster_data.json
node_modules/
static/cache/css/
assets/

View File

@ -1,82 +0,0 @@
pipeline:
backend:
image: rust
environment:
- DATABASE_URL=postgres://postgres:password@database:5432/postgres
commands:
- curl -fsSL https://deb.nodesource.com/setup_16.x | bash - &&\
- apt update && apt-get -y --no-install-recommends install nodejs tar gpg curl wget
- rustup component add rustfmt
- rustup component add clippy
# rewrite conducotr configuration
- sed -i 's%url = "http:\/\/localhost:5000"%url = "http:\/\/librepages-conductor:5000"%' config/default.toml
- make dev-env
- make migrate
- make lint
- make test
- make release
build_docker_img:
image: plugins/docker
when:
event: [pull_request]
settings:
dry_run: true
repo: realaravinth/librepages
tags: latest
build_and_publish_docker_img:
image: plugins/docker
when:
event: [push, tag, deployment]
settings:
username: realaravinth
password:
from_secret: DOCKER_TOKEN
repo: realaravinth/librepages
tags: latest
# build_publisher_docker_img:
# image: plugins/docker
# when:
# event: [push, tag, deployment]
# settings:
# dry_run: true
# dockerfile: scripts/publish-bins-docker
# purge: false
# repo: realaravinth/librepages-publisher
# tags: latest
#
publish_bins:
image: rust
when:
event: [push, tag, deployment]
commands:
- apt update
- apt-get -y --no-install-recommends install gpg tar curl wget
- echo -n "$RELEASE_BOT_GPG_SIGNING_KEY" | gpg --batch --import --pinentry-mode loopback
- scripts/bin-publish.sh publish master latest $DUMBSERVE_PASSWORD
secrets: [RELEASE_BOT_GPG_SIGNING_KEY, DUMBSERVE_PASSWORD, GPG_PASSWORD]
services:
database:
image: postgres
environment:
- POSTGRES_PASSWORD=password
librepages-conductor:
image: realaravinth/librepages-conductor
command: conductor serve
environment:
- LPCONDUCTOR_SERVER__PROXY_HAS_TLS=false
- LPCONDUCTOR_DEBUG=false
- LPCONDUCTOR_CONDUCTOR=dummy
- LPCONDUCTOR_SERVER_URL_PREFIX=""
- LPCONDUCTOR_SERVER_DOMAIN="librepages.test"
- LPCONDUCTOR_SERVER_IP=0.0.0.0
- LPCONDUCTOR_SERVER_PROXY_HAS_TLS=false
- LPCONDUCTOR_SERVER_PORT=7000
- LPCONDUCTOR_SOURCE_CODE=https://example.org
- LPCONDUCTOR_CREDS_USERNAME="librepages_api"
- LPCONDUCTOR_CREDS_PASSWORD="longrandomlygeneratedpassword"
- PORT=5000

2654
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@ -1,12 +1,12 @@
[package]
name = "librepages"
name = "pages"
version = "0.1.0"
edition = "2021"
build = "build.rs"
homepage = "https://git.batsense.net/LibrePages/librepages"
repository = "https://git.batsense.net/LibrePages/librepages"
documentation = "https://git.batsense.net/LibrePages/librepages"
readme = "https://git.batsense.net/LibrePages/librepages/blob/master/README.md"
homepage = "https://github.com/realaravinth/pages"
repository = "https://github.com/realaravinth/pages"
documentation = "https://github.con/realaravinth/pages"
readme = "https://github.com/realaravinth/pages/blob/master/README.md"
license = "AGPLv3 or later version"
authors = ["realaravinth <realaravinth@batsense.net>"]
@ -15,66 +15,15 @@ authors = ["realaravinth <realaravinth@batsense.net>"]
[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 = ["runtime-actix-rustls", "postgres", "time", "offline", "json", "uuid"] }
clap = { version = "3.2.20", features = ["derive"]}
libconfig = { version = "0.1.0", git = "https://git.batsense.net/librepages/libconfig" }
libconductor = { version = "0.1.0", git = "https://git.batsense.net/librepages/conductor/" }
config = "0.13"
git2 = "0.14.2"
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"
actix-rt = "2.6.0"
config = "0.11"
derive_more = "0.99"
git2 = "0.14.2"
lazy_static = "1.4"
log = "0.4"
my-codegen = {package = "actix-web-codegen", git ="https://github.com/realaravinth/actix-web"}
num_cpus = "1.13"
tokio = { version = "1", features=["sync"]}
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 = ["json"] }
sha2 = "0.10.6"
hmac = "0.12.1"
hex= "0.4.3"
[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"]
pretty_env_logger = "0.4"
serde = { version = "1", features = ["derive"]}
serde_json = "1"
url = "2.2"

View File

@ -1,29 +1,20 @@
FROM node:16.9.1 as frontend
COPY package.json package-lock.json /src/
WORKDIR /src
RUN npm install
COPY . .
RUN npm run sass
FROM rust:slim as rust
WORKDIR /src
RUN apt-get update && apt-get install -y git pkg-config libssl-dev make
RUN apt-get update && apt-get install -y git pkg-config libssl-dev
RUN mkdir src && echo "fn main() {}" > src/main.rs
COPY Cargo.toml .
RUN sed -i '/.*build.rs.*/d' Cargo.toml
COPY Cargo.lock .
RUN cargo build --release || true
COPY --from=frontend /src/static/ /src/static/
RUN cargo build --release
COPY . /src
RUN cd utils/cache-bust && cargo run
RUN cargo build --release
FROM debian:bullseye-slim
#RUN useradd -ms /bin/bash -u 1000 librepages
#RUN mkdir -p /var/www/librepages && chown librepages /var/www/librepages
#RUN useradd -ms /bin/bash -u 1000 pages
#RUN mkdir -p /var/www/pages && chown pages /var/www/pages
RUN apt-get update && apt-get install -y ca-certificates
COPY scripts/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh
COPY --from=rust /src/target/release/librepages /usr/local/bin/
COPY --from=rust /src/target/release/pages /usr/local/bin/
ENTRYPOINT ["/usr/local/bin/entrypoint.sh"]

View File

@ -1,71 +1,39 @@
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
cargo build
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/librepages:master \
-t realaravinth/librepages:latest \
-t realaravinth/librepages:0.1.0 .
docker build -t realaravinth/pages:master -t realaravinth/pages:latest .
docker-publish: docker ## Build and publish docker images
docker push realaravinth/librepages:master
docker push realaravinth/librepages:latest
docker push realaravinth/librepages:0.1.0
docker push realaravinth/pages:master
docker push realaravinth/pages:latest
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
DATABASE_URL=${DATABASE_URL} 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
cargo run
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

View File

@ -4,9 +4,15 @@
**Auto-deploy static websites from git repositories**
</p>
[![status-badge](https://ci.batsense.net/api/badges/LibrePages/librepages/status.svg)](https://ci.batsense.net/LibrePages/librepages)
`service-provider` branch contains changes and features that are more
suited for use by folks that host large number of websites like GitHub
Pages, etc.
</p>
[![Build](https://github.com/realaravinth/pages/actions/workflows/linux.yml/badge.svg)](https://github.com/realaravinth/pages/actions/workflows/linux.yml)
[![dependency status](https://deps.rs/repo/github/realaravinth/pages/status.svg)](https://deps.rs/repo/github/realaravinth/pages)
[![codecov](https://codecov.io/gh/realaravinth/pages/branch/master/graph/badge.svg)](https://codecov.io/gh/realaravinth/pages)
</div>

View File

@ -18,7 +18,7 @@ use std::process::Command;
fn main() {
let output = Command::new("git")
.args(["rev-parse", "HEAD"])
.args(&["rev-parse", "HEAD"])
.output()
.expect("error in git command, is git installed?");
let git_hash = String::from_utf8(output.stdout).unwrap();

View File

@ -1,40 +1,16 @@
debug = true
allow_registration = true
# source code of your copy of pages server.
source_code = "https://git.batsense.net/LibrePages/pages"
support_email = "support@librepages.example.org"
conductors = [
{ username = "librepages_api", api_key = "longrandomlygeneratedpassword", url = "http://localhost:5000"}
log = "info" # possible values: "info", "warn", "trace", "error", "debug"
source_code = "https://github.com/realaravinth/pages"
pages = [
{ branch = "gh-pages", repo = "https://github.com/mCaptcha/website/", path ="/tmp/pages/mcaptcha/website", secret = "faee1b650ac586068a54cb160bd6353c5e16be7c64b49113fe57726e5393" },
]
[server]
# The port at which you want Pages to listen to
# The port at which you want authentication to listen to
# takes a number, choose from 1000-10000 if you dont know what you are doing
port = 7000
#IP address. Enter 0.0.0.0 to listen on all availale addresses
ip= "0.0.0.0"
# 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
# enter your hostname, eg: example.com
domain = "localhost"
cookie_secret = "94b2b2732626fdb7736229a7c777cb451e6304c147c4549f30"
[page]
base_path = "/tmp/librepages-defualt-config/"
base_domain = "librepages.test" # domain where customer pages will be deployed.
[database]
# This section deals with the database location and how to access it
# Please note that at the moment, we have support for only postgresqa.
# Example, if you are Batman, your config would be:
# hostname = "batcave.org"
# port = "5432"
# username = "batman"
# password = "somereallycomplicatedBatmanpassword"
hostname = "localhost"
port = "5432"
username = "postgres"
password = "password"
name = "postgres"
pool = 4
database_type="postgres" # "postgres"
proxy_has_tls = false
#workers = 2

View File

@ -1,21 +0,0 @@
server {
server_name HOSTNAME;
error_log /var/log/nginx/error.log;
listen 443 ssl http2;
location / {
proxy_pass http://127.0.0.1:12081;
proxy_set_header Host $host;
}
}
server {
listen 80;
listen [::]:80;
server_name HOSTNAME; # change hostname
location / {
return 301 https://$host$request_uri;
}
}

View File

@ -1,7 +0,0 @@
## Contrib
[Loïc Dachary](https://dachary.org) from the [Enough
Community](https://enough.community) has kindly created an Ansible an
Ansible Playbook for Pages deployment. Please see
[here](https://enough-community.readthedocs.io/en/latest/services/pages.html)
for more information.

View File

@ -1,37 +0,0 @@
# Configuration
Pages is highly configurable. Configuration is applied/merged in the
following order:
1. `/etc/static-pages/config.toml`
2. `./config/default.toml`
3. path to configuration file passed in via `PAGES_CONFIG`
4. environment variables.
So if `/etc/static-pages/config.toml` says Pages must listen on port
`4000` and environment variable or `PAGES_CONFIG` file say it should
listen on port `5000`, Pages will listen on `5000`.
## Setup
### Environment variables
Setting environment variables are optional. The configuration files have
all the necessary parameters listed. By setting environment variables,
you will be overriding the values set in the configuration files.
### General
| Name | Value |
| --------------------- | ---------------------------------------- |
| `PAGES_CONFIG` | Path to configuration file |
| `PAGES__SOURCE__CODE` | Link to the source code of this instance |
#### Server
| Name | Value |
| --------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `PAGES__SERVER__PORT` | The port on which you want Pages to listen to |
| `PORT`(overrides `PAGES__SERVER__PORT`) | The port on which you want Pages to listen to |
| `PAGES__SERVER__IP` | The IP address on which you want Pages to listen to |
| `PAGES__SERVER__WORKERS` | 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. |

View File

@ -1,119 +0,0 @@
# Bare metal:
The process is tedious, most of this will be automated with a script in
the future.
## 1. Create new user for running `librepages`:
```bash
sudo useradd -b /srv -m -s /usr/bin/zsh librepages
```
## 2. Install Runtime dependencies
1. [Nginx](https://packages.debian.org/bullseye/nginx)
On Debian-based systems, run:
```bash
sudo apt install nginx
```
## 3. Build `librepages`
### i. Install Build Dependencies
To build `librepages`, you need the following dependencies:
1. [Git](https://packages.debian.org/bullseye/git)
2. [pkg-config](https://packages.debian.org/bullseye/pkg-config)
3. [GNU make](https://packages.debian.org/bullseye/make)
4. [libssl-dev](https://packages.debian.org/bullseye/libssl-dev)
5. Rust
To install all dependencies **except Rust** on Debian boxes, run:
```bash
sudo apt-get install -y git pkg-config libssl-dev
```
### ii. Install Rust
Install Rust using [rustup](https://rustup.rs/).
rustup is the official Rust installation tool. It enables installation
of multiple versions of `rustc` for different architectures across
multiple release channels(stable, nightly, etc.).
Rust undergoes [six-week release
cycles](https://doc.rust-lang.org/book/appendix-05-editions.html#appendix-e---editions)
and some of the dependencies that are used in this program have often
relied on cutting edge features of the Rust compiler. OS Distribution
packaging teams don't often track the latest releases. For this reason,
we encourage managing your Rust installation with `rustup`.
**rustup is the officially supported Rust installation method of this
project.**
```bash
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
```
### iii. Build
```bash
$ make release
```
## 5. Install
Install binary and copy demo configuration file into default configuration
lookup path(`/etc/static-pages/config.toml`)
```bash
sudo cp ./target/release/librepages /usr/local/bin/ && \
sudo mkdir /etc/static-pages && \
sudo cp config/default.toml /etc/static-pages/config.toml
```
## 4. Systemd service configuration:
### i. Copy the following to `/etc/systemd/system/librepages.service`:
```systemd
[Unit]
Description=librepages: Auto-deploy static websites from git repositories
[Service]
Type=simple
User=librepages
ExecStart=/usr/local/bin/librepages
Restart=on-failure
RestartSec=1
MemoryDenyWriteExecute=true
NoNewPrivileges=true
Environment="RUST_LOG=info"
[Unit]
Wants=network-online.target
Wants=network-online.target
After=syslog.target
[Install]
WantedBy=multi-user.target
```
### ii. Enable and start service:
```bash
sudo systemctl daemon-reload && \
sudo systemctl enable librepages && \ # Auto startup during boot
sudo systemctl start librepages
```
## 5. Optionally configure Nginx to reverse proxy requests to LibrePages
**NOTE: This sections includes instructions to reverse proxy requests to
LibrePages API, not the websites managed by librepages.**
See [here](../../config/librepages-nginx-config) for sample Nginx configuration.

View File

@ -1,7 +0,0 @@
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 SERIAL PRIMARY KEY NOT NULL
);

View File

@ -1,13 +0,0 @@
CREATE TABLE IF NOT EXISTS librepages_sites (
site_secret VARCHAR(32) NOT NULL UNIQUE,
repo_url VARCHAR(3000) NOT NULL,
branch TEXT NOT NULL,
hostname VARCHAR(3000) NOT NULL UNIQUE,
pub_id uuid NOT NULL UNIQUE,
ID SERIAL PRIMARY KEY NOT NULL,
deleted BOOLEAN DEFAULT FALSE,
owned_by INTEGER NOT NULL references librepages_users(ID) ON DELETE CASCADE
);
CREATE UNIQUE INDEX librepages_sites_site_secret ON librepages_sites(site_secret);
CREATE UNIQUE INDEX librepages_sites_site_pub_id ON librepages_sites(pub_id);

View File

@ -1,14 +0,0 @@
CREATE TABLE IF NOT EXISTS librepages_deploy_event_type (
name VARCHAR(30) NOT NULL UNIQUE,
ID SERIAL PRIMARY KEY NOT NULL
);
CREATE UNIQUE INDEX librepages_deploy_event_name_index ON librepages_deploy_event_type(name);
CREATE TABLE IF NOT EXISTS librepages_site_deploy_events (
site INTEGER NOT NULL references librepages_sites(ID) ON DELETE CASCADE,
event_type INTEGER NOT NULL references librepages_deploy_event_type(ID),
time timestamptz NOT NULL,
pub_id uuid NOT NULL UNIQUE,
ID SERIAL PRIMARY KEY NOT NULL
);

View File

@ -1,15 +0,0 @@
CREATE TABLE IF NOT EXISTS librepages_forgejo_webhooks (
forgejo_webhook_secret VARCHAR(40) NOT NULL UNIQUE,
forgejo_url VARCHAR(3000) NOT NULL,
auth_token VARCHAR(40) NOT NULL UNIQUE,
ID SERIAL PRIMARY KEY NOT NULL,
owned_by INTEGER NOT NULL references librepages_users(ID) ON DELETE CASCADE
);
CREATE UNIQUE INDEX librepages_forgejo_webhook_auth_token_index ON librepages_forgejo_webhooks(auth_token);
CREATE TABLE IF NOT EXISTS librepages_forgejo_webhook_site_mapping (
site_id INTEGER NOT NULL references librepages_sites(ID) ON DELETE CASCADE,
forgejo_webhook_id INTEGER NOT NULL references librepages_forgejo_webhooks(ID) ON DELETE CASCADE,
UNIQUE(site_id, forgejo_webhook_id)
);

385
package-lock.json generated
View File

@ -1,385 +0,0 @@
{
"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"
}
}
}
}

View File

@ -1,25 +0,0 @@
{
"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"
}
}

View File

@ -1,119 +0,0 @@
#!/bin/bash
# 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/>.
# publish.sh: grab bin from docker container, pack, sign and upload
# $2: binary version
# $3: Docker img tag
# $4: dumbserve password
set -xEeuo pipefail
DUMBSERVE_USERNAME=librepages
DUMBSERVE_PASSWORD=$4
DUMBSERVE_HOST="https://$DUMBSERVE_USERNAME:$DUMBSERVE_PASSWORD@dl.librepages.org"
NAME=librebages
KEY=67880CA5F4BC99BF247330E2DA576B07BC323961
TMP_DIR=$(mktemp -d)
FILENAME="$NAME-$2-linux-amd64"
TARBALL=$FILENAME.tar.gz
TARGET_DIR="$TMP_DIR/$FILENAME/"
mkdir -p $TARGET_DIR
DOCKER_IMG="realaravinth/librepages:$3"
get_bin(){
echo "[*] Grabbing binary"
#container_id=$(docker create $DOCKER_IMG)
#docker cp $container_id:/usr/local/bin/pages $TARGET_DIR/
#docker rm -v $container_id
cp target/release/librepages $TARGET_DIR
}
copy() {
echo "[*] Copying dist assets"
cp README.md $TARGET_DIR
cp LICENSE.md $TARGET_DIR
mkdir $TARGET_DIR/docs
cp docs/CONFIGURATION.md $TARGET_DIR/docs
cp -r docs/installation/ $TARGET_DIR/docs
get_bin
}
pack() {
echo "[*] Creating dist tarball"
pushd $TMP_DIR
tar -cvzf $TARBALL $FILENAME
popd
}
checksum() {
echo "[*] Generating dist tarball checksum"
pushd $TMP_DIR
sha256sum $TARBALL > $TARBALL.sha256
popd
}
sign() {
echo "[*] Signing dist tarball checksum"
pushd $TMP_DIR
export GPG_TTY=$(tty)
gpg --verbose \
--pinentry-mode loopback \
--batch --yes \
--passphrase $GPG_PASSWORD \
--local-user $KEY \
--output $TARBALL.asc \
--sign --detach \
--armor $TARBALL
popd
}
delete_dir() {
curl --location --request DELETE "$DUMBSERVE_HOST/api/v1/files/delete" \
--header 'Content-Type: application/json' \
--data-raw "{
\"path\": \"$1\"
}"
}
upload_dist() {
upload_dist="librepages/$1"
delete_dir $upload_dist
pushd $TMP_DIR
for file in $TARBALL $TARBALL.asc $TARBALL.sha256
do
curl -v \
-F upload=@$file \
"$DUMBSERVE_HOST/api/v1/files/upload?path=$upload_dist/"
done
popd
}
publish() {
copy
pack
checksum
sign
upload_dist $2
}
$1 $@

View File

@ -1,23 +0,0 @@
#!/bin/bash
readonly NAME=librepages-conductor
docker rm -f $NAME
docker create --name $NAME -p 5000:5000 \
-e LPCONDUCTOR__SOURCE_CODE="https://git.batsense.net/LibrePages/conductor" \
-e LPCONDUCTOR_SERVER__PROXY_HAS_TLS=false \
-e LPCONDUCTOR_DEBUG="false" \
-e LPCONDUCTOR_CONDUCTOR="dummy" \
-e LPCONDUCTOR_SERVER_URL_PREFIX="" \
-e LPCONDUCTOR_SERVER_DOMAIN="librepages.test" \
-e LPCONDUCTOR_SERVER_IP="0.0.0.0" \
-e LPCONDUCTOR_SERVER_PROXY_HAS_TLS="false" \
-e LPCONDUCTOR_SERVER_PORT=7000 \
-e LPCONDUCTOR_SOURCE_CODE="https://example.org" \
-e LPCONDUCTOR_CREDS_USERNAME=$LPCONDUCTOR_CREDS_USERNAME \
-e LPCONDUCTOR_CREDS_PASSWORD=$LPCONDUCTOR_CREDS_PASSWORD \
-e PORT="5000"\
realaravinth/librepages-conductor conductor serve
docker start $NAME

View File

@ -2,17 +2,11 @@
USER_ID=${LOCAL_USER_ID}
echo $USER_ID
LIBREPAGES_USER=librepages
echo "Starting with UID : $USER_ID"
export HOME=/home/$LIBREPAGES_USER
export HOME=/home/user
#adduser --disabled-password --shell /bin/bash --home $HOME --uid $USER_ID user
#--uid
if id "$1" &>/dev/null; then
echo "User $LIBREPAGES_USER exists"
else
useradd --uid $USER_ID -b /home -m -s /bin/bash $LIBREPAGES_USER
fi
su $LIBREPAGES_USER -c 'librepages'
sudo useradd --uid $USER_ID -b /home -m -s /bin/bash user
su - user
pages

View File

@ -1,14 +0,0 @@
FROM realaravinth/librepages:latest as base
RUN echo foo
FROM debian:bullseye-slim
RUN apt update
RUN apt-get -y --no-install-recommends install gpg tar curl wget
WORKDIR /src
COPY --from=base /usr/local/bin/librepages .
COPY . .
ARG RELEASE_BOT_GPG_SIGNING_KEY
RUN echo -n "$RELEASE_BOT_GPG_SIGNING_KEY"
RUN echo -n "$RELEASE_BOT_GPG_SIGNING_KEY" | gpg --batch --import --pinentry-mode loopback
env GPG_PASSWORD=$GPG_PASSWORD
RUN /src/scripts/bin-publish.sh publish master latest $DUMBSERVE_PASSWORD

View File

@ -1,783 +0,0 @@
{
"db": "PostgreSQL",
"10d30dade86d79210203bdbce4b6db5d2aa446b0f88ca834771ecbbe11be51fb": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "UPDATE librepages_sites SET deleted = true\n WHERE hostname = ($1)\n AND owned_by = ( SELECT ID FROM librepages_users WHERE name = $2);\n "
},
"12391b10cf16a807322c49c9cc7e0a015f26b445beacf4cdd4e7714f36b4adf6": {
"describe": {
"columns": [
{
"name": "site_secret",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "repo_url",
"ordinal": 1,
"type_info": "Varchar"
},
{
"name": "branch",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "hostname",
"ordinal": 3,
"type_info": "Varchar"
},
{
"name": "pub_id",
"ordinal": 4,
"type_info": "Uuid"
}
],
"nullable": [
false,
false,
false,
false,
false
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT site_secret, repo_url, branch, hostname, pub_id\n FROM librepages_sites\n WHERE deleted = false\n AND owned_by = (SELECT ID FROM librepages_users WHERE name = $1 );\n "
},
"14cdc724af64942e93994f97e9eafc8272d15605eff7aab9e5177d01f2bf6118": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text",
"Timestamptz",
"Text",
"Uuid"
]
}
},
"query": "INSERT INTO librepages_site_deploy_events\n (event_type, time, site, pub_id) VALUES (\n (SELECT iD from librepages_deploy_event_type WHERE name = $1),\n $2,\n (SELECT ID from librepages_sites WHERE hostname = $3),\n $4\n );\n "
},
"1be33ea4fe0e6079c88768ff912b824f4b0250193f2d086046c1fd0da125ae0c": {
"describe": {
"columns": [
{
"name": "name",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "password",
"ordinal": 1,
"type_info": "Text"
}
],
"nullable": [
false,
false
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT name, password FROM librepages_users WHERE name = ($1)"
},
"279b5ae27935279b06d2799eef2da6a316324a05d23ba7a729c608c70168c496": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
}
},
"query": "UPDATE librepages_users set name = $1\n WHERE name = $2"
},
"39854fcbfb0247377c6c5ca70c2c0fa7804548848bf56f881eea2f8242e7a09d": {
"describe": {
"columns": [
{
"name": "name",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "time",
"ordinal": 1,
"type_info": "Timestamptz"
},
{
"name": "pub_id",
"ordinal": 2,
"type_info": "Uuid"
}
],
"nullable": [
false,
false,
false
],
"parameters": {
"Left": [
"Text",
"Uuid"
]
}
},
"query": "SELECT\n librepages_deploy_event_type.name,\n librepages_site_deploy_events.time,\n librepages_site_deploy_events.pub_id\n FROM\n librepages_site_deploy_events\n INNER JOIN librepages_deploy_event_type ON\n librepages_deploy_event_type.ID = librepages_site_deploy_events.event_type\n WHERE\n librepages_site_deploy_events.site = (\n SELECT ID FROM librepages_sites WHERE hostname = $1\n )\n AND\n librepages_site_deploy_events.pub_id = $2\n "
},
"3ecc3a4c89b1289368ef9d9c97204330f74138a0da614ef2174c59a687119595": {
"describe": {
"columns": [
{
"name": "forgejo_url",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "auth_token",
"ordinal": 1,
"type_info": "Varchar"
},
{
"name": "forgejo_webhook_secret",
"ordinal": 2,
"type_info": "Varchar"
}
],
"nullable": [
false,
false,
false
],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "SELECT\n forgejo_url, auth_token, forgejo_webhook_secret\n FROM\n librepages_forgejo_webhooks\n WHERE\n auth_token = $1\n AND\n owned_by = (SELECT ID FROM librepages_users WHERE name = $2);\n "
},
"432fe829719ce8110f768b4a611724bb34191ac224d2143ff4c81548da75c103": {
"describe": {
"columns": [
{
"name": "repo_url",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "branch",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "hostname",
"ordinal": 2,
"type_info": "Varchar"
},
{
"name": "owned_by",
"ordinal": 3,
"type_info": "Int4"
},
{
"name": "site_secret",
"ordinal": 4,
"type_info": "Varchar"
}
],
"nullable": [
false,
false,
false,
false,
false
],
"parameters": {
"Left": [
"Uuid",
"Text"
]
}
},
"query": "SELECT repo_url, branch, hostname, owned_by, site_secret\n FROM librepages_sites\n WHERE pub_id = $1\n AND\n owned_by = (SELECT ID from librepages_users WHERE name = $2)\n AND\n deleted = false;\n "
},
"4445ff3226af3b5a24b255c5bb012c99b222cc7bd6dda80f232809ed273fc712": {
"describe": {
"columns": [
{
"name": "repo_url",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "site_secret",
"ordinal": 1,
"type_info": "Varchar"
},
{
"name": "branch",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "hostname",
"ordinal": 3,
"type_info": "Varchar"
},
{
"name": "owned_by",
"ordinal": 4,
"type_info": "Int4"
},
{
"name": "pub_id",
"ordinal": 5,
"type_info": "Uuid"
}
],
"nullable": [
false,
false,
false,
false,
false,
false
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT repo_url, site_secret, branch, hostname, owned_by, pub_id\n FROM librepages_sites\n WHERE repo_url = $1\n AND deleted = false;\n "
},
"4cddf1049783251bfc79090055724e894a2be9451302f7691ce2f4240f1ee3ad": {
"describe": {
"columns": [
{
"name": "id",
"ordinal": 0,
"type_info": "Int4"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT ID FROM librepages_sites WHERE repo_url = $1"
},
"53f3c21c06c8d1c218537dfa9183fd0604aaf28200d8aa12e97db4ac317df39e": {
"describe": {
"columns": [
{
"name": "name",
"ordinal": 0,
"type_info": "Varchar"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Int4"
]
}
},
"query": "SELECT name FROM librepages_users WHERE ID = $1"
},
"54f1ad328c83997d5e80686665d4cfef58d3529d24cb6caaa7ff301479e5d735": {
"describe": {
"columns": [
{
"name": "repo_url",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "branch",
"ordinal": 1,
"type_info": "Text"
},
{
"name": "hostname",
"ordinal": 2,
"type_info": "Varchar"
},
{
"name": "owned_by",
"ordinal": 3,
"type_info": "Int4"
},
{
"name": "pub_id",
"ordinal": 4,
"type_info": "Uuid"
}
],
"nullable": [
false,
false,
false,
false,
false
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT repo_url, branch, hostname, owned_by, pub_id\n FROM librepages_sites\n WHERE site_secret = $1\n AND deleted = false;\n "
},
"5c5d774bde06c0ab83c3616a56a28f12dfd9c546cbaac9f246d3b350c587823e": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "DELETE FROM librepages_users WHERE name = ($1)"
},
"65f6181364cd8c6ed4eae3f62b5ae771a27e8da6e698c235ca77d4cec784d04b": {
"describe": {
"columns": [
{
"name": "site_secret",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "repo_url",
"ordinal": 1,
"type_info": "Varchar"
},
{
"name": "branch",
"ordinal": 2,
"type_info": "Text"
},
{
"name": "hostname",
"ordinal": 3,
"type_info": "Varchar"
},
{
"name": "pub_id",
"ordinal": 4,
"type_info": "Uuid"
}
],
"nullable": [
false,
false,
false,
false,
false
],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "SELECT site_secret, repo_url, branch, hostname, pub_id\n FROM librepages_sites\n WHERE deleted = false\n AND owned_by = (SELECT ID FROM librepages_users WHERE name = $1 )\n AND hostname = $2;\n "
},
"6a557f851d4f47383b864085093beb0954e79779f21b655978f07e285281e0ac": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Text"
]
}
},
"query": "UPDATE librepages_users set email = $1\n WHERE name = $2"
},
"6db98c6ae90b8eb98ace1a5acfa3c8af2b6ed7d51c6debda12637f5d7b076c15": {
"describe": {
"columns": [
{
"name": "exists",
"ordinal": 0,
"type_info": "Bool"
}
],
"nullable": [
null
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT EXISTS (SELECT 1 from librepages_sites WHERE hostname = $1 AND deleted = false)"
},
"77612c85be99e6de2e4a6e3105ebaeb470d6cc57b2999b673a085da41c035f9e": {
"describe": {
"columns": [
{
"name": "time",
"ordinal": 0,
"type_info": "Timestamptz"
},
{
"name": "pub_id",
"ordinal": 1,
"type_info": "Uuid"
}
],
"nullable": [
false,
false
],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "SELECT\n time,\n pub_id\n FROM\n librepages_site_deploy_events\n WHERE\n site = (SELECT ID FROM librepages_sites WHERE hostname = $1)\n AND\n event_type = (SELECT ID FROM librepages_deploy_event_type WHERE name = $2)\n AND\n time = (\n SELECT MAX(time) \n FROM\n librepages_site_deploy_events\n WHERE\n site = (SELECT ID FROM librepages_sites WHERE hostname = $1)\n )\n "
},
"8735b654bc261571e6a5908d55a8217474c76bdff7f3cbcc71500a0fe13249db": {
"describe": {
"columns": [
{
"name": "exists",
"ordinal": 0,
"type_info": "Bool"
}
],
"nullable": [
null
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT EXISTS (SELECT 1 from librepages_users WHERE email = $1)"
},
"8bf4e01b8c38d035fe6bdbfbe8ad9cb35e3fc2fd11107bae92880d157ed11379": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Varchar",
"Text"
]
}
},
"query": "INSERT INTO librepages_forgejo_webhooks\n (forgejo_url , auth_token, forgejo_webhook_secret, owned_by) VALUES ($1, $2, $3, \n (SELECT ID FROM librepages_users WHERE name = $4)\n )"
},
"90907d6cb4ca3b485f7b583584fb5821a950362679d061e490545c76634c211e": {
"describe": {
"columns": [
{
"name": "exists",
"ordinal": 0,
"type_info": "Bool"
}
],
"nullable": [
null
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT EXISTS (SELECT 1 from librepages_sites WHERE repo_url = $1)"
},
"924e756de5544cece865a10a7e136ecc6126e3a603947264cc7899387c18c819": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text",
"Text"
]
}
},
"query": "UPDATE librepages_users set password = $1\n WHERE name = $2"
},
"9710a01bc4c5c5cda2db27d14baca3d7a6ceffa66c7d539da6fda7947c222e71": {
"describe": {
"columns": [
{
"name": "forgejo_url",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "auth_token",
"ordinal": 1,
"type_info": "Varchar"
},
{
"name": "forgejo_webhook_secret",
"ordinal": 2,
"type_info": "Varchar"
}
],
"nullable": [
false,
false,
false
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT\n forgejo_url, auth_token, forgejo_webhook_secret\n FROM\n librepages_forgejo_webhooks\n WHERE\n owned_by = (SELECT ID FROM librepages_users WHERE name = $1);\n "
},
"a6284ede1dbf340942dd97afb75865ba0a41009a145254117b03002bd9afa588": {
"describe": {
"columns": [
{
"name": "forgejo_url",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "auth_token",
"ordinal": 1,
"type_info": "Varchar"
},
{
"name": "forgejo_webhook_secret",
"ordinal": 2,
"type_info": "Varchar"
}
],
"nullable": [
false,
false,
false
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT forgejo_url, auth_token, forgejo_webhook_secret\n FROM librepages_forgejo_webhooks\n WHERE auth_token = $1\n "
},
"b48c77db6e663d97df44bf9ec2ee92fd3e02f2dcbcdbd1d491e09fab2da68494": {
"describe": {
"columns": [
{
"name": "name",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "password",
"ordinal": 1,
"type_info": "Text"
}
],
"nullable": [
false,
false
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT name, password FROM librepages_users WHERE email = ($1)"
},
"b7e51e976a4a80a78df8dbfed1f195af212023d00faee88ab2d09326896bd653": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Text",
"Int4",
"Text"
]
}
},
"query": "INSERT INTO librepages_forgejo_webhook_site_mapping\n (site_id, forgejo_webhook_id) VALUES (\n (SELECT ID FROM librepages_sites WHERE repo_url = $1 AND ID = $2),\n (SELECT ID FROM librepages_forgejo_webhooks WHERE auth_token = $3)\n ) ON CONFLICT (site_id, forgejo_webhook_id) DO NOTHING;"
},
"b8b1b3c5fa205b071f577b2ce9993ddfc7c99ada26aea48aa1c201c8c3c7fcf6": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Varchar",
"Text",
"Varchar",
"Uuid",
"Text"
]
}
},
"query": "\n INSERT INTO librepages_sites\n (site_secret, repo_url, branch, hostname, pub_id, owned_by)\n VALUES ($1, $2, $3, $4, $5, ( SELECT ID FROM librepages_users WHERE name = $6 ));\n "
},
"bdd4d2a1b0b97ebf8ed61cfd120b40146fbf3ea9afb5cd0e03c9d29860b6a26b": {
"describe": {
"columns": [
{
"name": "exists",
"ordinal": 0,
"type_info": "Bool"
}
],
"nullable": [
null
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT EXISTS (SELECT 1 from librepages_users WHERE name = $1)"
},
"ced69a08729ffb906e8971dbdce6a8d4197bc9bb8ccd7c58b3a88eb7be73fc2e": {
"describe": {
"columns": [
{
"name": "email",
"ordinal": 0,
"type_info": "Varchar"
}
],
"nullable": [
false
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT email FROM librepages_users WHERE name = $1"
},
"d2327c1bcb40e18518c2112413a19a9b26eb0f54f83c53e968c9752d70c8dd4e": {
"describe": {
"columns": [
{
"name": "name",
"ordinal": 0,
"type_info": "Varchar"
},
{
"name": "time",
"ordinal": 1,
"type_info": "Timestamptz"
},
{
"name": "pub_id",
"ordinal": 2,
"type_info": "Uuid"
}
],
"nullable": [
false,
false,
false
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT\n librepages_deploy_event_type.name,\n librepages_site_deploy_events.time,\n librepages_site_deploy_events.pub_id\n FROM\n librepages_site_deploy_events\n INNER JOIN librepages_deploy_event_type ON\n librepages_deploy_event_type.ID = librepages_site_deploy_events.event_type\n WHERE\n librepages_site_deploy_events.site = (\n SELECT ID FROM librepages_sites WHERE hostname = $1\n );\n "
},
"e4adf1bc9175eeb9d61b495653bb452039cc38818c8792acdc6a1c732b6f4554": {
"describe": {
"columns": [
{
"name": "exists",
"ordinal": 0,
"type_info": "Bool"
}
],
"nullable": [
null
],
"parameters": {
"Left": [
"Text"
]
}
},
"query": "SELECT EXISTS (SELECT 1 from librepages_deploy_event_type WHERE name = $1)"
},
"f651da8f411b7977cb87dd8d4bd5d167661d7ef1d865747e76219453d386d593": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar"
]
}
},
"query": "INSERT INTO librepages_deploy_event_type\n (name) VALUES ($1) ON CONFLICT (name) DO NOTHING;"
},
"faa4170a309f19a4abf1ca3f8dd3c0526945aa00f028ebf8bd7063825d448f5b": {
"describe": {
"columns": [],
"nullable": [],
"parameters": {
"Left": [
"Varchar",
"Text",
"Varchar"
]
}
},
"query": "INSERT INTO librepages_users\n (name , password, email) VALUES ($1, $2, $3)"
}
}

View File

@ -1,17 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
pub mod v1;

View File

@ -1,144 +0,0 @@
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use 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())
}

View File

@ -1,296 +0,0 @@
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::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);
}

View File

@ -1,74 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use 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()
}

View File

@ -1,282 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use actix_identity::Identity;
use actix_web::{web, HttpRequest, HttpResponse, Responder};
use serde::{Deserialize, Serialize};
use tracing::info;
use url::Url;
use super::get_auth_middleware;
use crate::{errors::*, AppCtx};
pub mod routes {
use crate::ctx::Ctx;
pub struct Forgejo {
pub add_webhook: &'static str,
pub view_webhook: &'static str,
pub list_webhooks: &'static str,
pub webhook: &'static str,
}
impl Forgejo {
pub const fn new() -> Self {
Self {
add_webhook: "/api/v1/forgejo/webhook/add",
list_webhooks: "/api/v1/forgejo/webhook/add",
view_webhook: "/api/v1/forgejo/webhook/view/{auth_token}",
webhook: "/api/v1/forgejo/webhook/event/new",
}
}
pub fn get_view(&self, auth_token: &str) -> String {
self.view_webhook.replace("{auth_token}", auth_token)
}
pub fn get_webhook_url(&self, ctx: &Ctx, auth_token: &str) -> String {
format!(
"https://{}{}?auth={auth_token}",
&ctx.settings.server.domain, self.webhook
)
}
}
}
#[derive(Serialize, Deserialize)]
pub struct AddWebhook {
pub forgejo_url: Url,
}
#[actix_web_codegen_const_routes::post(
path = "crate::V1_API_ROUTES.forgejo.add_webhook",
wrap = "get_auth_middleware()"
)]
#[tracing::instrument(name = "Add webhook" skip(id, ctx, payload))]
async fn add_webhook(
ctx: AppCtx,
id: Identity,
payload: web::Json<AddWebhook>,
) -> ServiceResult<impl Responder> {
info!(
"Adding webhook for Forgejo instance: {}",
payload.forgejo_url.as_str()
);
let owner = id.identity().unwrap();
let payload = payload.into_inner();
let hook = ctx.db.new_webhook(payload.forgejo_url, &owner).await?;
Ok(HttpResponse::Ok().json(hook))
}
#[actix_web_codegen_const_routes::get(
path = "crate::V1_API_ROUTES.forgejo.list_webhooks",
wrap = "get_auth_middleware()"
)]
#[tracing::instrument(name = "Delete webhook" skip(id, ctx))]
async fn list_webhooks(ctx: AppCtx, id: Identity) -> ServiceResult<impl Responder> {
let owner = id.identity().unwrap();
info!("Getting all webhooks created by {}", owner);
let hooks = ctx.db.list_all_webhooks_with_owner(&owner).await?;
Ok(HttpResponse::Ok().json(hooks))
}
#[actix_web_codegen_const_routes::get(
path = "crate::V1_API_ROUTES.forgejo.view_webhook",
wrap = "get_auth_middleware()"
)]
#[tracing::instrument(name = "Delete webhook" skip(id, ctx, path))]
async fn view_webhook(
ctx: AppCtx,
id: Identity,
path: web::Path<String>,
) -> ServiceResult<impl Responder> {
let path = path.into_inner();
let owner = id.identity().unwrap();
info!("Gitting webhook webhook for Forgejo instance: {}", path,);
let hook = ctx.db.get_webhook_with_owner(&path, &owner).await?;
Ok(HttpResponse::Ok().json(hook))
}
#[derive(Serialize, Deserialize)]
struct Auth {
auth: String,
}
#[actix_web_codegen_const_routes::post(path = "crate::V1_API_ROUTES.forgejo.webhook")]
#[tracing::instrument(name = "Update ", skip(body, ctx, req, q))]
async fn webhook(
ctx: AppCtx,
body: web::Bytes,
req: HttpRequest,
q: web::Query<Auth>,
) -> ServiceResult<impl Responder> {
ctx.process_webhook(&body, &req, &q.auth).await?;
Ok(HttpResponse::Ok())
}
pub fn services(cfg: &mut web::ServiceConfig) {
cfg.service(add_webhook);
cfg.service(view_webhook);
cfg.service(list_webhooks);
cfg.service(webhook);
}
#[cfg(test)]
mod tests {
use actix_web::{error::ResponseError, http::StatusCode, test};
use hmac::Mac;
use crate::ctx::api::v1::forgejo::{HmacSha256, WebhookPayload};
use crate::db::ForgejoWebhook;
use crate::tests;
use crate::*;
use super::*;
#[actix_rt::test]
async fn test_api_forgejo_webhook() {
const NAME: &str = "apiforgejowebhookuser";
const PASSWORD: &str = "longpasswordasdfa2";
const EMAIL: &str = "apiforgejowebhookuser@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 cookies = get_cookie!(signin_resp);
let app = get_app!(ctx).await;
let payload = AddWebhook {
forgejo_url: Url::parse("https://git.batnsense.net").unwrap(),
};
let add_webhook_resp = test::call_service(
&app,
post_request!(&payload, V1_API_ROUTES.forgejo.add_webhook)
.cookie(cookies.clone())
.to_request(),
)
.await;
check_status!(add_webhook_resp, StatusCode::OK);
let response: ForgejoWebhook = actix_web::test::read_body_json(add_webhook_resp).await;
assert_eq!(response.forgejo_url, payload.forgejo_url);
let view_webhook_resp = get_request!(
&app,
&V1_API_ROUTES.forgejo.get_view(&response.auth_token),
cookies.clone()
);
check_status!(view_webhook_resp, StatusCode::OK);
let hook: ForgejoWebhook = actix_web::test::read_body_json(view_webhook_resp).await;
assert_eq!(hook, response);
let list_all_webhooks_resp =
get_request!(&app, V1_API_ROUTES.forgejo.list_webhooks, cookies.clone());
check_status!(list_all_webhooks_resp, StatusCode::OK);
let hooks: Vec<ForgejoWebhook> =
actix_web::test::read_body_json(list_all_webhooks_resp).await;
assert_eq!(vec![hook.clone()], hooks);
let webhook_url = format!("{}?auth={}", V1_API_ROUTES.forgejo.webhook, hook.auth_token);
// test webhook
let mut webhook_payload = WebhookPayload::default();
webhook_payload.reference = format!("refs/origin/{}", page.branch);
webhook_payload.repository.html_url = page.repo;
let body = serde_json::to_string(&webhook_payload).unwrap();
let body = body.as_bytes();
let mut mac = HmacSha256::new_from_slice(hook.forgejo_webhook_secret.as_bytes())
.expect("HMAC can take key of any size");
mac.update(body);
let res = mac.finalize();
let sig = res.into_bytes();
let sig = hex::encode(&sig[..]);
let post_to_webhook_resp = test::call_service(
&app,
post_request!(&webhook_payload, &webhook_url)
.insert_header(("X-Gitea-Delivery", "foobar213randomuuid"))
.insert_header(("X-Gitea-Signature", sig.clone()))
.insert_header(("X-Gitea-Event", "push"))
.cookie(cookies.clone())
.to_request(),
)
.await;
check_status!(post_to_webhook_resp, StatusCode::OK);
// no webhook
let fake_webhook_url = format!(
"{}?auth={}",
V1_API_ROUTES.forgejo.webhook, hook.forgejo_webhook_secret
);
let body = serde_json::to_string(&webhook_payload).unwrap();
let body = body.as_bytes();
let mut mac =
HmacSha256::new_from_slice(b"nosecret").expect("HMAC can take key of any size");
mac.update(body);
let res = mac.finalize();
let fake_sig = res.into_bytes();
let fake_sig = hex::encode(&fake_sig[..]);
let post_to_no_exist_webhook_resp = test::call_service(
&app,
post_request!(&webhook_payload, &fake_webhook_url)
.insert_header(("X-Gitea-Delivery", "foobar213randomuuid"))
.insert_header(("X-Gitea-Signature", fake_sig))
.insert_header(("X-Gitea-Event", "push"))
.cookie(cookies.clone())
.to_request(),
)
.await;
let err = ServiceError::WebhookNotFound;
assert_eq!(post_to_no_exist_webhook_resp.status(), err.status_code());
let resp_err: ErrorToResponse =
actix_web::test::read_body_json(post_to_no_exist_webhook_resp).await;
assert_eq!(resp_err.error, err.to_string());
// no website
let mut webhook_payload = WebhookPayload::default();
webhook_payload.reference = format!("refs/origin/{}", page.branch);
webhook_payload.repository.html_url = "https://no-exist-git.example.org".into();
let body = serde_json::to_string(&webhook_payload).unwrap();
let body = body.as_bytes();
let mut mac = HmacSha256::new_from_slice(hook.forgejo_webhook_secret.as_bytes())
.expect("HMAC can take key of any size");
mac.update(body);
let res = mac.finalize();
let sig = res.into_bytes();
let sig = hex::encode(&sig[..]);
let post_to_no_website_webhook_resp = test::call_service(
&app,
post_request!(&webhook_payload, &webhook_url)
.insert_header(("X-Gitea-Delivery", "foobar213randomuuid"))
.insert_header(("X-Gitea-Signature", sig.clone()))
.insert_header(("X-Gitea-Event", "push"))
.cookie(cookies.clone())
.to_request(),
)
.await;
let err = ServiceError::WebsiteNotFound;
assert_eq!(post_to_no_website_webhook_resp.status(), err.status_code());
let resp_err: ErrorToResponse =
actix_web::test::read_body_json(post_to_no_website_webhook_resp).await;
assert_eq!(resp_err.error, err.to_string());
}
}

View File

@ -1,48 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use actix_auth_middleware::Authentication;
use actix_web::web::ServiceConfig;
use serde::Deserialize;
pub mod account;
pub mod auth;
pub mod forgejo;
pub mod meta;
pub mod pages;
pub mod routes;
pub use routes::ROUTES;
pub fn services(cfg: &mut ServiceConfig) {
auth::services(cfg);
account::services(cfg);
meta::services(cfg);
forgejo::services(cfg);
pages::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;

View File

@ -1,193 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use actix_web::{web, HttpResponse, Responder};
use serde::{Deserialize, Serialize};
use uuid::Uuid;
use crate::errors::*;
use crate::page::Page;
use crate::AppCtx;
pub mod routes {
pub struct Deploy {
pub update: &'static str,
pub info: &'static str,
}
impl Deploy {
pub const fn new() -> Self {
Self {
update: "/api/v1/update",
info: "/api/v1/info",
}
}
}
}
#[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,
}
#[actix_web_codegen_const_routes::post(path = "crate::V1_API_ROUTES.deploy.update")]
#[tracing::instrument(name = "Update webpages", skip(payload, ctx))]
async fn update(payload: web::Json<DeployEvent>, ctx: AppCtx) -> ServiceResult<impl Responder> {
let payload = payload.into_inner();
let id = ctx
.update_site(&payload.secret, Some(payload.branch))
.await?;
Ok(HttpResponse::Ok().json(DeployEventResp { id }))
}
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
pub struct DeploySecret {
pub secret: String,
}
#[derive(Clone, Debug, Eq, PartialEq, Deserialize, Serialize)]
pub struct DeployInfo {
pub head: String,
pub remote: String,
pub commit: String,
}
impl DeployInfo {
pub fn from_page(page: &Page) -> ServiceResult<Self> {
let repo = page.open_repo()?;
let head = page.get_deploy_branch(&repo)?;
let commit = Page::get_deploy_commit(&repo)?.to_string();
let remote = Page::get_deploy_remote(&repo)?;
let remote = remote.url().unwrap().to_owned();
Ok(Self {
head,
remote,
commit,
})
}
}
#[actix_web_codegen_const_routes::post(path = "crate::V1_API_ROUTES.deploy.info")]
#[tracing::instrument(name = "Get webpage deploy info", skip(payload, ctx))]
async fn deploy_info(
payload: web::Json<DeploySecret>,
ctx: AppCtx,
) -> ServiceResult<impl Responder> {
if let Ok(page) = ctx.db.get_site_from_secret(&payload.secret).await {
let resp = DeployInfo::from_page(&Page::from_site(&ctx.settings, page))?;
Ok(HttpResponse::Ok().json(resp))
} else {
Err(ServiceError::WebsiteNotFound)
}
}
pub fn services(cfg: &mut web::ServiceConfig) {
cfg.service(update);
cfg.service(deploy_info);
}
#[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);
}
#[actix_rt::test]
async fn deploy_info_works() {
const NAME: &str = "dplyinfwrkuser";
const PASSWORD: &str = "longpasswordasdfa2";
const EMAIL: &str = "dplyinfwrkuser@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 = DeploySecret {
secret: page.secret.clone(),
};
let resp = test::call_service(
&app,
post_request!(&payload, V1_API_ROUTES.deploy.info).to_request(),
)
.await;
check_status!(resp, StatusCode::OK);
let response: DeployInfo = actix_web::test::read_body_json(resp).await;
assert_eq!(response.head, page.branch);
assert_eq!(response.remote, page.repo);
payload.secret = page.branch.clone();
let resp = test::call_service(
&app,
post_request!(&payload, V1_API_ROUTES.deploy.info).to_request(),
)
.await;
check_status!(resp, StatusCode::NOT_FOUND);
}
}

View File

@ -1,127 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
//! V1 API Routes
use actix_auth_middleware::GetLoginRoute;
use crate::serve::routes::Serve;
use super::forgejo::routes::Forgejo;
use super::meta::routes::Meta;
use super::pages::routes::Deploy;
/// 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,
pub forgejo: Forgejo,
pub deploy: Deploy,
pub serve: Serve,
}
impl Routes {
/// create new instance of Routes
const fn new() -> Routes {
Routes {
auth: Auth::new(),
account: Account::new(),
meta: Meta::new(),
forgejo: Forgejo::new(),
deploy: Deploy::new(),
serve: Serve::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()
}
}
}

View File

@ -1,174 +0,0 @@
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use 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!(&register_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));
}

View File

@ -1,18 +0,0 @@
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
mod auth;
mod protected;

View File

@ -1,70 +0,0 @@
/*
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use 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);
}
}
}

View File

@ -1,96 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use reqwest::Client;
use libconductor::EventType;
use libconfig::Config;
use tracing::info;
use crate::errors::ServiceResult;
use crate::{page::Page, settings::Settings};
#[derive(Clone)]
pub struct Conductor {
client: Client,
pub settings: Settings,
}
impl Conductor {
pub fn new(settings: Settings, client: Option<Client>) -> Self {
let client = if let Some(client) = client {
client
} else {
Client::new()
};
Self { client, settings }
}
async fn tx(&self, e: &EventType) -> ServiceResult<()> {
for c in self.settings.conductors.iter() {
info!("Tx event to {}", c.url);
let mut event_url = c.url.clone();
event_url.set_path("/api/v1/events/new");
self.client
.post(event_url)
.basic_auth(&c.username, Some(&c.api_key))
.json(e)
.send()
.await
.unwrap();
}
Ok(())
}
pub async fn new_site(&self, page: Page) -> ServiceResult<()> {
let msg = EventType::NewSite {
hostname: page.domain,
branch: page.branch,
path: page.path,
};
self.tx(&msg).await
}
pub async fn tx_config(&self, config: Config) -> ServiceResult<()> {
self.tx(&EventType::Config { data: config }).await
}
pub async fn delete_site(&self, hostname: String) -> ServiceResult<()> {
self.tx(&EventType::DeleteSite { hostname }).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use uuid::Uuid;
#[actix_rt::test]
pub async fn test_conductor() {
let settings = Settings::new().unwrap();
let c = Conductor::new(settings.clone(), None);
c.delete_site("example.org".into()).await.unwrap();
let page = Page {
secret: "foo".into(),
repo: "foo".into(),
path: "foo".into(),
branch: "foo".into(),
domain: "foo".into(),
pub_id: Uuid::new_v4(),
};
c.new_site(page).await.unwrap();
}
}

View File

@ -1,17 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
pub mod v1;

View File

@ -1,136 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
//! 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(())
}
}

View File

@ -1,104 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
//! 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
}
}

View File

@ -1,204 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use actix_web::web;
use actix_web::HttpRequest;
use hmac::{Hmac, Mac};
use serde::{Deserialize, Serialize};
use sha2::Sha256;
use tracing::{info, warn};
use url::Url;
use crate::ctx::Ctx;
use crate::errors::ServiceError;
use crate::errors::ServiceResult;
pub type HmacSha256 = Hmac<Sha256>;
#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)]
pub struct CommitPerson {
pub name: String,
pub email: String,
pub username: String,
}
#[derive(Serialize, Deserialize, Default, Debug, Clone, Eq, PartialEq)]
pub struct Commit {
pub id: String,
pub message: String,
pub url: String,
pub author: CommitPerson,
pub committer: CommitPerson,
pub verification: serde_json::Value,
pub timestamp: String,
pub added: serde_json::Value,
pub removed: serde_json::Value,
pub modified: serde_json::Value,
}
#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)]
pub struct Person {
pub id: usize,
pub login: String,
pub full_name: String,
pub email: String,
pub avatar_url: String,
pub language: String,
pub is_admin: bool,
pub last_login: String,
pub created: String,
pub restricted: bool,
pub active: bool,
pub prohibit_login: bool,
pub location: String,
pub website: String,
pub description: String,
pub visibility: String,
pub followers_count: usize,
pub following_count: usize,
pub starred_repos_count: usize,
pub username: String,
}
#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)]
pub struct Permissions {
pub admin: bool,
pub push: bool,
pub pull: bool,
}
#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)]
pub struct InternalTracker {
pub enable_time_tracker: bool,
pub allow_only_contributors_to_track_time: bool,
pub enable_issue_dependencies: bool,
}
#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)]
pub struct Repository {
pub id: usize,
pub owner: Person,
pub name: String,
pub full_name: String,
pub description: String,
pub empty: bool,
pub private: bool,
pub fork: bool,
pub template: bool,
pub parent: Option<serde_json::Value>,
pub mirror: bool,
pub size: usize,
pub html_url: String,
pub ssh_url: String,
pub clone_url: String,
pub original_url: String,
pub website: String,
pub stars_count: usize,
pub forks_count: usize,
pub watchers_count: usize,
pub open_issues_count: usize,
pub open_pr_counter: usize,
pub release_counter: usize,
pub default_branch: String,
pub archived: bool,
pub created_at: String,
pub updated_at: String,
pub permissions: Permissions,
pub has_issues: bool,
pub internal_tracker: InternalTracker,
pub has_wiki: bool,
pub has_pull_requests: bool,
pub has_projects: bool,
pub ignore_whitespace_conflicts: bool,
pub allow_merge_commits: bool,
pub allow_rebase: bool,
pub allow_rebase_explicit: bool,
pub allow_squash_merge: bool,
pub default_merge_style: String,
pub avatar_url: String,
pub internal: bool,
pub mirror_interval: String,
pub mirror_updated: String,
pub repo_transfer: Option<serde_json::Value>,
}
#[derive(Serialize, Deserialize, Debug, Default, Clone, Eq, PartialEq)]
pub struct WebhookPayload {
#[serde(rename(serialize = "ref", deserialize = "ref"))]
pub reference: String,
pub before: String,
pub after: String,
pub compare_url: String,
pub repository: Repository,
pub pusher: Person,
pub sender: Person,
}
impl Ctx {
pub async fn process_webhook(
&self,
body: &web::Bytes,
req: &HttpRequest,
auth_token: &str,
) -> ServiceResult<()> {
let headers = req.headers();
let _uuid = headers.get("X-Gitea-Delivery").unwrap();
let sig = headers.get("X-Gitea-Signature").unwrap();
let sig = hex::decode(sig).unwrap();
let event_type = headers.get("X-Gitea-Event").unwrap();
let payload: WebhookPayload = serde_json::from_slice(body).unwrap();
let hook = self.db.get_webhook(auth_token).await?;
for url in [
&payload.repository.html_url,
&payload.repository.ssh_url,
&payload.repository.clone_url,
] {
if self.db.site_with_repository_exists(url).await? {
let mut mac = HmacSha256::new_from_slice(hook.forgejo_webhook_secret.as_bytes())?;
mac.update(body);
mac.verify_slice(&sig[..])?;
let site = self.db.get_site_from_repo_url(url).await?;
self.db
.webhook_link_site(auth_token, &Url::parse(&site.repo_url)?)
.await?;
if payload.reference.contains(&site.branch) {
info!(
"[webhook][forgejo/gitea] received update {:?} from {url} repository on deployed branch",
event_type
);
self.update_site(&site.site_secret, Some(site.branch))
.await?;
} else {
info!(
"[webhook][forgejo/gitea] received update {:?} from {url} repository on non-deployed branch {}",
event_type,
payload.reference
);
}
return Ok(());
}
}
warn!(
"[webhook][forgejo/gitea] stray update from {} repository",
payload.repository.html_url
);
Err(ServiceError::WebsiteNotFound)
}
}

View File

@ -1,123 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use actix_web::web;
use serde::{Deserialize, Serialize};
use tokio::fs;
use tokio::sync::oneshot;
use uuid::Uuid;
use crate::ctx::Ctx;
use crate::db;
use crate::db::Site;
use crate::errors::*;
use crate::page::Page;
use crate::page_config;
use crate::settings::Settings;
use crate::subdomains::get_random_subdomain;
use crate::utils::get_random;
use crate::utils::get_website_path;
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
/// Data required to add site
pub struct AddSite {
pub repo_url: String,
pub branch: String,
pub owner: String,
}
impl AddSite {
fn to_site(self, s: &Settings) -> Site {
let site_secret = get_random(32);
let hostname = get_random_subdomain(s);
let pub_id = Uuid::new_v4();
Site {
site_secret,
repo_url: self.repo_url,
branch: self.branch,
hostname,
owner: self.owner,
pub_id,
}
}
}
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?;
let page = Page::from_site(&self.settings, db_site);
page.update(&page.branch)?;
self.db
.log_event(&page.domain, &db::EVENT_TYPE_CREATE)
.await?;
self.conductor.new_site(page.clone()).await?;
if let Some(config) = page_config::load(&page.path, &page.branch) {
self.conductor.tx_config(config).await?;
unimplemented!("Parse and store custom domains in DB");
}
Ok(page)
}
pub async fn update_site(&self, secret: &str, branch: Option<String>) -> ServiceResult<Uuid> {
if let Ok(db_site) = self.db.get_site_from_secret(secret).await {
let page = Page::from_site(&self.settings, db_site);
let (tx, rx) = oneshot::channel();
{
let page = page.clone();
web::block(move || {
if let Some(branch) = branch {
tx.send(page.update(&branch)).unwrap();
} else {
tx.send(page.update(&page.branch)).unwrap();
}
})
.await
.unwrap();
}
rx.await.unwrap()?;
if let Some(config) = page_config::load(&page.path, &page.branch) {
self.conductor.tx_config(config).await?;
unimplemented!("Parse and store custom domains in DB");
}
self.db
.log_event(&page.domain, &db::EVENT_TYPE_UPDATE)
.await
} else {
Err(ServiceError::WebsiteNotFound)
}
}
pub async fn delete_site(&self, owner: String, site_id: Uuid) -> ServiceResult<()> {
if let Ok(db_site) = self.db.get_site_from_pub_id(site_id, owner).await {
let path = get_website_path(&self.settings, &db_site.hostname);
self.db
.log_event(&db_site.hostname, &db::EVENT_TYPE_DELETE)
.await?;
fs::remove_dir_all(&path).await?;
self.db
.delete_site(&db_site.owner, &db_site.hostname)
.await?;
self.conductor.delete_site(db_site.hostname).await?;
Ok(())
} else {
Err(ServiceError::WebsiteNotFound)
}
}
}

View File

@ -1,227 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use 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(&register_payload).await.unwrap();
register_payload.username = NAME2.into();
register_payload.email = EMAIL2.into();
ctx.register(&register_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;
}

View File

@ -1,104 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::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(&register_payload).await.err(),
Some(ServiceError::PasswordsDontMatch)
));
register_payload.confirm_password = PASSWORD.into();
ctx.register(&register_payload).await.unwrap();
// check if duplicate username is allowed
assert!(matches!(
ctx.register(&register_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(&register_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();
}

View File

@ -1,2 +0,0 @@
mod accounts;
mod auth;

View File

@ -1,79 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::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;
use crate::conductor::Conductor;
pub type ArcCtx = Arc<Ctx>;
#[derive(Clone)]
pub struct Ctx {
pub settings: Settings,
pub db: Database,
pub conductor: Conductor,
/// credential-procession policy
pub creds: ArgonConfig,
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();
let client = Client::default();
let conductor = Conductor::new(settings.clone(), Some(client.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,
client,
db,
creds,
conductor,
})
}
}

1299
src/db.rs

File diff suppressed because it is too large Load Diff

110
src/deploy.rs Normal file
View File

@ -0,0 +1,110 @@
/*
* 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::{web, HttpResponse, Responder};
use serde::{Deserialize, Serialize};
use crate::SETTINGS;
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,
}
#[my_codegen::post(path = "crate::V1_API_ROUTES.deploy.update")]
async fn update(payload: web::Json<DeployEvent>) -> impl Responder {
let mut found = false;
for page in SETTINGS.pages.iter() {
if page.secret == payload.secret {
web::block(|| {
page.fetch_upstream(&page.branch);
})
.await
.unwrap();
found = true;
}
}
if found {
HttpResponse::Ok()
} else {
HttpResponse::NotFound()
}
}
pub fn services(cfg: &mut web::ServiceConfig) {
cfg.service(update);
}
#[cfg(test)]
mod tests {
use actix_web::{http::StatusCode, test, App};
use crate::services;
use crate::*;
use super::*;
#[actix_rt::test]
async fn deploy_update_works() {
let app = test::init_service(App::new().configure(services)).await;
let page = SETTINGS.pages.get(0);
let page = page.unwrap();
let mut payload = DeployEvent {
secret: page.secret.clone(),
branch: page.branch.clone(),
};
let resp = test::call_service(
&app,
test::TestRequest::post()
.uri(V1_API_ROUTES.deploy.update)
.set_json(&payload)
.to_request(),
)
.await;
assert_eq!(resp.status(), StatusCode::OK);
payload.secret = page.branch.clone();
let resp = test::call_service(
&app,
test::TestRequest::post()
.uri(V1_API_ROUTES.deploy.update)
.set_json(&payload)
.to_request(),
)
.await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
}

View File

@ -1,293 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
//! Represents all the ways a trait can fail using this crate
use std::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 git2::Error as GitError;
use hmac::digest::InvalidLength;
use hmac::digest::MacError;
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 = "Git Error {}", _0)]
GitError(GitError),
#[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,
/// Webhook not found
#[display(fmt = "Webhook not found")]
WebhookNotFound,
}
impl From<InvalidLength> for ServiceError {
#[cfg(not(tarpaulin_include))]
fn from(_: InvalidLength) -> ServiceError {
ServiceError::InternalServerError
}
}
impl From<MacError> for ServiceError {
#[cfg(not(tarpaulin_include))]
fn from(_: MacError) -> ServiceError {
ServiceError::WebhookNotFound
}
}
impl From<ParseError> for ServiceError {
#[cfg(not(tarpaulin_include))]
fn from(_: ParseError) -> ServiceError {
ServiceError::NotAUrl
}
}
impl From<GitError> for ServiceError {
#[cfg(not(tarpaulin_include))]
fn from(e: GitError) -> ServiceError {
ServiceError::GitError(e)
}
}
/// 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::GitError(_) => 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,
ServiceError::WebhookNotFound => StatusCode::NOT_FOUND, //NOT FOUND,
}
}
}
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,
}
}
}

View File

@ -1,309 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::path::Path;
use std::path::PathBuf;
use git2::*;
use mime_guess::MimeGuess;
use num_enum::FromPrimitive;
use serde::{Deserialize, Serialize};
use crate::errors::*;
/// A FileMode represents the kind of tree entries used by git. It
/// resembles regular file systems modes, although FileModes are
/// considerably simpler (there are not so many), and there are some,
/// like Submodule that has no file system equivalent.
// Adapted from https://github.com/go-git/go-git/blob/master/plumbing/filemode/filemode.go(Apache-2.0 License)
#[derive(Debug, PartialEq, Eq, Clone, FromPrimitive)]
#[repr(isize)]
pub enum GitFileMode {
/// Empty is used as the GitFileMode of tree elements when comparing
/// trees in the following situations:
///
/// - the mode of tree elements before their creation.
/// - the mode of tree elements after their deletion.
/// - the mode of unmerged elements when checking the index.
///
/// Empty has no file system equivalent. As Empty is the zero value
/// of [GitFileMode]
Empty = 0,
/// Regular represent non-executable files.
Regular = 0o100644,
/// Dir represent a Directory.
Dir = 0o40000,
/// Deprecated represent non-executable files with the group writable bit set. This mode was
/// supported by the first versions of git, but it has been deprecated nowadays. This
/// library(github.com/go-git/go-git uses it, not realaravinth/gitpad at the moment) uses them
/// internally, so you can read old packfiles, but will treat them as Regulars when interfacing
/// with the outside world. This is the standard git behaviour.
Deprecated = 0o100664,
/// Executable represents executable files.
Executable = 0o100755,
/// Symlink represents symbolic links to files.
Symlink = 0o120000,
/// Submodule represents git submodules. This mode has no file system
/// equivalent.
Submodule = 0o160000,
/// Unsupported file mode
#[num_enum(default)]
Unsupported = -1,
}
impl From<&'_ TreeEntry<'_>> for GitFileMode {
fn from(t: &TreeEntry) -> Self {
GitFileMode::from(t.filemode() as isize)
}
}
impl From<TreeEntry<'_>> for GitFileMode {
fn from(t: TreeEntry) -> Self {
GitFileMode::from(t.filemode() as isize)
}
}
#[derive(Debug, Clone, Eq, PartialEq)]
pub struct FileInfo {
pub filename: String,
pub content: ContentType,
pub mime: MimeGuess,
}
#[derive(Serialize, Eq, PartialEq, Clone, Debug, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ContentType {
Binary(Vec<u8>),
Text(String),
}
impl ContentType {
pub fn bytes(self) -> Vec<u8> {
match self {
Self::Text(text) => text.into(),
Self::Binary(bin) => bin,
}
}
pub fn from_blob(blob: &git2::Blob) -> Self {
if blob.is_binary() {
Self::Binary(blob.content().to_vec())
} else {
Self::Text(String::from_utf8_lossy(blob.content()).to_string())
}
}
}
/// Please note that this method expects path to not contain any spaces
/// Use [escape_spaces] before calling this method
///
/// For example, a read request for "foo bar.md" will fail even if that file is present
/// in the repository. However, it will succeed if the output of [escape_spaces] is
/// used in the request.
pub fn read_file(repo_path: &PathBuf, path: &str) -> ServiceResult<FileInfo> {
let repo = git2::Repository::open(repo_path).unwrap();
let head = repo.head().unwrap();
let tree = head.peel_to_tree().unwrap();
read_file_inner(&repo, path, &tree)
}
pub fn read_preview_file(
repo_path: &PathBuf,
preview_name: &str,
path: &str,
) -> ServiceResult<FileInfo> {
let repo = git2::Repository::open(repo_path).unwrap();
let branch = repo
.find_branch(preview_name, git2::BranchType::Local)
.unwrap();
// let tree = head.peel_to_tree().unwrap();
let branch = branch.into_reference();
let tree = branch.peel_to_tree().unwrap();
read_file_inner(&repo, path, &tree)
}
fn read_file_inner(
repo: &git2::Repository,
path: &str,
tree: &git2::Tree,
) -> ServiceResult<FileInfo> {
fn read_file(id: Oid, repo: &git2::Repository) -> ContentType {
let blob = repo.find_blob(id).unwrap();
ContentType::from_blob(&blob)
}
fn get_index_file(id: Oid, repo: &Repository) -> ContentType {
let tree = repo.find_tree(id).unwrap();
const INDEX_FILES: [&str; 7] = [
"index.html",
"index.md",
"INDEX.md",
"README.md",
"README",
"readme.txt",
"readme",
];
let content = if let Some(index_file) = tree.iter().find(|x| {
if let Some(name) = x.name() {
INDEX_FILES.iter().any(|index_name| *index_name == name)
} else {
false
}
}) {
read_file(index_file.id(), repo)
} else {
unimplemented!("Index file not found");
};
content
}
let inner = |repo: &git2::Repository, tree: &git2::Tree| -> ServiceResult<FileInfo> {
let mut path = path;
if path == "/" {
let content = get_index_file(tree.id(), repo);
return Ok(FileInfo {
filename: "/".into(),
content,
mime: mime_guess::from_path("index.html"),
});
}
if path.starts_with('/') {
path = path.trim_start_matches('/');
}
fn file_not_found(e: git2::Error) -> ServiceError {
if e.code() == ErrorCode::NotFound && e.class() == ErrorClass::Tree {
return ServiceError::FileNotFound;
}
e.into()
}
let entry = tree.get_path(Path::new(path)).map_err(file_not_found)?;
let mode: GitFileMode = entry.clone().into();
if let Some(name) = entry.name() {
let file = match mode {
GitFileMode::Dir => get_index_file(entry.id(), repo),
GitFileMode::Submodule => unimplemented!(),
GitFileMode::Empty => unimplemented!(),
GitFileMode::Deprecated => unimplemented!(),
GitFileMode::Unsupported => unimplemented!(),
GitFileMode::Symlink => unimplemented!(),
GitFileMode::Executable => read_file(entry.id(), repo),
GitFileMode::Regular => read_file(entry.id(), repo),
};
Ok(FileInfo {
filename: name.to_string(),
mime: mime_guess::from_path(path),
content: file,
})
} else {
unimplemented!();
}
};
inner(repo, tree)
}
#[cfg(test)]
pub mod tests {
use super::*;
use mktemp::Temp;
const FILE_CONTENT: &str = "foobar";
pub fn write_file_util(repo_path: &str, file_name: &str, content: Option<&str>) {
// TODO change updated in DB
let inner = |repo: &mut Repository| -> ServiceResult<()> {
let mut tree_builder = match repo.head() {
Err(_) => repo.treebuilder(None).unwrap(),
Ok(h) => repo.treebuilder(Some(&h.peel_to_tree().unwrap())).unwrap(),
};
let odb = repo.odb().unwrap();
let content = if content.is_some() {
content.as_ref().unwrap()
} else {
FILE_CONTENT
};
let obj = odb.write(ObjectType::Blob, content.as_bytes()).unwrap();
tree_builder.insert(file_name, obj, 0o100644).unwrap();
let tree_hash = tree_builder.write().unwrap();
let author = Signature::now("librepages", "admin@librepages.org").unwrap();
let committer = Signature::now("librepages", "admin@librepages.org").unwrap();
let commit_tree = repo.find_tree(tree_hash).unwrap();
let msg = "";
if let Err(e) = repo.head() {
if e.code() == ErrorCode::UnbornBranch && e.class() == ErrorClass::Reference {
// fisrt commit ever; set parent commit(s) to empty array
repo.commit(Some("HEAD"), &author, &committer, msg, &commit_tree, &[])
.unwrap();
} else {
panic!("{:?}", e);
}
} else {
let head_ref = repo.head().unwrap();
let head_commit = head_ref.peel_to_commit().unwrap();
repo.commit(
Some("HEAD"),
&author,
&committer,
msg,
&commit_tree,
&[&head_commit],
)
.unwrap();
};
Ok(())
};
if Repository::open(repo_path).is_err() {
let _ = Repository::init(repo_path);
}
let mut repo = Repository::open(repo_path).unwrap();
let _ = inner(&mut repo);
}
#[test]
fn test_git_write_read_works() {
const FILENAME: &str = "README.txt";
let tmp_dir = Temp::new_dir().unwrap();
let path = tmp_dir.to_str().unwrap();
write_file_util(path, FILENAME, None);
let resp = read_file(&Path::new(path).into(), FILENAME).unwrap();
assert_eq!(resp.filename, FILENAME);
assert_eq!(resp.content.bytes(), FILE_CONTENT.as_bytes());
assert_eq!(resp.mime.first().unwrap(), "text/plain");
let resp = read_preview_file(&Path::new(path).into(), "master", FILENAME).unwrap();
assert_eq!(resp.filename, FILENAME);
assert_eq!(resp.content.bytes(), FILE_CONTENT.as_bytes());
assert_eq!(resp.mime.first().unwrap(), "text/plain");
assert_eq!(
read_preview_file(&Path::new(path).into(), "master", "file-does-not-exist.txt"),
Err(ServiceError::FileNotFound)
);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@ -16,38 +16,26 @@
*/
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,
error::InternalError, http::StatusCode, middleware as actix_middleware, web::JsonConfig, App,
HttpServer,
};
use clap::{Parser, Subcommand};
use static_assets::FileMap;
use tracing::info;
use tracing_actix_web::TracingLogger;
use lazy_static::lazy_static;
use log::info;
mod api;
mod conductor;
mod ctx;
mod db;
mod errors;
mod git;
mod deploy;
mod meta;
mod page;
mod page_config;
mod pages;
mod preview;
mod serve;
mod routes;
mod settings;
mod static_assets;
mod subdomains;
#[cfg(test)]
mod tests;
mod utils;
pub use crate::api::v1::ROUTES as V1_API_ROUTES;
use ctx::Ctx;
pub use routes::ROUTES as V1_API_ROUTES;
pub use settings::Settings;
lazy_static! {
pub static ref SETTINGS: Settings = Settings::new().unwrap();
}
pub const CACHE_AGE: u32 = 604800;
pub const GIT_COMMIT_HASH: &str = env!("GIT_HASH");
@ -56,69 +44,24 @@ 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))]
#[actix_web::main]
async fn main() -> std::io::Result<()> {
if env::var("RUST_LOG").is_err() {
env::set_var("RUST_LOG", "info");
}
lazy_static::initialize(&SETTINGS);
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();
println!("Starting server on: http://{}", SETTINGS.server.get_ip());
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);
settings.init();
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::Logger::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=()")),
@ -128,8 +71,8 @@ async fn serve(settings: Settings, ctx: AppCtx) -> std::io::Result<()> {
))
.configure(services)
})
.workers(workers)
.bind(ip)
.workers(SETTINGS.server.workers.unwrap_or_else(num_cpus::get))
.bind(SETTINGS.server.get_ip())
.unwrap()
.run()
.await
@ -143,23 +86,6 @@ pub fn get_json_err() -> JsonConfig {
})
}
#[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::static_assets::services(cfg);
crate::serve::services(cfg);
routes::services(cfg);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@ -17,13 +17,12 @@
use actix_web::{web, HttpResponse, Responder};
use serde::{Deserialize, Serialize};
use crate::{AppCtx, GIT_COMMIT_HASH, VERSION};
use crate::{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 struct BuildDetails {
pub version: &'static str,
pub git_commit_hash: &'static str,
}
pub mod routes {
@ -42,73 +41,38 @@ pub mod routes {
}
}
/// 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 {
/// emmits build details of the bninary
#[my_codegen::get(path = "crate::V1_API_ROUTES.meta.build_details")]
async fn build_details() -> 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 actix_web::{http::StatusCode, test, App};
use crate::services;
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 app = test::init_service(App::new().configure(services)).await;
let resp = test::call_service(
&app,
test::TestRequest::get()
.uri(crate::V1_API_ROUTES.meta.health)
.uri(V1_API_ROUTES.meta.build_details)
.to_request(),
)
.await;
assert_eq!(resp.status(), StatusCode::OK);
let health_resp: super::Health = test::read_body_json(resp).await;
assert!(health_resp.db);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@ -14,298 +14,72 @@
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
#[cfg(test)]
use std::println as info;
#[cfg(test)]
use std::println as error;
#[cfg(test)]
use std::println as debug;
use git2::{build::CheckoutBuilder, BranchType, Direction, Oid, Remote, Repository};
use git2::{build::CheckoutBuilder, BranchType, Direction, ObjectType, Repository};
use log::info;
use serde::Deserialize;
use serde::Serialize;
#[cfg(not(test))]
use tracing::{debug, error, info};
use uuid::Uuid;
use crate::db::Site;
use crate::errors::*;
use crate::settings::Settings;
use crate::utils::get_website_path;
#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)]
#[derive(Debug, Clone, Deserialize)]
pub struct Page {
pub secret: String,
pub repo: String,
pub path: String,
pub branch: String,
pub domain: String,
pub pub_id: Uuid,
}
impl Page {
pub fn from_site(settings: &Settings, s: Site) -> Self {
Self {
secret: s.site_secret,
repo: s.repo_url,
path: get_website_path(settings, &s.hostname)
.to_str()
.unwrap()
.to_owned(),
domain: s.hostname,
branch: s.branch,
pub_id: s.pub_id,
}
}
pub fn open_repo(&self) -> ServiceResult<Repository> {
Ok(Repository::open(&self.path)?)
}
fn create_repo(&self) -> ServiceResult<Repository> {
let repo = self.open_repo();
let repo = if let Ok(repo) = repo {
repo
} else {
info!("Cloning repository {} at {}", self.repo, self.path);
Repository::clone(&self.repo, &self.path)?;
Repository::open(&self.path)?
pub fn create_repo(&self) -> Repository {
let repo = match Repository::open(&self.path) {
Ok(repo) => repo,
Err(e) => {
log::error!("Opening repo {} caused error {}", &self.path, e);
info!("Cloning repository {} at {}", self.repo, self.path);
Repository::clone(&self.repo, &self.path).unwrap()
}
};
//let repo = Repository::open(&self.path);
self._fetch_remote_branch(&repo, &self.branch)?;
self.deploy_branch(&repo).unwrap();
//let repo = if repo.is_err() {
// info!("Cloning repository {} at {}", self.repo, self.path);
// Repository::clone(&self.repo, &self.path).unwrap()
//} else {
// repo.unwrap()
//};
// let branch = repo.find_branch(&self.branch, BranchType::Local).unwrap();
Ok(repo)
}
pub fn deploy_branch(&self, repo: &Repository) -> ServiceResult<()> {
let mut checkout_options = CheckoutBuilder::default();
checkout_options
.allow_conflicts(true)
.conflict_style_merge(true)
.force();
let refname = format!("refs/heads/{}", self.branch);
repo.set_head(&refname).unwrap();
repo.checkout_head(Some(&mut checkout_options)).unwrap();
info!("Deploying branch {}", self.branch);
Ok(())
}
fn fetch<'a>(
&self,
repo: &'a git2::Repository,
branch: &str,
) -> ServiceResult<git2::AnnotatedCommit<'a>> {
let mut remote = repo.find_remote("origin")?;
info!("Fetching {} for repo", remote.name().unwrap());
remote.fetch(&[branch], None, None)?;
let fetch_head = repo.find_reference("FETCH_HEAD")?;
Ok(repo.reference_to_annotated_commit(&fetch_head)?)
}
fn merge<'a>(
&self,
repo: &'a Repository,
fetch_commit: git2::AnnotatedCommit<'a>,
branch: &str,
) -> ServiceResult<()> {
// 1. do a merge analysis
let analysis = repo.merge_analysis(&[&fetch_commit])?;
// 2. Do the appropriate merge
if analysis.0.is_fast_forward() {
debug!("Doing a fast forward");
// do a fast forward
let refname = format!("refs/heads/{}", branch);
match repo.find_reference(&refname) {
Ok(mut r) => {
debug!("fast forwarding");
Self::fast_forward(repo, &mut r, &fetch_commit).unwrap();
}
Err(_) => {
// The branch doesn't exist so just set the reference to the
// commit directly. Usually this is because you are pulling
// into an empty repository.
error!("Error in find ref");
repo.reference(
&refname,
fetch_commit.id(),
true,
&format!("Setting {} to {}", branch, fetch_commit.id()),
)
.unwrap();
repo.set_head(&refname).unwrap();
repo.checkout_head(Some(
git2::build::CheckoutBuilder::default()
.allow_conflicts(true)
.conflict_style_merge(true)
.force(),
))
.unwrap();
}
};
} else if analysis.0.is_normal() {
// do a normal merge
// expects repo.head to point to the branch when is going to receive merges
let head_commit = repo
.reference_to_annotated_commit(&repo.head().unwrap())
//repo.branches(BranchType::Local).unwrap().find(|b| b.unwrap().na
{
let repo = Repository::open(&self.path).unwrap();
self._fetch_upstream(&repo, &self.branch);
let branch = repo
.find_branch(&format!("origin/{}", &self.branch), BranchType::Remote)
.unwrap();
Self::normal_merge(repo, &head_commit, &fetch_commit).unwrap();
} else {
info!("Nothing to do...");
let mut checkout_options = CheckoutBuilder::new();
checkout_options.force();
let tree = branch.get().peel(ObjectType::Tree).unwrap();
repo.checkout_tree(&tree, Some(&mut checkout_options))
.unwrap();
// repo.set_head(&format!("refs/heads/{}", &self.branch))
// .unwrap();
repo.set_head(branch.get().name().unwrap()).unwrap();
// }
}
Ok(())
repo
}
fn _fetch_remote_branch(&self, repo: &Repository, branch: &str) -> ServiceResult<()> {
let mut remote = Self::get_deploy_remote(repo)?;
remote.connect(Direction::Fetch)?;
fn _fetch_upstream(&self, repo: &Repository, branch: &str) {
let mut remote = repo.find_remote("origin").unwrap();
remote.connect(Direction::Fetch).unwrap();
info!("Updating repository {}", self.repo);
let remote_branch_name = format!("origin/{branch}");
remote.fetch(&[&remote_branch_name], None, None)?;
remote.disconnect()?;
let branch = repo.find_branch(&remote_branch_name, BranchType::Remote)?;
let commit = branch.get().peel_to_commit()?;
if repo.find_branch(&self.branch, BranchType::Local).is_err() {
repo.branch(&self.branch, &commit, true)?;
}
Ok(())
remote.fetch(&[branch], None, None).unwrap();
remote.disconnect().unwrap();
}
fn normal_merge(
repo: &Repository,
local: &git2::AnnotatedCommit,
remote: &git2::AnnotatedCommit,
) -> Result<(), git2::Error> {
let local_tree = repo.find_commit(local.id())?.tree().unwrap();
let remote_tree = repo.find_commit(remote.id())?.tree().unwrap();
debug!("{} {}", local.id(), remote.id());
let ancestor = repo
.find_commit(repo.merge_base(local.id(), remote.id()).unwrap())
.unwrap()
.tree()
.unwrap();
let mut idx = repo
.merge_trees(&ancestor, &local_tree, &remote_tree, None)
.unwrap();
if idx.has_conflicts() {
debug!("Merge conflicts detected...");
repo.checkout_index(Some(&mut idx), None)?;
return Ok(());
}
let result_tree = repo.find_tree(idx.write_tree_to(repo)?)?;
// now create the merge commit
let msg = format!("Merge: {} into {}", remote.id(), local.id());
let sig = repo.signature()?;
let local_commit = repo.find_commit(local.id())?;
let remote_commit = repo.find_commit(remote.id())?;
// Do our merge commit and set current branch head to that commit.
let _merge_commit = repo.commit(
Some("HEAD"),
&sig,
&sig,
&msg,
&result_tree,
&[&local_commit, &remote_commit],
)?;
// Set working tree to match head.
repo.checkout_head(None)?;
Ok(())
}
fn fast_forward(
repo: &Repository,
lb: &mut git2::Reference,
rc: &git2::AnnotatedCommit,
) -> ServiceResult<()> {
let name = match lb.name() {
Some(s) => s.to_string(),
None => String::from_utf8_lossy(lb.name_bytes()).to_string(),
};
let msg = format!("Fast-Forward: Setting {} to id: {}", name, rc.id());
debug!("{}", msg);
lb.set_target(rc.id(), &msg)?;
repo.set_head(&name)?;
repo.checkout_head(Some(git2::build::CheckoutBuilder::default().force()))?;
Ok(())
}
pub fn update(&self, branch: &str) -> ServiceResult<()> {
let repo = self.create_repo()?;
let fetch_commit = self.fetch(&repo, branch)?;
self.merge(&repo, fetch_commit, branch)?;
Ok(())
}
pub fn get_deploy_branch(&self, repo: &Repository) -> ServiceResult<String> {
let branch = repo.find_branch(&self.branch, BranchType::Local)?;
if branch.is_head() {
Ok(self.branch.clone())
} else {
Err(ServiceError::BranchNotFound(self.branch.clone()))
}
}
pub fn get_deploy_commit(repo: &Repository) -> ServiceResult<Oid> {
let head = repo.head()?;
let commit = head.peel_to_commit()?;
Ok(commit.id())
}
pub fn get_deploy_remote(repo: &Repository) -> ServiceResult<Remote> {
Ok(repo.find_remote("origin")?)
}
}
#[cfg(test)]
mod tests {
use super::*;
use git2::Repository;
use mktemp::Temp;
use crate::tests;
#[actix_rt::test]
async fn pages_works() {
let tmp_dir = Temp::new_dir().unwrap();
assert!(tmp_dir.exists(), "tmp directory successully created");
let mut page = Page {
secret: String::default(),
repo: tests::REPO_URL.into(),
path: tmp_dir.to_str().unwrap().to_string(),
branch: tests::BRANCH.to_string(),
domain: "mcaptcha.org".into(),
pub_id: Uuid::new_v4(),
};
assert!(
Repository::open(tmp_dir.as_path()).is_err(),
"repository doesn't exist yet"
);
let repo = page.create_repo().unwrap();
assert!(!repo.is_bare(), "repository isn't bare");
page.create_repo().unwrap();
assert!(
Repository::open(tmp_dir.as_path()).is_ok(),
"repository exists yet"
);
let gh_pages = page.get_deploy_branch(&repo).unwrap();
assert_eq!(gh_pages, "gh-pages");
page.branch = "master".to_string();
page.update(&page.branch).unwrap();
let master = page.get_deploy_branch(&repo).unwrap();
assert_eq!(master, "master");
assert_eq!(
Page::get_deploy_remote(&repo).unwrap().url().unwrap(),
page.repo
);
pub fn fetch_upstream(&self, branch: &str) {
let repo = self.create_repo();
self._fetch_upstream(&repo, branch);
}
}

View File

@ -1,165 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::path::Path;
use libconfig::Config;
use serde::{Deserialize, Serialize};
use crate::git::{ContentType, GitFileMode};
#[derive(Deserialize, Debug, Serialize, PartialEq, Eq)]
struct Policy<'a> {
rel_path: &'a str,
format: SupportedFormat,
}
impl<'a> Policy<'a> {
const fn new(rel_path: &'a str, format: SupportedFormat) -> Self {
Self { rel_path, format }
}
}
#[derive(Deserialize, Debug, Serialize, PartialEq, Eq)]
enum SupportedFormat {
Json,
Yaml,
Toml,
}
pub fn load<P: AsRef<Path>>(repo_path: &P, branch: &str) -> Option<Config> {
const POLICIES: [Policy; 2] = [
Policy::new("librepages.toml", SupportedFormat::Toml),
Policy::new("librepages.json", SupportedFormat::Json),
];
if let Some(policy) = discover(repo_path, branch, &POLICIES) {
// let path = p.repo.as_ref().join(policy.rel_path);
//let contents = fs::read_to_string(path).await.unwrap();
let file =
crate::git::read_preview_file(&repo_path.as_ref().into(), branch, policy.rel_path)
.unwrap();
if let ContentType::Text(contents) = file.content {
let res = match policy.format {
SupportedFormat::Json => load_json(&contents),
SupportedFormat::Yaml => load_yaml(&contents),
SupportedFormat::Toml => load_toml(&contents),
};
return Some(res);
};
}
None
}
fn discover<'a, P: AsRef<Path>>(
repo_path: &P,
branch: &str,
policies: &'a [Policy<'a>],
) -> Option<&'a Policy<'a>> {
let repo = git2::Repository::open(repo_path).unwrap();
let branch = repo.find_branch(branch, git2::BranchType::Local).unwrap();
// let tree = head.peel_to_tree().unwrap();
let branch = branch.into_reference();
let tree = branch.peel_to_tree().unwrap();
for p in policies.iter() {
let file_exists = tree.iter().any(|x| {
if let Some(name) = x.name() {
if policies.iter().any(|p| p.rel_path == name) {
let mode: GitFileMode = x.into();
matches!(mode, GitFileMode::Executable | GitFileMode::Regular)
} else {
false
}
} else {
false
}
});
if file_exists {
return Some(p);
}
}
None
}
fn load_toml(c: &str) -> Config {
toml::from_str(c).unwrap()
}
fn load_yaml(c: &str) -> Config {
serde_yaml::from_str(c).unwrap()
}
fn load_json(c: &str) -> Config {
serde_json::from_str(c).unwrap()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::git::tests::write_file_util;
use mktemp::Temp;
use libconfig::*;
#[actix_rt::test]
async fn page_config_test() {
let tmp_dir = Temp::new_dir().unwrap();
let repo_path = tmp_dir.join("page_config_test");
let content = std::fs::read_to_string(
&Path::new("./tests/cases/contains-everything/toml/librepages.toml")
.canonicalize()
.unwrap(),
)
.unwrap();
write_file_util(
repo_path.to_str().unwrap(),
"librepages.toml",
Some(&content),
);
let config = load(&repo_path, "master").unwrap();
assert!(config.forms.as_ref().unwrap().enable);
assert!(config.image_compression.as_ref().unwrap().enable);
assert_eq!(config.source.production_branch, "librepages");
assert_eq!(config.source.staging.as_ref().unwrap(), "beta");
assert_eq!(
config.redirects.as_ref().unwrap(),
&vec![
Redirects {
from: "/from1".into(),
to: "/to1".into()
},
Redirects {
from: "/from2".into(),
to: "/to2".into()
},
]
);
assert_eq!(
config.domains.as_ref().unwrap(),
&vec!["example.org".to_string(), "example.com".to_string(),]
);
}
}

View File

@ -1,121 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::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();
}
}

View File

@ -1,162 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use actix_identity::Identity;
use actix_web::*;
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);
// }
//}
//

View File

@ -1,109 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use actix_web::http::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();
}
}

View File

@ -1,144 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use actix_auth_middleware::GetLoginRoute;
use 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!(&register_msg, PAGES.auth.register, FORM).to_request(),
)
.await;
assert_eq!(
resp.status(),
ServiceError::PasswordsDontMatch.status_code()
);
}

View File

@ -1,193 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::cell::RefCell;
use actix_identity::Identity;
use actix_web::http::header::ContentType;
use tera::Context;
use tracing::info;
use super::get_auth_middleware;
use crate::api::v1::forgejo::AddWebhook;
use crate::pages::errors::*;
use crate::settings::Settings;
use crate::AppCtx;
pub use super::*;
pub const DASH_FORGEJO_WEBHOOK_ADD: TemplateFile =
TemplateFile::new("dash_forgejo_webhook_add", "pages/dash/forgejo/add.html");
pub struct Add {
ctx: RefCell<Context>,
}
impl CtxError for Add {
fn with_error(&self, e: &ReadableError) -> String {
self.ctx.borrow_mut().insert(ERROR_KEY, e);
self.render()
}
}
impl Add {
pub fn new(settings: &Settings) -> Self {
let ctx = RefCell::new(context(settings));
Self { ctx }
}
pub fn render(&self) -> String {
TEMPLATES
.render(DASH_FORGEJO_WEBHOOK_ADD.name, &self.ctx.borrow())
.unwrap()
}
}
#[actix_web_codegen_const_routes::get(
path = "PAGES.dash.forgejo_webhook.add",
wrap = "get_auth_middleware()"
)]
#[tracing::instrument(name = "Dashboard add forgejo webhook webpage", skip(ctx))]
pub async fn get_add_forgejo_webhook(ctx: AppCtx) -> PageResult<impl Responder, Add> {
let add = Add::new(&ctx.settings).render();
let html = ContentType::html();
Ok(HttpResponse::Ok().content_type(html).body(add))
}
#[actix_web_codegen_const_routes::post(
path = "PAGES.dash.forgejo_webhook.add",
wrap = "get_auth_middleware()"
)]
#[tracing::instrument(
name = "Post Dashboard add Forgejo webhook webpage",
skip(ctx, id, payload)
)]
pub async fn post_add_forgejo_webhook(
ctx: AppCtx,
id: Identity,
payload: web::Form<AddWebhook>,
) -> PageResult<impl Responder, Add> {
let owner = id.identity().unwrap();
let payload = payload.into_inner();
info!(
"Adding webhook for Forgejo instance: {}",
payload.forgejo_url.as_str()
);
let hook = ctx
.db
.new_webhook(payload.forgejo_url, &owner)
.await
.map_err(|e| PageError::new(Add::new(&ctx.settings), e))?;
Ok(HttpResponse::Found()
.append_header((
http::header::LOCATION,
PAGES.dash.forgejo_webhook.get_view(&hook.auth_token),
))
.finish())
}
pub fn services(cfg: &mut web::ServiceConfig) {
cfg.service(get_add_forgejo_webhook);
cfg.service(post_add_forgejo_webhook);
}
#[cfg(test)]
mod tests {
use actix_web::http::StatusCode;
use actix_web::test;
use url::Url;
use crate::api::v1::forgejo::AddWebhook;
use crate::ctx::ArcCtx;
use crate::tests;
use crate::*;
use super::PAGES;
#[actix_rt::test]
async fn postgres_dashboadr_add_forgejo_webhook_works() {
let (_, ctx) = tests::get_ctx().await;
dashboadr_add_forgejo_webhook_works(ctx.clone()).await;
}
async fn dashboadr_add_forgejo_webhook_works(ctx: ArcCtx) {
const NAME: &str = "testdashwebhookforgejoadduser";
const EMAIL: &str = "testdashwebhookforgejoadduser@foo.com";
const PASSWORD: &str = "longpassword";
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.clone()).await;
let resp = get_request!(&app, PAGES.dash.forgejo_webhook.add, cookies.clone());
assert_eq!(resp.status(), StatusCode::OK);
let res = String::from_utf8(test::read_body(resp).await.to_vec()).unwrap();
assert!(res.contains("Add Forgejo Webhook"));
let payload = AddWebhook {
forgejo_url: Url::parse("https://git.batsense.net").unwrap(),
};
let add_webhook = test::call_service(
&app,
post_request!(&payload, PAGES.dash.forgejo_webhook.add, FORM)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(add_webhook.status(), StatusCode::FOUND);
let mut hooks = ctx.db.list_all_webhooks_with_owner(NAME).await.unwrap();
let hook = hooks.pop().unwrap();
// let mut event = ctx.db.list(&site.hostname).await.unwrap();
// let event = event.pop().unwrap();
let headers = add_webhook.headers();
let view_webhook_url = PAGES.dash.forgejo_webhook.get_view(&hook.auth_token);
assert_eq!(
headers.get(actix_web::http::header::LOCATION).unwrap(),
&view_webhook_url
);
// list webhooks
let resp = get_request!(&app, PAGES.dash.forgejo_webhook.list, cookies.clone());
assert_eq!(resp.status(), StatusCode::OK);
let res = String::from_utf8(test::read_body(resp).await.to_vec()).unwrap();
assert!(res.contains(hook.forgejo_url.as_str()));
// view webhook
let resp = get_request!(&app, &view_webhook_url, cookies.clone());
assert_eq!(resp.status(), StatusCode::OK);
let res = String::from_utf8(test::read_body(resp).await.to_vec()).unwrap();
assert!(res.contains("****"));
assert!(res.contains(
&crate::V1_API_ROUTES
.forgejo
.get_webhook_url(&ctx, &hook.auth_token)
));
let show_forgejo_webhook_secret =
format!("{view_webhook_url}?show_forgejo_webhook_secret=true");
let resp = get_request!(&app, &show_forgejo_webhook_secret, cookies.clone());
assert_eq!(resp.status(), StatusCode::OK);
let res = String::from_utf8(test::read_body(resp).await.to_vec()).unwrap();
assert!(res.contains(&hook.forgejo_webhook_secret));
}
}

View File

@ -1,93 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::cell::RefCell;
use actix_identity::Identity;
use actix_web::http::header::ContentType;
use tera::Context;
use super::get_auth_middleware;
use crate::errors::ServiceResult;
use crate::pages::errors::*;
use crate::settings::Settings;
use crate::AppCtx;
pub use super::*;
pub const DASH_FORGEJO_WEBHOOK_LIST: TemplateFile =
TemplateFile::new("dash_forgejo_webhook_list", "pages/dash/forgejo/list.html");
pub struct List {
ctx: RefCell<Context>,
}
impl CtxError for List {
fn with_error(&self, e: &ReadableError) -> String {
self.ctx.borrow_mut().insert(ERROR_KEY, e);
self.render()
}
}
impl List {
pub fn new(settings: &Settings, hooks: Option<&[TemplateForgejoWebhook]>) -> Self {
let ctx = RefCell::new(context(settings));
if let Some(hooks) = hooks {
ctx.borrow_mut().insert(PAYLOAD_KEY, hooks);
}
Self { ctx }
}
pub fn render(&self) -> String {
TEMPLATES
.render(DASH_FORGEJO_WEBHOOK_LIST.name, &self.ctx.borrow())
.unwrap()
}
}
async fn get_webhook_data(
ctx: &AppCtx,
id: &Identity,
) -> ServiceResult<Vec<TemplateForgejoWebhook>> {
let db_hooks = ctx
.db
.list_all_webhooks_with_owner(&id.identity().unwrap())
.await?;
let mut hooks = Vec::with_capacity(db_hooks.len());
for hook in db_hooks {
hooks.push(TemplateForgejoWebhook::new(ctx, hook));
}
Ok(hooks)
}
#[actix_web_codegen_const_routes::get(
path = "PAGES.dash.forgejo_webhook.list",
wrap = "get_auth_middleware()"
)]
#[tracing::instrument(name = "List all Forgejo webhooks", skip(ctx, id))]
pub async fn list_hooks(ctx: AppCtx, id: Identity) -> PageResult<impl Responder, List> {
let sites = get_webhook_data(&ctx, &id)
.await
.map_err(|e| PageError::new(List::new(&ctx.settings, None), e))?;
let home = List::new(&ctx.settings, Some(&sites)).render();
let html = ContentType::html();
Ok(HttpResponse::Ok().content_type(html).body(home))
}
pub fn services(cfg: &mut web::ServiceConfig) {
cfg.service(list_hooks);
}

View File

@ -1,69 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use actix_web::*;
use serde::{Deserialize, Serialize};
use super::get_auth_middleware;
pub use super::home::TemplateSite;
pub use super::{context, Footer, TemplateFile, PAGES, PAYLOAD_KEY, TEMPLATES};
use crate::ctx::Ctx;
use crate::db::ForgejoWebhook;
pub mod add;
pub mod list;
pub mod view;
pub fn register_templates(t: &mut tera::Tera) {
add::DASH_FORGEJO_WEBHOOK_ADD
.register(t)
.expect(add::DASH_FORGEJO_WEBHOOK_ADD.name);
list::DASH_FORGEJO_WEBHOOK_LIST
.register(t)
.expect(list::DASH_FORGEJO_WEBHOOK_LIST.name);
view::DASH_FORGEJO_WEBHOOK_VIEW
.register(t)
.expect(view::DASH_FORGEJO_WEBHOOK_VIEW.name);
}
pub fn services(cfg: &mut web::ServiceConfig) {
add::services(cfg);
list::services(cfg);
view::services(cfg);
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub struct TemplateForgejoWebhook {
pub webhook: ForgejoWebhook,
pub view: String,
pub url: String,
}
impl TemplateForgejoWebhook {
pub fn new(ctx: &Ctx, hook: ForgejoWebhook) -> Self {
let view = PAGES.dash.forgejo_webhook.get_view(&hook.auth_token);
let url = crate::V1_API_ROUTES
.forgejo
.get_webhook_url(ctx, &hook.auth_token);
Self {
webhook: hook,
view,
url,
}
}
}

View File

@ -1,108 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::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::pages::errors::*;
use crate::settings::Settings;
use crate::AppCtx;
pub use super::*;
pub const DASH_FORGEJO_WEBHOOK_VIEW: TemplateFile =
TemplateFile::new("dash_forgejo_webhook_view", "pages/dash/forgejo/view.html");
const SHOW_FORGEJO_WEBHOOK_SECRET_KEY: &str = "show_forgejo_webhook_secret";
pub struct View {
ctx: RefCell<Context>,
}
impl CtxError for View {
fn with_error(&self, e: &ReadableError) -> String {
self.ctx.borrow_mut().insert(ERROR_KEY, e);
self.render()
}
}
impl View {
pub fn new(settings: &Settings, payload: Option<TemplateForgejoWebhook>) -> Self {
let ctx = RefCell::new(context(settings));
if let Some(payload) = payload {
ctx.borrow_mut().insert(PAYLOAD_KEY, &payload);
}
Self { ctx }
}
pub fn show_forgejo_webhook_secret(&mut self) {
self.ctx
.borrow_mut()
.insert(SHOW_FORGEJO_WEBHOOK_SECRET_KEY, &true);
}
pub fn render(&self) -> String {
TEMPLATES
.render(DASH_FORGEJO_WEBHOOK_VIEW.name, &self.ctx.borrow())
.unwrap()
}
}
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
pub struct ViewOptions {
show_forgejo_webhook_secret: Option<bool>,
}
#[actix_web_codegen_const_routes::get(
path = "PAGES.dash.forgejo_webhook.view",
wrap = "get_auth_middleware()"
)]
#[tracing::instrument(name = "Dashboard Forgejo webhook webpage", skip(ctx, id))]
pub async fn get_view_site(
ctx: AppCtx,
id: Identity,
path: web::Path<String>,
query: web::Query<ViewOptions>,
) -> PageResult<impl Responder, View> {
let auth_token = path.into_inner();
let owner = id.identity().unwrap();
let hook = ctx
.db
.get_webhook_with_owner(&auth_token, &owner)
.await
.map_err(|e| PageError::new(View::new(&ctx.settings, None), e))?;
let payload = TemplateForgejoWebhook::new(&ctx, hook);
let mut page = View::new(&ctx.settings, Some(payload));
if let Some(true) = query.show_forgejo_webhook_secret {
page.show_forgejo_webhook_secret();
}
let add = page.render();
let html = ContentType::html();
Ok(HttpResponse::Ok().content_type(html).body(add))
}
pub fn services(cfg: &mut web::ServiceConfig) {
cfg.service(get_view_site);
}

View File

@ -1,157 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::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;
use super::TemplateSiteEvent;
pub use super::*;
pub const DASH_HOME: TemplateFile = TemplateFile::new("dash_home", "pages/dash/index.html");
pub struct Home {
ctx: RefCell<Context>,
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub struct TemplateSite {
pub site: Site,
pub view: String,
pub last_update: Option<TemplateSiteEvent>,
}
impl TemplateSite {
pub fn new(site: Site, last_update: Option<TemplateSiteEvent>) -> Self {
let view = PAGES.dash.site.get_view(site.pub_id);
Self {
site,
last_update,
view,
}
}
}
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<&[TemplateSite]>) -> 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()
}
}
async fn get_site_data(ctx: &AppCtx, id: &Identity) -> ServiceResult<Vec<TemplateSite>> {
let db_sites = ctx.db.list_all_sites(&id.identity().unwrap()).await?;
let mut sites = Vec::with_capacity(db_sites.len());
for site in db_sites {
// TODO: impl method on DB to get latest "update" event
let last_update = ctx
.db
.get_latest_update_event(&site.hostname)
.await?
.map(|e| e.into());
sites.push(TemplateSite::new(site, last_update));
}
Ok(sites)
}
#[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 sites = get_site_data(&ctx, &id)
.await
.map_err(|e| PageError::new(Home::new(&ctx.settings, None), e))?;
let home = Home::new(&ctx.settings, Some(&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);
}
#[cfg(test)]
mod tests {
use actix_web::http::StatusCode;
use actix_web::test;
use crate::ctx::ArcCtx;
use crate::tests;
use crate::*;
use super::PAGES;
#[actix_rt::test]
async fn postgres_dash_home_works() {
let (_, ctx) = tests::get_ctx().await;
dashboard_home_works(ctx.clone()).await;
}
async fn dashboard_home_works(ctx: ArcCtx) {
const NAME: &str = "testdashuser";
const EMAIL: &str = "testdashuser@foo.com";
const PASSWORD: &str = "longpassword";
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 resp = get_request!(&app, PAGES.dash.home, cookies.clone());
assert_eq!(resp.status(), StatusCode::OK);
let res = String::from_utf8(test::read_body(resp).await.to_vec()).unwrap();
println!("before adding site: {res}");
assert!(res.contains("Nothing to show"));
let page = ctx.add_test_site(NAME.into()).await;
let resp = get_request!(&app, PAGES.dash.home, cookies.clone());
assert_eq!(resp.status(), StatusCode::OK);
let res = String::from_utf8(test::read_body(resp).await.to_vec()).unwrap();
println!("after adding site: {res}");
assert!(!res.contains("Nothing here"));
assert!(res.contains(&page.domain));
assert!(res.contains(&page.repo));
let _ = ctx.delete_user(NAME, PASSWORD).await;
}
}

View File

@ -1,60 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use actix_web::*;
use serde::{Deserialize, Serialize};
use uuid::Uuid;
pub use super::get_auth_middleware;
pub use super::{context, Footer, TemplateFile, PAGES, PAYLOAD_KEY, TEMPLATES};
use crate::db::Event;
use crate::db::LibrePagesEvent;
pub mod forgejo;
pub mod home;
pub mod sites;
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
pub struct TemplateSiteEvent {
pub event_type: Event,
pub time: i64,
pub site: String,
pub id: Uuid,
}
impl From<LibrePagesEvent> for TemplateSiteEvent {
fn from(e: LibrePagesEvent) -> Self {
Self {
event_type: e.event_type,
time: e.time.unix_timestamp(),
site: e.site,
id: e.id,
}
}
}
pub fn register_templates(t: &mut tera::Tera) {
home::DASH_HOME.register(t).expect(home::DASH_HOME.name);
sites::register_templates(t);
forgejo::register_templates(t);
}
pub fn services(cfg: &mut web::ServiceConfig) {
home::services(cfg);
sites::services(cfg);
forgejo::services(cfg);
}

View File

@ -1,230 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::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::ctx::api::v1::pages::AddSite;
use crate::pages::errors::*;
use crate::settings::Settings;
use crate::AppCtx;
pub use super::*;
pub const DASH_SITE_ADD: TemplateFile =
TemplateFile::new("dash_site_add", "pages/dash/sites/add.html");
pub struct Add {
ctx: RefCell<Context>,
}
impl CtxError for Add {
fn with_error(&self, e: &ReadableError) -> String {
self.ctx.borrow_mut().insert(ERROR_KEY, e);
self.render()
}
}
impl Add {
pub fn new(settings: &Settings) -> Self {
let ctx = RefCell::new(context(settings));
Self { ctx }
}
pub fn render(&self) -> String {
TEMPLATES
.render(DASH_SITE_ADD.name, &self.ctx.borrow())
.unwrap()
}
}
#[actix_web_codegen_const_routes::get(path = "PAGES.dash.site.add", wrap = "get_auth_middleware()")]
#[tracing::instrument(name = "Dashboard add site webpage", skip(ctx))]
pub async fn get_add_site(ctx: AppCtx) -> PageResult<impl Responder, Add> {
let add = Add::new(&ctx.settings).render();
let html = ContentType::html();
Ok(HttpResponse::Ok().content_type(html).body(add))
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
/// Data required to add site
pub struct TemplateAddSite {
pub repo_url: String,
pub branch: String,
}
#[actix_web_codegen_const_routes::post(
path = "PAGES.dash.site.add",
wrap = "get_auth_middleware()"
)]
#[tracing::instrument(name = "Post Dashboard add site webpage", skip(ctx, id))]
pub async fn post_add_site(
ctx: AppCtx,
id: Identity,
payload: web::Form<TemplateAddSite>,
) -> PageResult<impl Responder, Add> {
let owner = id.identity().unwrap();
let payload = payload.into_inner();
let msg = AddSite {
branch: payload.branch,
repo_url: payload.repo_url,
owner,
};
let page = ctx
.add_site(msg)
.await
.map_err(|e| PageError::new(Add::new(&ctx.settings), e))?;
Ok(HttpResponse::Found()
.append_header((
http::header::LOCATION,
PAGES.dash.site.get_view(page.pub_id),
))
.finish())
}
pub fn services(cfg: &mut web::ServiceConfig) {
cfg.service(get_add_site);
cfg.service(post_add_site);
}
#[cfg(test)]
mod tests {
use actix_web::http::StatusCode;
use actix_web::test;
use crate::ctx::api::v1::auth::Password;
use crate::ctx::ArcCtx;
use crate::errors::ServiceError;
use crate::pages::dash::sites::add::TemplateAddSite;
use crate::tests;
use crate::*;
use super::PAGES;
#[actix_rt::test]
async fn postgres_dashboard_add_site_works() {
let (_, ctx) = tests::get_ctx().await;
dashboard_add_site_works(ctx.clone()).await;
}
async fn dashboard_add_site_works(ctx: ArcCtx) {
const NAME: &str = "testdashaddsiteuser";
const EMAIL: &str = "testdashaddsiteuser@foo.com";
const PASSWORD: &str = "longpassword";
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.clone()).await;
let resp = get_request!(&app, PAGES.dash.site.add, cookies.clone());
assert_eq!(resp.status(), StatusCode::OK);
let res = String::from_utf8(test::read_body(resp).await.to_vec()).unwrap();
assert!(res.contains("Add Site"));
let payload = TemplateAddSite {
repo_url: tests::REPO_URL.into(),
branch: tests::BRANCH.into(),
};
let add_site = test::call_service(
&app,
post_request!(&payload, PAGES.dash.site.add, FORM)
.cookie(cookies.clone())
.to_request(),
)
.await;
assert_eq!(add_site.status(), StatusCode::FOUND);
let mut site = ctx.db.list_all_sites(NAME).await.unwrap();
let site = site.pop().unwrap();
let mut event = ctx.db.list_all_site_events(&site.hostname).await.unwrap();
let event = event.pop().unwrap();
let headers = add_site.headers();
let view_site = &PAGES.dash.site.get_view(site.pub_id);
assert_eq!(
headers.get(actix_web::http::header::LOCATION).unwrap(),
view_site
);
// view site
let resp = get_request!(&app, view_site, cookies.clone());
assert_eq!(resp.status(), StatusCode::OK);
let res = String::from_utf8(test::read_body(resp).await.to_vec()).unwrap();
assert!(res.contains("****"));
assert!(res.contains(&site.hostname));
assert!(res.contains(&site.repo_url));
assert!(res.contains(&site.branch));
assert!(res.contains(&event.event_type.name));
assert!(res.contains(&event.id.to_string()));
let show_deploy_secret_route = format!("{view_site}?show_deploy_secret=true");
let resp = get_request!(&app, &show_deploy_secret_route, cookies.clone());
assert_eq!(resp.status(), StatusCode::OK);
let res = String::from_utf8(test::read_body(resp).await.to_vec()).unwrap();
assert!(res.contains(&site.site_secret));
// delete site
let delete_site = &PAGES.dash.site.get_delete(site.pub_id);
let resp = get_request!(&app, delete_site, cookies.clone());
assert_eq!(resp.status(), StatusCode::OK);
let res = String::from_utf8(test::read_body(resp).await.to_vec()).unwrap();
assert!(res.contains(&site.hostname));
let msg = Password {
password: PASSWORD.into(),
};
let resp = test::call_service(
&app,
post_request!(&msg, delete_site, FORM)
.cookie(cookies.clone())
.to_request(),
)
.await;
// delete_request!(&app, delete_site, cookies.clone(), &msg, FORM);
assert_eq!(resp.status(), StatusCode::FOUND);
let headers = resp.headers();
assert_eq!(
headers.get(actix_web::http::header::LOCATION).unwrap(),
PAGES.dash.home,
);
assert!(!utils::get_website_path(&ctx.settings, &site.hostname).exists());
assert_eq!(
ctx.db
.get_site_from_pub_id(site.pub_id, NAME.into())
.await
.err(),
Some(ServiceError::WebsiteNotFound)
);
let mut events = ctx.db.list_all_site_events(&site.hostname).await.unwrap();
let possible_delete = events.pop().unwrap();
assert_eq!(&possible_delete.event_type, &*crate::db::EVENT_TYPE_DELETE);
let _ = ctx.delete_user(NAME, PASSWORD).await;
}
}

View File

@ -1,185 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::cell::RefCell;
use actix_identity::Identity;
use actix_web::http::header::ContentType;
use serde::{Deserialize, Serialize};
use tera::Context;
use uuid::Uuid;
use super::get_auth_middleware;
use crate::ctx::api::v1::auth::{Login, Password};
use crate::db::Site;
use crate::pages::dash::TemplateSiteEvent;
use crate::pages::errors::*;
use crate::settings::Settings;
use crate::AppCtx;
pub use super::*;
pub const DASH_SITE_DELETE: TemplateFile =
TemplateFile::new("dash_site_delete", "pages/dash/sites/delete.html");
const SHOW_DEPLOY_SECRET_KEY: &str = "show_deploy_secret";
pub struct Delete {
ctx: RefCell<Context>,
}
impl CtxError for Delete {
fn with_error(&self, e: &ReadableError) -> String {
self.ctx.borrow_mut().insert(ERROR_KEY, e);
self.render()
}
}
impl Delete {
pub fn new(settings: &Settings, payload: Option<TemplateSiteWithEvents>) -> Self {
let ctx = RefCell::new(context(settings));
if let Some(payload) = payload {
ctx.borrow_mut().insert(PAYLOAD_KEY, &payload);
}
Self { ctx }
}
pub fn show_deploy_secret(&mut self) {
self.ctx.borrow_mut().insert(SHOW_DEPLOY_SECRET_KEY, &true);
}
pub fn render(&self) -> String {
TEMPLATES
.render(DASH_SITE_DELETE.name, &self.ctx.borrow())
.unwrap()
}
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub struct TemplateSiteWithEvents {
pub site: Site,
pub delete: String,
pub last_update: Option<TemplateSiteEvent>,
pub events: Vec<TemplateSiteEvent>,
}
impl TemplateSiteWithEvents {
pub fn new(
site: Site,
last_update: Option<TemplateSiteEvent>,
events: Vec<TemplateSiteEvent>,
) -> Self {
let delete = PAGES.dash.site.get_delete(site.pub_id);
Self {
site,
last_update,
delete,
events,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
pub struct DeleteOptions {
show_deploy_secret: Option<bool>,
}
#[actix_web_codegen_const_routes::get(
path = "PAGES.dash.site.delete",
wrap = "get_auth_middleware()"
)]
#[tracing::instrument(name = "Dashboard delete site webpage", skip(ctx, id))]
pub async fn get_delete_site(
ctx: AppCtx,
id: Identity,
path: web::Path<Uuid>,
query: web::Query<DeleteOptions>,
) -> PageResult<impl Responder, Delete> {
let site_id = path.into_inner();
let owner = id.identity().unwrap();
let site = ctx
.db
.get_site_from_pub_id(site_id, owner)
.await
.map_err(|e| PageError::new(Delete::new(&ctx.settings, None), e))?;
let last_update = ctx
.db
.get_latest_update_event(&site.hostname)
.await
.map_err(|e| PageError::new(Delete::new(&ctx.settings, None), e))?;
let last_update = last_update.map(|e| e.into());
let mut db_events = ctx
.db
.list_all_site_events(&site.hostname)
.await
.map_err(|e| PageError::new(Delete::new(&ctx.settings, None), e))?;
let mut events = Vec::with_capacity(db_events.len());
for e in db_events.drain(0..) {
events.push(e.into());
}
let payload = TemplateSiteWithEvents::new(site, last_update, events);
let mut page = Delete::new(&ctx.settings, Some(payload));
if let Some(true) = query.show_deploy_secret {
page.show_deploy_secret();
}
let add = page.render();
let html = ContentType::html();
Ok(HttpResponse::Ok().content_type(html).body(add))
}
#[actix_web_codegen_const_routes::post(
path = "PAGES.dash.site.delete",
wrap = "get_auth_middleware()"
)]
#[tracing::instrument(name = "Delete site from webpage", skip(ctx, id))]
pub async fn post_delete_site(
ctx: AppCtx,
id: Identity,
path: web::Path<Uuid>,
payload: web::Form<Password>,
) -> PageResult<impl Responder, Delete> {
let site_id = path.into_inner();
let owner = id.identity().unwrap();
let payload = payload.into_inner();
let msg = Login {
login: owner,
password: payload.password,
};
ctx.login(&msg)
.await
.map_err(|e| PageError::new(Delete::new(&ctx.settings, None), e))?;
ctx.delete_site(msg.login, site_id)
.await
.map_err(|e| PageError::new(Delete::new(&ctx.settings, None), e))?;
Ok(HttpResponse::Found()
.append_header((http::header::LOCATION, PAGES.dash.home))
.finish())
}
pub fn services(cfg: &mut web::ServiceConfig) {
cfg.service(get_delete_site);
cfg.service(post_delete_site);
}

View File

@ -1,43 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use actix_web::*;
use super::get_auth_middleware;
pub use super::home::TemplateSite;
pub use super::{context, Footer, TemplateFile, PAGES, PAYLOAD_KEY, TEMPLATES};
pub mod add;
pub mod delete;
pub mod view;
pub fn register_templates(t: &mut tera::Tera) {
add::DASH_SITE_ADD
.register(t)
.expect(add::DASH_SITE_ADD.name);
view::DASH_SITE_VIEW
.register(t)
.expect(view::DASH_SITE_VIEW.name);
delete::DASH_SITE_DELETE
.register(t)
.expect(delete::DASH_SITE_DELETE.name);
}
pub fn services(cfg: &mut web::ServiceConfig) {
add::services(cfg);
view::services(cfg);
delete::services(cfg);
}

View File

@ -1,154 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::cell::RefCell;
use actix_identity::Identity;
use actix_web::http::header::ContentType;
use serde::{Deserialize, Serialize};
use tera::Context;
use uuid::Uuid;
use super::get_auth_middleware;
use crate::db::Site;
use crate::pages::dash::TemplateSiteEvent;
use crate::pages::errors::*;
use crate::settings::Settings;
use crate::AppCtx;
pub use super::*;
pub const DASH_SITE_VIEW: TemplateFile =
TemplateFile::new("dash_site_view", "pages/dash/sites/view.html");
const SHOW_DEPLOY_SECRET_KEY: &str = "show_deploy_secret";
pub struct View {
ctx: RefCell<Context>,
}
impl CtxError for View {
fn with_error(&self, e: &ReadableError) -> String {
self.ctx.borrow_mut().insert(ERROR_KEY, e);
self.render()
}
}
impl View {
pub fn new(settings: &Settings, payload: Option<TemplateSiteWithEvents>) -> Self {
let ctx = RefCell::new(context(settings));
if let Some(payload) = payload {
ctx.borrow_mut().insert(PAYLOAD_KEY, &payload);
}
Self { ctx }
}
pub fn show_deploy_secret(&mut self) {
self.ctx.borrow_mut().insert(SHOW_DEPLOY_SECRET_KEY, &true);
}
pub fn render(&self) -> String {
TEMPLATES
.render(DASH_SITE_VIEW.name, &self.ctx.borrow())
.unwrap()
}
}
#[derive(Clone, Debug, Deserialize, Serialize, Eq, PartialEq)]
pub struct TemplateSiteWithEvents {
pub site: Site,
pub view: String,
pub delete: String,
pub last_update: Option<TemplateSiteEvent>,
pub events: Vec<TemplateSiteEvent>,
}
impl TemplateSiteWithEvents {
pub fn new(
site: Site,
last_update: Option<TemplateSiteEvent>,
events: Vec<TemplateSiteEvent>,
) -> Self {
let view = PAGES.dash.site.get_view(site.pub_id);
let delete = PAGES.dash.site.get_delete(site.pub_id);
Self {
site,
last_update,
view,
delete,
events,
}
}
}
#[derive(Serialize, Deserialize, Clone, Debug, Eq, PartialEq)]
pub struct ViewOptions {
show_deploy_secret: Option<bool>,
}
#[actix_web_codegen_const_routes::get(
path = "PAGES.dash.site.view",
wrap = "get_auth_middleware()"
)]
#[tracing::instrument(name = "Dashboard add site webpage", skip(ctx, id))]
pub async fn get_view_site(
ctx: AppCtx,
id: Identity,
path: web::Path<Uuid>,
query: web::Query<ViewOptions>,
) -> PageResult<impl Responder, View> {
let site_id = path.into_inner();
let owner = id.identity().unwrap();
let site = ctx
.db
.get_site_from_pub_id(site_id, owner)
.await
.map_err(|e| PageError::new(View::new(&ctx.settings, None), e))?;
let last_update = ctx
.db
.get_latest_update_event(&site.hostname)
.await
.map_err(|e| PageError::new(View::new(&ctx.settings, None), e))?;
let last_update = last_update.map(|e| e.into());
let mut db_events = ctx
.db
.list_all_site_events(&site.hostname)
.await
.map_err(|e| PageError::new(View::new(&ctx.settings, None), e))?;
let mut events = Vec::with_capacity(db_events.len());
for e in db_events.drain(0..) {
events.push(e.into());
}
let payload = TemplateSiteWithEvents::new(site, last_update, events);
let mut page = View::new(&ctx.settings, Some(payload));
if let Some(true) = query.show_deploy_secret {
page.show_deploy_secret();
}
let add = page.render();
let html = ContentType::html();
Ok(HttpResponse::Ok().content_type(html).body(add))
}
pub fn services(cfg: &mut web::ServiceConfig) {
cfg.service(get_view_site);
}

View File

@ -1,105 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::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>>;

View File

@ -1,203 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use actix_identity::Identity;
use actix_web::http::header;
use actix_web::*;
use lazy_static::lazy_static;
use rust_embed::RustEmbed;
use serde::*;
use tera::*;
use crate::settings::Settings;
use crate::static_assets::ASSETS;
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,
}
}
}
pub async fn home(id: &Identity) -> HttpResponse {
let location = if id.identity().is_some() {
PAGES.home
} 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(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,
super::dash::sites::add::DASH_SITE_ADD,
]
.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);
}
}
}

View File

@ -1,168 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use actix_auth_middleware::{Authentication, GetLoginRoute};
use serde::*;
use uuid::Uuid;
/// 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,
}
impl Pages {
/// create new instance of Routes
const fn new() -> Pages {
let auth = Auth::new();
let dash = Dash::new();
let home = auth.login;
Pages { auth, home, dash }
}
}
#[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 site: DashSite,
pub forgejo_webhook: ForgejoWebhook,
}
impl Dash {
/// create new instance of Dash route
pub const fn new() -> Dash {
let home = "/dash";
let site = DashSite::new();
let forgejo_webhook = ForgejoWebhook::new();
Dash {
home,
site,
forgejo_webhook,
}
}
}
#[derive(Serialize)]
/// Dashboard ForgejoWebhook routes
pub struct ForgejoWebhook {
/// add forgejo webhook route
pub add: &'static str,
/// view forgejo webhook route
pub view: &'static str,
/// list forgejo webhooks route
pub list: &'static str,
}
impl ForgejoWebhook {
/// create new instance of ForgejoWebhook route
pub const fn new() -> ForgejoWebhook {
let add = "/dash/forgejo/webhook/add";
let list = "/dash/forgejo/webhook/list";
let view = "/dash/forgejo/webhook/view/{auth_token}";
ForgejoWebhook { add, view, list }
}
pub fn get_view(&self, auth_token: &str) -> String {
self.view.replace("{auth_token}", auth_token)
}
}
#[derive(Serialize)]
/// Dashboard Site routes
pub struct DashSite {
/// add site route
pub add: &'static str,
/// view site route
pub view: &'static str,
/// delete site route
pub delete: &'static str,
}
impl DashSite {
/// create new instance of DashSite route
pub const fn new() -> DashSite {
let add = "/dash/site/add";
let view = "/dash/site/view/{deployment_pub_id}";
let delete = "/dash/site/delete/{deployment_pub_id}";
DashSite { add, view, delete }
}
pub fn get_view(&self, deployment_pub_id: Uuid) -> String {
self.view.replace(
"{deployment_pub_id}",
deployment_pub_id.to_string().as_ref(),
)
}
pub fn get_delete(&self, deployment_pub_id: Uuid) -> String {
self.delete.replace(
"{deployment_pub_id}",
deployment_pub_id.to_string().as_ref(),
)
}
}
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()
}
}
}

View File

@ -1,116 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use crate::AppCtx;
pub struct Preview<'a> {
pub base: &'a str,
pub delimiter: &'static str,
pub prefix: &'static str,
}
impl<'a> Preview<'a> {
pub fn new(ctx: &'a AppCtx) -> Self {
Self {
base: &ctx.settings.page.base_domain,
delimiter: ".",
prefix: "deploy-preview-",
}
}
pub fn get_name(&self, preview_number: usize) -> String {
format!(
"{}{preview_number}{}{}",
self.prefix, self.delimiter, self.base
)
}
pub fn extract(&self, hostname: &'a str) -> Option<&'a str> {
if !hostname.contains(self.delimiter)
|| !hostname.contains(self.prefix)
|| !hostname.contains(self.base)
{
return None;
}
let d = format!("{}{}", self.delimiter, self.base);
if hostname.split(&d).count() == 2 {
return hostname.split(&d).next();
}
None
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn preview_extract_generate_works() {
const BASE_DOMAIN: &str = "librepages.site";
const PREVIEW_DELIMITER: &str = ".";
const PREVIEW_PREFIX: &str = "deploy-preview-";
const PREVIEW_NUMBER: usize = 1666;
let preview_hostname =
format!("{PREVIEW_PREFIX}{PREVIEW_NUMBER}{PREVIEW_DELIMITER}{BASE_DOMAIN}");
let bad_hostname = BASE_DOMAIN.to_string();
let extractor = Preview {
base: BASE_DOMAIN,
prefix: PREVIEW_PREFIX,
delimiter: PREVIEW_DELIMITER,
};
assert_eq!(extractor.get_name(PREVIEW_NUMBER), preview_hostname);
assert_eq!(extractor.extract(&bad_hostname), None);
assert_eq!(
extractor.extract(&format!(
"{PREVIEW_PREFIX}{PREVIEW_NUMBER}{PREVIEW_DELIMITER}no_base_domain"
)),
None
);
assert_eq!(
extractor.extract(&format!(
"{PREVIEW_PREFIX}{PREVIEW_NUMBER}no-delimiter{BASE_DOMAIN}"
)),
None
);
assert_eq!(
extractor.extract(&format!(
"{PREVIEW_PREFIX}{PREVIEW_NUMBER}no-delimiter{BASE_DOMAIN}"
)),
None
);
assert_eq!(
extractor.extract(&format!(
"noprefix{PREVIEW_NUMBER}{PREVIEW_DELIMITER}{BASE_DOMAIN}"
)),
None
);
assert_eq!(
extractor.extract(&preview_hostname),
Some(format!("{PREVIEW_PREFIX}{PREVIEW_NUMBER}").as_str())
);
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@ -14,10 +14,28 @@
* 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 forgejo;
pub mod pages;
use actix_web::web;
#[cfg(test)]
mod tests;
use crate::deploy::routes::Deploy;
use crate::meta::routes::Meta;
pub const ROUTES: Routes = Routes::new();
pub struct Routes {
pub meta: Meta,
pub deploy: Deploy,
}
impl Routes {
pub const fn new() -> Self {
Self {
meta: Meta::new(),
deploy: Deploy::new(),
}
}
}
pub fn services(cfg: &mut web::ServiceConfig) {
crate::meta::services(cfg);
crate::deploy::services(cfg);
}

View File

@ -1,98 +0,0 @@
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::{web, HttpRequest, HttpResponse, Responder};
use crate::errors::*;
use crate::pages;
use crate::AppCtx;
pub mod routes {
pub struct Serve {
pub catch_all: &'static str,
}
impl Serve {
pub const fn new() -> Self {
Self {
catch_all: "/{path:.*}",
}
}
}
}
#[actix_web_codegen_const_routes::get(path = "crate::V1_API_ROUTES.serve.catch_all")]
#[tracing::instrument(name = "Serve webpages", skip(req, ctx, id))]
async fn index(req: HttpRequest, ctx: AppCtx, id: Identity) -> ServiceResult<impl Responder> {
let c = req.connection_info();
let mut host = c.host();
if host.contains(':') {
host = host.split(':').next().unwrap();
}
tracing::debug!("Current host {host}");
// serve meta page
if host == ctx.settings.server.domain || host == "localhost" {
tracing::debug!("Into home");
return Ok(pages::home(&id).await);
}
// serve default hostname content
if host.contains(&ctx.settings.page.base_domain) {
let extractor = crate::preview::Preview::new(&ctx);
if let Some(preview_branch) = extractor.extract(host) {
let res = if ctx.db.hostname_exists(host).await? {
let path = crate::utils::get_website_path(&ctx.settings, host);
let content =
crate::git::read_preview_file(&path, preview_branch, req.uri().path())?;
let mime = if let Some(mime) = content.mime.first_raw() {
mime
} else {
"text/html; charset=utf-8"
};
Ok(HttpResponse::Ok()
.content_type(mime)
.body(content.content.bytes()))
} else {
Err(ServiceError::WebsiteNotFound)
};
return res;
}
}
// TODO: custom domains.
if ctx.db.hostname_exists(host).await? {
let path = crate::utils::get_website_path(&ctx.settings, host);
let content = crate::git::read_file(&path, req.uri().path())?;
let mime = if let Some(mime) = content.mime.first_raw() {
mime
} else {
"text/html; charset=utf-8"
};
Ok(HttpResponse::Ok()
.content_type(mime)
.body(content.content.bytes()))
} else {
Err(ServiceError::WebsiteNotFound)
}
}
pub fn services(cfg: &mut web::ServiceConfig) {
cfg.service(index);
}

View File

@ -1,5 +1,5 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
* Copyright (C) 2021 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
@ -19,25 +19,19 @@ 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 log::warn;
use serde::Deserialize;
use serde::Serialize;
use url::Url;
use crate::errors::*;
use crate::page::Page;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Deserialize)]
pub struct Server {
pub port: u32,
pub ip: String,
pub workers: Option<usize>,
pub cookie_secret: String,
pub domain: String,
pub ip: String,
pub proxy_has_tls: bool,
pub workers: Option<usize>,
}
impl Server {
@ -47,152 +41,102 @@ impl Server {
}
}
#[derive(Deserialize, Serialize, Display, Eq, PartialEq, Clone, Debug)]
#[derive(Deserialize, Display, Clone, Debug)]
#[serde(rename_all = "lowercase")]
pub enum DBType {
#[display(fmt = "postgres")]
Postgres,
// #[display(fmt = "maria")]
// Maria,
pub enum LogLevel {
#[display(fmt = "debug")]
Debug,
#[display(fmt = "info")]
Info,
#[display(fmt = "trace")]
Trace,
#[display(fmt = "error")]
Error,
#[display(fmt = "warn")]
Warn,
}
impl DBType {
fn from_url(url: &Url) -> Result<Self, ConfigError> {
match url.scheme() {
// "mysql" => Ok(Self::Maria),
"postgres" => Ok(Self::Postgres),
_ => Err(ConfigError::Message("Unknown database type".into())),
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Database {
pub url: String,
pub pool: u32,
pub database_type: DBType,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Deserialize)]
pub struct Settings {
pub allow_registration: bool,
pub support_email: String,
pub debug: bool,
pub log: LogLevel,
// pub database: Database,
pub server: Server,
pub source_code: String,
pub database: Database,
pub page: PageConfig,
pub conductors: Vec<Conductor>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Conductor {
pub username: String,
pub api_key: String,
pub url: Url,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PageConfig {
pub base_path: String,
pub base_domain: String,
pub pages: Vec<Page>,
}
#[cfg(not(tarpaulin_include))]
impl Settings {
pub fn new() -> ServiceResult<Self> {
let mut s = Config::builder();
pub fn new() -> Result<Self, ConfigError> {
let mut s = Config::new();
// setting default values
#[cfg(test)]
s.set_default("database.pool", 2.to_string())
.expect("Couldn't get the number of CPUs");
const CURRENT_DIR: &str = "./config/default.toml";
const ETC: &str = "/etc/static-pages/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() {
if let Ok(path) = env::var("ATHENA_CONFIG") {
s.merge(File::with_name(&path))?;
} else if Path::new(CURRENT_DIR).exists() {
// merging default config from file
s = s.add_source(File::with_name(CURRENT_DIR));
read_file = true;
s.merge(File::with_name(CURRENT_DIR))?;
} else if Path::new(ETC).exists() {
s.merge(File::with_name(ETC))?;
} else {
log::warn!("configuration file not found");
}
if let Ok(path) = env::var("PAGES_CONFIG") {
s = s.add_source(File::with_name(&path));
read_file = true;
}
s.merge(Environment::with_prefix("PAGES").separator("__"))?;
if !read_file {
warn!("configuration file not found");
}
s = s.add_source(Environment::with_prefix("PAGES").separator("__"));
check_url(&s);
match env::var("PORT") {
Ok(val) => {
s = s.set_override("server.port", val).unwrap();
s.set("server.port", val).unwrap();
}
Err(e) => warn!("couldn't interpret PORT: {}", e),
}
let intermediate_config = s.build_cloned().unwrap();
let settings: Settings = s.try_into()?;
s = s
.set_override(
"database.url",
format!(
r"postgres://{}:{}@{}:{}/{}",
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 url = Url::parse(&val).expect("couldn't parse Database URL");
s = s.set_override("database.url", url.to_string()).unwrap();
let database_type = DBType::from_url(&url).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)
}
pub fn init(&self) {
fn create_dir_util(path: &Path) {
for (index, page) in settings.pages.iter().enumerate() {
Url::parse(&page.repo).unwrap();
let path = Path::new(&page.path);
if path.exists() && path.is_file() {
panic!("Path is a file, should be a directory: {:?}", path);
panic!("Path is a file, should be a directory: {:?}", page);
}
if !path.exists() {
std::fs::create_dir_all(path).unwrap();
std::fs::create_dir_all(&path).unwrap();
}
for (index2, page2) in settings.pages.iter().enumerate() {
if index2 == index {
continue;
}
if page.secret == page2.secret || page.repo == page2.repo || page.path == page2.path
{
panic!("duplicate page onfiguration {:?} and {:?}", page, page2);
}
}
page.fetch_upstream(&page.branch);
}
create_dir_util(Path::new(&self.page.base_path));
}
#[cfg(not(tarpaulin_include))]
fn check_url(&self) {
Url::parse(&self.source_code).expect("Please enter a URL for source_code in settings");
const LOG_VAR: &str = "RUST_LOG";
if env::var(LOG_VAR).is_err() {
env::set_var("RUST_LOG", format!("{}", settings.log));
}
Ok(settings)
}
}
#[cfg(not(tarpaulin_include))]
fn check_url(s: &Config) {
let url = s
.get::<String>("source_code")
.expect("Couldn't access source_code");
Url::parse(&url).expect("Please enter a URL for source_code in settings");
}

View File

@ -1,46 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use 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"));
}
}

View File

@ -1,73 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use actix_web::*;
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)]
pub struct Svg {
pub eye_off: &'static str,
pub eye: &'static str,
}
impl Svg {
/// create new instance of Routes
pub fn new() -> Svg {
Svg {
eye: &static_files::assets::CSS,
eye_off: &static_files::assets::CSS,
}
}
}
#[derive(Serialize)]
/// Top-level routes data structure for V1 AP1
pub struct Assets {
/// Authentication routes
pub css: &'static str,
pub mobile_css: &'static str,
pub svg: Svg,
}
impl Assets {
/// create new instance of Routes
pub fn new() -> Assets {
Assets {
css: &static_files::assets::CSS,
mobile_css: &static_files::assets::CSS,
svg: Svg::new(),
}
}
}
}

View File

@ -1,97 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::borrow::Cow;
use actix_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 EYE: &'static str = FILES.get("./static/cache/img/svg/eye.svg").unwrap();
pub static ref EYE_OFF: &'static str =
FILES.get("./static/cache/img/svg/eye-off.svg").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);
}
}
}

View File

@ -1,972 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use crate::settings::Settings;
// source: https://www.randomlists.com/data/nouns.json
const LEN: usize = 876;
const WORDLIST: [&str; LEN] = [
"account",
"achiever",
"acoustics",
"act",
"action",
"activity",
"actor",
"addition",
"adjustment",
"advertisement",
"advice",
"aftermath",
"afternoon",
"afterthought",
"agreement",
"air",
"airplane",
"airport",
"alarm",
"amount",
"amusement",
"anger",
"angle",
"animal",
"ants",
"apparatus",
"apparel",
"appliance",
"approval",
"arch",
"argument",
"arithmetic",
"arm",
"army",
"art",
"attack",
"attraction",
"aunt",
"authority",
"babies",
"baby",
"back",
"badge",
"bag",
"bait",
"balance",
"ball",
"base",
"baseball",
"basin",
"basket",
"basketball",
"bat",
"bath",
"battle",
"bead",
"bear",
"bed",
"bedroom",
"beds",
"bee",
"beef",
"beginner",
"behavior",
"belief",
"believe",
"bell",
"bells",
"berry",
"bike",
"bikes",
"bird",
"birds",
"birth",
"birthday",
"bit",
"bite",
"blade",
"blood",
"blow",
"board",
"boat",
"bomb",
"bone",
"book",
"books",
"boot",
"border",
"bottle",
"boundary",
"box",
"boy",
"brake",
"branch",
"brass",
"breath",
"brick",
"bridge",
"brother",
"bubble",
"bucket",
"building",
"bulb",
"burst",
"bushes",
"business",
"butter",
"button",
"cabbage",
"cable",
"cactus",
"cake",
"cakes",
"calculator",
"calendar",
"camera",
"camp",
"can",
"cannon",
"canvas",
"cap",
"caption",
"car",
"card",
"care",
"carpenter",
"carriage",
"cars",
"cart",
"cast",
"cat",
"cats",
"cattle",
"cause",
"cave",
"celery",
"cellar",
"cemetery",
"cent",
"chalk",
"chance",
"change",
"channel",
"cheese",
"cherries",
"cherry",
"chess",
"chicken",
"chickens",
"children",
"chin",
"church",
"circle",
"clam",
"class",
"cloth",
"clover",
"club",
"coach",
"coal",
"coast",
"coat",
"cobweb",
"coil",
"collar",
"color",
"committee",
"company",
"comparison",
"competition",
"condition",
"connection",
"control",
"cook",
"copper",
"corn",
"cough",
"country",
"cover",
"cow",
"cows",
"crack",
"cracker",
"crate",
"crayon",
"cream",
"creator",
"creature",
"credit",
"crib",
"crime",
"crook",
"crow",
"crowd",
"crown",
"cub",
"cup",
"current",
"curtain",
"curve",
"cushion",
"dad",
"daughter",
"day",
"death",
"debt",
"decision",
"deer",
"degree",
"design",
"desire",
"desk",
"destruction",
"detail",
"development",
"digestion",
"dime",
"dinner",
"dinosaurs",
"direction",
"dirt",
"discovery",
"discussion",
"distance",
"distribution",
"division",
"dock",
"doctor",
"dog",
"dogs",
"doll",
"dolls",
"donkey",
"door",
"downtown",
"drain",
"drawer",
"dress",
"drink",
"driving",
"drop",
"duck",
"ducks",
"dust",
"ear",
"earth",
"earthquake",
"edge",
"education",
"effect",
"egg",
"eggnog",
"eggs",
"elbow",
"end",
"engine",
"error",
"event",
"example",
"exchange",
"existence",
"expansion",
"experience",
"expert",
"eye",
"eyes",
"face",
"fact",
"fairies",
"fall",
"fang",
"farm",
"fear",
"feeling",
"field",
"finger",
"fire",
"fireman",
"fish",
"flag",
"flame",
"flavor",
"flesh",
"flight",
"flock",
"floor",
"flower",
"flowers",
"fly",
"fog",
"fold",
"food",
"foot",
"force",
"fork",
"form",
"fowl",
"frame",
"friction",
"friend",
"friends",
"frog",
"frogs",
"front",
"fruit",
"fuel",
"furniture",
"gate",
"geese",
"ghost",
"giants",
"giraffe",
"girl",
"girls",
"glass",
"glove",
"gold",
"government",
"governor",
"grade",
"grain",
"grandfather",
"grandmother",
"grape",
"grass",
"grip",
"ground",
"group",
"growth",
"guide",
"guitar",
"gun",
"hair",
"haircut",
"hall",
"hammer",
"hand",
"hands",
"harbor",
"harmony",
"hat",
"hate",
"head",
"health",
"heat",
"hill",
"history",
"hobbies",
"hole",
"holiday",
"home",
"honey",
"hook",
"hope",
"horn",
"horse",
"horses",
"hose",
"hospital",
"hot",
"hour",
"house",
"houses",
"humor",
"hydrant",
"ice",
"icicle",
"idea",
"impulse",
"income",
"increase",
"industry",
"ink",
"insect",
"instrument",
"insurance",
"interest",
"invention",
"iron",
"island",
"jail",
"jam",
"jar",
"jeans",
"jelly",
"jellyfish",
"jewel",
"join",
"judge",
"juice",
"jump",
"kettle",
"key",
"kick",
"kiss",
"kittens",
"kitty",
"knee",
"knife",
"knot",
"knowledge",
"laborer",
"lace",
"ladybug",
"lake",
"lamp",
"land",
"language",
"laugh",
"leather",
"leg",
"legs",
"letter",
"letters",
"lettuce",
"level",
"library",
"limit",
"line",
"linen",
"lip",
"liquid",
"loaf",
"lock",
"locket",
"look",
"loss",
"love",
"low",
"lumber",
"lunch",
"lunchroom",
"machine",
"magic",
"maid",
"mailbox",
"man",
"marble",
"mark",
"market",
"mask",
"mass",
"match",
"meal",
"measure",
"meat",
"meeting",
"memory",
"men",
"metal",
"mice",
"middle",
"milk",
"mind",
"mine",
"minister",
"mint",
"minute",
"mist",
"mitten",
"mom",
"money",
"monkey",
"month",
"moon",
"morning",
"mother",
"motion",
"mountain",
"mouth",
"move",
"muscle",
"name",
"nation",
"neck",
"need",
"needle",
"nerve",
"nest",
"night",
"noise",
"north",
"nose",
"note",
"notebook",
"number",
"nut",
"oatmeal",
"observation",
"ocean",
"offer",
"office",
"oil",
"orange",
"oranges",
"order",
"oven",
"page",
"pail",
"pan",
"pancake",
"paper",
"parcel",
"part",
"partner",
"party",
"passenger",
"payment",
"peace",
"pear",
"pen",
"pencil",
"person",
"pest",
"pet",
"pets",
"pickle",
"picture",
"pie",
"pies",
"pig",
"pigs",
"pin",
"pipe",
"pizzas",
"place",
"plane",
"planes",
"plant",
"plantation",
"plants",
"plastic",
"plate",
"play",
"playground",
"pleasure",
"plot",
"plough",
"pocket",
"point",
"poison",
"pollution",
"popcorn",
"porter",
"position",
"pot",
"potato",
"powder",
"power",
"price",
"produce",
"profit",
"property",
"prose",
"protest",
"pull",
"pump",
"punishment",
"purpose",
"push",
"quarter",
"quartz",
"queen",
"question",
"quicksand",
"quiet",
"quill",
"quilt",
"quince",
"quiver",
"rabbit",
"rabbits",
"rail",
"railway",
"rain",
"rainstorm",
"rake",
"range",
"rat",
"rate",
"ray",
"reaction",
"reading",
"reason",
"receipt",
"recess",
"record",
"regret",
"relation",
"religion",
"representative",
"request",
"respect",
"rest",
"reward",
"rhythm",
"rice",
"riddle",
"rifle",
"ring",
"rings",
"river",
"road",
"robin",
"rock",
"rod",
"roll",
"roof",
"room",
"root",
"rose",
"route",
"rub",
"rule",
"run",
"sack",
"sail",
"salt",
"sand",
"scale",
"scarecrow",
"scarf",
"scene",
"scent",
"school",
"science",
"scissors",
"screw",
"sea",
"seashore",
"seat",
"secretary",
"seed",
"selection",
"self",
"sense",
"servant",
"shade",
"shake",
"shame",
"shape",
"sheep",
"sheet",
"shelf",
"ship",
"shirt",
"shock",
"shoe",
"shoes",
"shop",
"show",
"side",
"sidewalk",
"sign",
"silk",
"silver",
"sink",
"sister",
"sisters",
"size",
"skate",
"skin",
"skirt",
"sky",
"slave",
"sleep",
"sleet",
"slip",
"slope",
"smash",
"smell",
"smile",
"smoke",
"snail",
"snails",
"snake",
"snakes",
"sneeze",
"snow",
"soap",
"society",
"sock",
"soda",
"sofa",
"son",
"song",
"songs",
"sort",
"sound",
"soup",
"space",
"spade",
"spark",
"spiders",
"sponge",
"spoon",
"spot",
"spring",
"spy",
"square",
"squirrel",
"stage",
"stamp",
"star",
"start",
"statement",
"station",
"steam",
"steel",
"stem",
"step",
"stew",
"stick",
"sticks",
"stitch",
"stocking",
"stomach",
"stone",
"stop",
"store",
"story",
"stove",
"stranger",
"straw",
"stream",
"street",
"stretch",
"string",
"structure",
"substance",
"sugar",
"suggestion",
"suit",
"summer",
"sun",
"support",
"surprise",
"sweater",
"swim",
"swing",
"system",
"table",
"tail",
"talk",
"tank",
"taste",
"tax",
"teaching",
"team",
"teeth",
"temper",
"tendency",
"tent",
"territory",
"test",
"texture",
"theory",
"thing",
"things",
"thought",
"thread",
"thrill",
"throat",
"throne",
"thumb",
"thunder",
"ticket",
"tiger",
"time",
"tin",
"title",
"toad",
"toe",
"toes",
"tomatoes",
"tongue",
"tooth",
"toothbrush",
"toothpaste",
"top",
"touch",
"town",
"toy",
"toys",
"trade",
"trail",
"train",
"trains",
"tramp",
"transport",
"tray",
"treatment",
"tree",
"trees",
"trick",
"trip",
"trouble",
"trousers",
"truck",
"trucks",
"tub",
"turkey",
"turn",
"twig",
"twist",
"umbrella",
"uncle",
"underwear",
"unit",
"use",
"vacation",
"value",
"van",
"vase",
"vegetable",
"veil",
"vein",
"verse",
"vessel",
"vest",
"view",
"visitor",
"voice",
"volcano",
"volleyball",
"voyage",
"walk",
"wall",
"war",
"wash",
"waste",
"watch",
"water",
"wave",
"waves",
"wax",
"way",
"wealth",
"weather",
"week",
"weight",
"wheel",
"whip",
"whistle",
"wilderness",
"wind",
"window",
"wine",
"wing",
"winter",
"wire",
"wish",
"woman",
"women",
"wood",
"wool",
"word",
"work",
"worm",
"wound",
"wren",
"wrench",
"wrist",
"writer",
"writing",
"yak",
"yam",
"yard",
"yarn",
"year",
"yoke",
"zebra",
"zephyr",
"zinc",
"zipper",
"zoo",
];
struct ID<'a> {
first: &'a str,
second: &'a str,
third: &'a str,
}
impl<'a> ID<'a> {
fn hostname(&self, base_domain: &str) -> String {
format!(
"{}-{}-{}.{}",
self.first, self.second, self.third, base_domain
)
}
}
fn get_random_id() -> ID<'static> {
use rand::{rngs::ThreadRng, thread_rng, Rng};
let mut rng: ThreadRng = thread_rng();
let first: usize = rng.gen_range(0..LEN);
let mut second: usize;
let mut third: usize;
loop {
second = rng.gen_range(0..LEN);
if second != first {
break;
}
}
loop {
third = rng.gen_range(0..LEN);
if third != first && second != third {
break;
}
}
let first = WORDLIST[first];
let second = WORDLIST[second];
let third = WORDLIST[third];
ID {
first,
second,
third,
}
}
pub fn get_random_subdomain(s: &Settings) -> String {
let id = get_random_id();
id.hostname(&s.page.base_domain)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_subdomains() {
// test random ID
let id = get_random_id();
assert_ne!(id.first, id.second);
assert_ne!(id.first, id.third);
assert_ne!(id.third, id.second);
// test ID::hostname
let delimiter = "foobar21312";
assert_eq!(
id.hostname(delimiter),
format!("{}-{}-{}.{delimiter}", id.first, id.second, id.third,)
);
}
}

View File

@ -1,291 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use std::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 const REPO_URL: &str = "https://github.com/mCaptcha/website/";
pub const BRANCH: &str = "gh-pages";
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 mut 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");
settings.page.base_path = page_base_path.to_str().unwrap().into();
settings.init();
println!("[log] Initialzing settings again with test config");
settings.init();
(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
};
($app:expr, $route:expr, $cookies:expr, $serializable:expr, FORM) => {
test::call_service(
&$app,
test::TestRequest::delete()
.uri($route)
.set_form($serializable)
.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 {
let msg = AddSite {
repo_url: REPO_URL.into(),
branch: BRANCH.into(),
owner,
};
self.add_site(msg).await.unwrap()
}
}

View File

@ -1,36 +0,0 @@
/*
* Copyright (C) 2022 Aravinth Manivannan <realaravinth@batsense.net>
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
use crate::Settings;
use std::path::{Path, PathBuf};
/// 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, hostname: &str) -> PathBuf {
Path::new(&s.page.base_path).join(hostname)
}

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="52" height="52" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="icon" class="feather feather-eye-off"><path d="M17.94 17.94A10.07 10.07 0 0 1 12 20c-7 0-11-8-11-8a18.45 18.45 0 0 1 5.06-5.94M9.9 4.24A9.12 9.12 0 0 1 12 4c7 0 11 8 11 8a18.5 18.5 0 0 1-2.16 3.19m-6.72-1.07a3 3 0 1 1-4.24-4.24"></path><line x1="1" y1="1" x2="23" y2="23"></line></svg>

Before

Width:  |  Height:  |  Size: 470 B

View File

@ -1 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" width="52" height="52" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" id="icon" class="feather feather-eye"><path d="M1 12s4-8 11-8 11 8 11 8-4 8-11 8-11-8-11-8z"></path><circle cx="12" cy="12" r="3"></circle></svg>

Before

Width:  |  Height:  |  Size: 326 B

View File

@ -1,14 +0,0 @@
<!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>{% block title %} {% endblock %} | LibrePages</title>
</head>
<body class="default-body">
<header>{% block nav %} {% endblock %}</header>
{% block main %} {% endblock %}
{% include "footer" %}
</body>
</html>

View File

@ -1,6 +0,0 @@
{% if error %}
<div class="error_container">
<h3 class="error-title">ERROR: {{ error.title }}</h3>
<p class="error-message">{{ error.reason }}</p>
</div>
{% endif %}

View File

@ -1,37 +0,0 @@
<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>

View File

@ -1,28 +0,0 @@
<nav class="nav__container">
<input type="checkbox" class="nav__toggle" id="nav__toggle" />
<div class="nav__header">
<a class="nav__logo-container" href="{{page.dash.home}}">
<p class="nav__home-btn">LibrePages</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.dash.site.add }}">New Site</a>
</div>
<div class="nav__link-container">
<a class="nav__link" rel="noreferrer" href="{{ page.dash.forgejo_webhook.list }}">Webhooks</a>
</div>
<div class="nav__link-container">
<a class="nav__link" rel="noreferrer" href="{{ page.auth.logout }}">Log out</a>
</div>
</div>
</nav>

View File

@ -1,18 +0,0 @@
<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">LibrePages</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>

View File

@ -1,26 +0,0 @@
<nav class="nav__container">
<input type="checkbox" class="nav__toggle" id="nav__toggle" />
<div class="nav__header">
<a class="nav__logo-container" href="{{ page.dash.home }}">
<p class="nav__home-btn">LibrePages</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="https://docs.librepages.org">Docs</a>
</div>
<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>

View File

@ -1,112 +0,0 @@
@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;
}

View File

@ -1,141 +0,0 @@
//@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;
}

View File

@ -1,5 +0,0 @@
@mixin fullscreen {
height: 100vh;
min-height: 500px;
// max-height: 800px;
}

View File

@ -1,4 +0,0 @@
@mixin a_hover {
color: rgb(0, 86, 179);
text-decoration: underline;
}

View File

@ -1,79 +0,0 @@
@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;
}

View File

@ -1,52 +0,0 @@
$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;
}

View File

@ -1,70 +0,0 @@
* {
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;
}
}

View File

@ -1,14 +0,0 @@
@import "defaults.scss";
@import "pages/auth/sass/main.scss";
@import "pages/auth/sass/form/main.scss";
@import "pages/dash/main.scss";
@import "pages/dash/sites/main.scss";
@import "components/sass/footer/main.scss";
@import "components/nav/sass/main.scss";
.default-body {
display: flex;
@include fullscreen;
flex-direction: column;
justify-content: space-between;
}

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