mirror of
https://github.com/clowzed/sero
synced 2026-03-14 12:45:50 +01:00
Release: 0.2.0. The servr works fine. Tested
This commit is contained in:
parent
bc8fd6de9c
commit
3f5701f6eb
184 changed files with 7337 additions and 2843 deletions
|
|
@ -6,3 +6,12 @@ readme.md
|
|||
logo.svg
|
||||
.github
|
||||
nginx-templates
|
||||
assets/
|
||||
rustfmt.toml
|
||||
openapi.json
|
||||
logs
|
||||
sero-docs
|
||||
scripts
|
||||
.env
|
||||
.env.tests
|
||||
CODE_OF_CONDUCT.md
|
||||
|
|
|
|||
11
.env.tests
Normal file
11
.env.tests
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
DATABASE_URL=postgres://postgres:postgres@localhost:5432/postgres
|
||||
JWT_SECRET=mysuperstrongjwtsecret
|
||||
PORT=8080
|
||||
JWT_TTL_SECONDS=120
|
||||
MAX_USERS=10000
|
||||
SQLX_LOGGING=true
|
||||
UPLOAD_FOLDER=./test_upload_files
|
||||
MAX_SITES_PER_USER=100
|
||||
MAX_BODY_LIMIT_SIZE=1000000
|
||||
CLEAN_OBSOLETE_INTERVAL=60
|
||||
RUST_LOG=none,sero=trace
|
||||
31
.github/workflows/build.yml
vendored
31
.github/workflows/build.yml
vendored
|
|
@ -1,31 +0,0 @@
|
|||
name: docker build latest master
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
-
|
||||
name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
-
|
||||
name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: clowzed
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
-
|
||||
name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
-
|
||||
name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: clowzed/sero:latest
|
||||
107
.github/workflows/release.yml
vendored
Normal file
107
.github/workflows/release.yml
vendored
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
name: "Release"
|
||||
|
||||
permissions:
|
||||
contents: "write"
|
||||
|
||||
on:
|
||||
workflow_run:
|
||||
workflows: ["Tag"]
|
||||
types:
|
||||
- "completed"
|
||||
|
||||
jobs:
|
||||
get-tag:
|
||||
name: "Get tag version from Cargo.toml"
|
||||
runs-on: "ubuntu-latest"
|
||||
outputs:
|
||||
pkg-version: ${{ steps.pkg-version.outputs.PKG_VERSION }}
|
||||
steps:
|
||||
- name: "Check out the repo"
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
token: ${{ secrets.G_TOKEN }}
|
||||
|
||||
- name: "Get tag"
|
||||
id: "pkg-version"
|
||||
shell: "bash"
|
||||
run: |
|
||||
echo PKG_VERSION=$(awk -F ' = ' '$1 ~ /version/ { gsub(/["]/, "", $2); printf("%s",$2) }' Cargo.toml) >> $GITHUB_OUTPUT
|
||||
|
||||
create-release:
|
||||
name: "Create release"
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
needs:
|
||||
- "get-tag"
|
||||
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- name: "Check out the repo"
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: "Create release"
|
||||
uses: "taiki-e/create-gh-release-action@v1"
|
||||
with:
|
||||
# (optional) Path to changelog.
|
||||
# changelog: CHANGELOG.md
|
||||
branch: "master"
|
||||
ref: refs/tags/v${{ needs.get-tag.outputs.pkg-version }}
|
||||
token: ${{ secrets.G_TOKEN }}
|
||||
|
||||
upload-assets:
|
||||
name: "Upload assets to Github releases"
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
needs:
|
||||
- "get-tag"
|
||||
- "create-release"
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- target: "x86_64-unknown-linux-gnu"
|
||||
os: "ubuntu-latest"
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
steps:
|
||||
- name: "Check out the repo"
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: "Upload Binaries"
|
||||
uses: "taiki-e/upload-rust-binary-action@v1"
|
||||
with:
|
||||
bin: "sero"
|
||||
target: ${{ matrix.target }}
|
||||
archive: $bin-${{ matrix.target }}
|
||||
ref: refs/tags/v${{ needs.get-tag.outputs.pkg-version }}
|
||||
token: ${{ secrets.G_TOKEN }}
|
||||
|
||||
push-to-registry:
|
||||
name: "Build and push Docker image to Docker Hub"
|
||||
if: ${{ github.event.workflow_run.conclusion == 'success' }}
|
||||
needs:
|
||||
- "get-tag"
|
||||
- "upload-assets"
|
||||
- "publish-api"
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- name: "Check out the repo"
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- name: "Log in to Docker Hub"
|
||||
uses: "docker/login-action@v2"
|
||||
with:
|
||||
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||
|
||||
- name: "Extract metadata (tags, labels) for Docker"
|
||||
id: "meta"
|
||||
uses: "docker/metadata-action@v4"
|
||||
with:
|
||||
images: "bwks/shazam"
|
||||
|
||||
- name: "Build and push Docker image"
|
||||
uses: "docker/build-push-action@v3"
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
tags: clowzed/sero:v${{ needs.get-tag.outputs.pkg-version }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
30
.github/workflows/tag.yml
vendored
Normal file
30
.github/workflows/tag.yml
vendored
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
name: "Tag"
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- "master"
|
||||
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
jobs:
|
||||
create-tag:
|
||||
name: "Create git tag"
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- name: "Check out the repo"
|
||||
uses: actions/checkout@v3
|
||||
with:
|
||||
token: ${{ secrets.G_TOKEN }}
|
||||
|
||||
- name: "Get tag from Cargo.toml"
|
||||
id: "get-tag"
|
||||
shell: "bash"
|
||||
run: |
|
||||
echo PKG_VERSION=$(awk -F ' = ' '$1 ~ /version/ { gsub(/["]/, "", $2); printf("%s",$2) }' Cargo.toml) >> $GITHUB_OUTPUT
|
||||
|
||||
- name: "Set git tag"
|
||||
shell: "bash"
|
||||
run: |
|
||||
git tag v${{ steps.get-tag.outputs.PKG_VERSION }} && git push --tags
|
||||
108
.github/workflows/test.yml
vendored
Normal file
108
.github/workflows/test.yml
vendored
Normal file
|
|
@ -0,0 +1,108 @@
|
|||
name: "Test"
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
push:
|
||||
branches:
|
||||
- "**" # matches every branch
|
||||
|
||||
jobs:
|
||||
check:
|
||||
name: "Run cargo check"
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- name: "Check out the repo"
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- uses: "actions-rs/toolchain@v1"
|
||||
with:
|
||||
profile: "minimal"
|
||||
toolchain: "stable"
|
||||
override: true
|
||||
|
||||
- uses: "actions-rs/cargo@v1"
|
||||
with:
|
||||
command: "check"
|
||||
|
||||
tests:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres:16
|
||||
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
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@v3
|
||||
- name: "Get version from Cargo.toml"
|
||||
id: "get-cargo-version"
|
||||
shell: "bash"
|
||||
run: |
|
||||
echo PKG_VERSION=$(awk -F ' = ' '$1 ~ /version/ { gsub(/["]/, "", $2); printf("%s",$2) }' Cargo.toml) >> $GITHUB_OUTPUT
|
||||
- name: Get version from openapi.json
|
||||
id: get-openapi-version
|
||||
run: |
|
||||
echo OAPI_VERSION=$(jq -r '.info.version' openapi.json) >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Compare versions
|
||||
run: |
|
||||
if [ "${{ steps.get-cargo-version.outputs.PKG_VERSION }}" != "${{ steps.get-openapi-version.outputs.OAPI_VERSION }}" ]; then
|
||||
echo "Version mismatch between cargo.toml and generated OpenAPI JSON."
|
||||
exit 1
|
||||
else
|
||||
echo "Version matches between cargo.toml and generated OpenAPI JSON."
|
||||
fi
|
||||
- name: Run tests (with database service)
|
||||
run: cargo test --verbose -- --test-threads=1
|
||||
|
||||
fmt:
|
||||
name: "Run cargo fmt"
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- name: "Check out the repo"
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- uses: "actions-rs/toolchain@v1"
|
||||
with:
|
||||
profile: "minimal"
|
||||
toolchain: "stable"
|
||||
override: true
|
||||
|
||||
- run: "rustup component add rustfmt"
|
||||
|
||||
- uses: "actions-rs/cargo@v1"
|
||||
with:
|
||||
command: "+nightly fmt"
|
||||
args: "--all -- --check"
|
||||
|
||||
clippy:
|
||||
name: "Run cargo clippy"
|
||||
runs-on: "ubuntu-latest"
|
||||
steps:
|
||||
- name: "Check out the repo"
|
||||
uses: actions/checkout@v3
|
||||
|
||||
- uses: "actions-rs/toolchain@v1"
|
||||
with:
|
||||
profile: "minimal"
|
||||
toolchain: "stable"
|
||||
override: true
|
||||
|
||||
- run: "rustup component add clippy"
|
||||
|
||||
- uses: "actions-rs/cargo@v1"
|
||||
with:
|
||||
command: "clippy"
|
||||
args: "-- -D warnings"
|
||||
27
.github/workflows/testable.yml
vendored
27
.github/workflows/testable.yml
vendored
|
|
@ -1,27 +0,0 @@
|
|||
name: docker build latest testable
|
||||
|
||||
on:
|
||||
push:
|
||||
branches-ignore:
|
||||
- master
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v4
|
||||
- name: Login to Docker Hub
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
username: clowzed
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
push: true
|
||||
tags: clowzed/sero:testable
|
||||
35
.github/workflows/tests.yml
vendored
35
.github/workflows/tests.yml
vendored
|
|
@ -1,35 +0,0 @@
|
|||
name: Run tests
|
||||
|
||||
on: [push]
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
services:
|
||||
postgres:
|
||||
image: postgres
|
||||
|
||||
env:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
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: Build executable
|
||||
run: cargo build --verbose
|
||||
- name: Run tests
|
||||
run: cargo test --verbose -- --test-threads=1
|
||||
env:
|
||||
DATABASE_URL: "postgres://postgres:postgres@localhost:5432/postgres"
|
||||
PORT: 8080
|
||||
|
||||
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -1,3 +1,6 @@
|
|||
/target
|
||||
Cargo.lock
|
||||
[0-9a-zA-Z]*-[0-9a-zA-Z]*-[0-9a-zA-Z]*-[0-9a-zA-Z]*-[0-9a-zA-Z]*
|
||||
sites-uploads
|
||||
logs
|
||||
test_upload_files
|
||||
.env
|
||||
|
|
|
|||
79
Cargo.toml
79
Cargo.toml
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "sero"
|
||||
version = "0.1.0"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
authors = ["clowzed <clowzed.work@gmail.com>"]
|
||||
description = "Muiltidomain static site hosting"
|
||||
|
|
@ -9,48 +9,73 @@ readme = "readme.md"
|
|||
license = "MIT"
|
||||
|
||||
[dependencies]
|
||||
axum = { version = "0.6.20", features = ["macros", "tracing", "headers", "multipart"] }
|
||||
envy = "0.4.2"
|
||||
sea-orm = { version = "0.12.3", features = ["sqlx-postgres", "runtime-tokio-rustls", "macros"] }
|
||||
sea-orm = { version = "0.12.3", features = [
|
||||
"sqlx-postgres",
|
||||
"runtime-tokio-rustls",
|
||||
"macros",
|
||||
] }
|
||||
tokio = { version = "1.32.0", features = ["full"] }
|
||||
tokio-postgres = "0.7.10"
|
||||
tracing = "0.1.37"
|
||||
tracing-subscriber = { version = "0.3.17", features = ["env-filter", "fmt", "ansi", "std", "json"] }
|
||||
tracing = { version = "0.1.37", features = ["async-await"] }
|
||||
tracing-subscriber = { version = "0.3.17", features = [
|
||||
"env-filter",
|
||||
"fmt",
|
||||
"ansi",
|
||||
"std",
|
||||
"json",
|
||||
] }
|
||||
entity = { path = "entity" }
|
||||
migration = { path = "migration" }
|
||||
serde = { version = "1.0.188", features = ["derive"] }
|
||||
jsonwebtoken = "8.3.0"
|
||||
axum-extra = { version = "0.8.0", features = ["cookie"] }
|
||||
sha256 = "1.4.0"
|
||||
jsonwebtoken = "9.2.0"
|
||||
thiserror = "1.0.49"
|
||||
chrono = "0.4.31"
|
||||
futures-util = "0.3.28"
|
||||
axum_typed_multipart = "0.10.0"
|
||||
uuid = { version = "1.4.1", features = ["v4"] }
|
||||
async_zip = { version = "0.0.15", features = ["tokio-fs", "tokio", "full"] }
|
||||
async_zip = { version = "0.0.17", features = ["full"] }
|
||||
bytes = "1.5.0"
|
||||
async-trait = "0.1.73"
|
||||
tokio-util = { version = "0.7.9", features = ["io"] }
|
||||
dotenv = "0.15.0"
|
||||
tower-http = { version = "0.4.4", features = ["cors"] }
|
||||
futures = "0.3.29"
|
||||
tower = { version = "0.4.13", features = ["util"] }
|
||||
serde_urlencoded = "0.7.1"
|
||||
serde_json = "1.0.108"
|
||||
mime = "0.3.17"
|
||||
axum-test-helper = "0.3.0"
|
||||
hyper = "0.14.27"
|
||||
reqwest = { version = "0.11.22", features = ["json", "multipart"] }
|
||||
oneshot = "0.1.6"
|
||||
crossfire = "1.0.1"
|
||||
http-body = "0.4.5"
|
||||
mime_guess = "2.0.4"
|
||||
argon2 = { version = "0.5.3", features = ["std"] }
|
||||
utoipa = { version = "4.2.0", features = [
|
||||
"axum_extras",
|
||||
"chrono",
|
||||
"preserve_order",
|
||||
] }
|
||||
dotenvy = "0.15.7"
|
||||
toml = "0.8.8"
|
||||
tokio-util = { version = "0.7.10", features = ["io"] }
|
||||
utoipa-rapidoc = { version = "4.0.0", features = ["axum"] }
|
||||
utoipa-redoc = { version = "4.0.0", features = ["axum"] }
|
||||
utoipa-swagger-ui = { version = "7.1.0", features = ["axum"] }
|
||||
axum = { version = "0.7.4", features = [
|
||||
"macros",
|
||||
"tracing",
|
||||
"json",
|
||||
"multipart",
|
||||
] }
|
||||
axum_typed_multipart = "0.11.0"
|
||||
tower-http = { git = "https://github.com/tower-rs/tower-http.git", features = [
|
||||
"cors",
|
||||
"trace",
|
||||
"timeout",
|
||||
] }
|
||||
tower = { version = "0.4.13", features = ["util"] }
|
||||
hyper = "0.14.28"
|
||||
utoipauto = "0.1.10"
|
||||
validator = { version = "0.18.0", features = ["derive"] }
|
||||
axum-valid = "0.18.0"
|
||||
sea-query = "0.30.7"
|
||||
serde_with = "3.8.1"
|
||||
tracing-appender = "0.2.3"
|
||||
|
||||
[workspace]
|
||||
members = [".", "entity", "migration"]
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
strip = true
|
||||
opt-level = 3
|
||||
codegen-units = 1
|
||||
|
||||
[dev-dependencies]
|
||||
|
||||
axum-test = "15.2.0"
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
FROM rust:1.73 as builder
|
||||
FROM rust:latest as builder
|
||||
WORKDIR /usr/src/app
|
||||
COPY . .
|
||||
RUN cargo build --release
|
||||
|
|
|
|||
69
Makefile.toml
Normal file
69
Makefile.toml
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
[tasks.start-postgres]
|
||||
command = "sudo"
|
||||
args = [
|
||||
"docker",
|
||||
"run",
|
||||
"--name",
|
||||
"postgres-test",
|
||||
"-d",
|
||||
"-e",
|
||||
"POSTGRES_PASSWORD=postgres",
|
||||
"-e",
|
||||
"POSTGRES_USER=postgres",
|
||||
"-e",
|
||||
"POSTGRES_DB=postgres",
|
||||
"-p",
|
||||
"5432:5432",
|
||||
"--health-cmd=pg_isready",
|
||||
"--health-interval=10s",
|
||||
"--health-timeout=5s",
|
||||
"--health-retries=5",
|
||||
"postgres:16",
|
||||
]
|
||||
|
||||
[tasks.stop-postgres]
|
||||
script = ['''
|
||||
sudo docker stop postgres-test || echo "Postgres container not running"
|
||||
''']
|
||||
|
||||
[tasks.remove-postgres]
|
||||
script = ['''
|
||||
sudo docker rm -v postgres-test || echo "Postgres container not found"
|
||||
''']
|
||||
|
||||
|
||||
[tasks.wait-for-postgres]
|
||||
script = ['''
|
||||
while ! sudo docker exec postgres-test pg_isready -U postgres; do
|
||||
echo "Waiting for postgres to be ready..."
|
||||
sleep 2
|
||||
done
|
||||
echo "Postgres is ready!"
|
||||
''']
|
||||
|
||||
[tasks.run-tests]
|
||||
env = { "RUST_BACKTRACE" = "0" }
|
||||
command = "cargo"
|
||||
args = ["test", "--", "--test-threads=1", "--nocapture", "--show-output"]
|
||||
|
||||
[tasks.cleanup]
|
||||
script = [
|
||||
'''
|
||||
sudo docker stop postgres-test && sudo docker rm -v postgres-test|| echo "Postgres container not found"
|
||||
''',
|
||||
]
|
||||
|
||||
[tasks.flow]
|
||||
dependencies = [
|
||||
"cleanup",
|
||||
"start-postgres",
|
||||
"wait-for-postgres",
|
||||
"run-tests",
|
||||
"cleanup",
|
||||
]
|
||||
|
||||
[config]
|
||||
skip_core_tasks = true
|
||||
|
||||
[env]
|
||||
CARGO_MAKE_EXTEND_WORKSPACE_MAKEFILE = true
|
||||
BIN
assets/a.zip
BIN
assets/a.zip
Binary file not shown.
BIN
assets/zips/correct-1.zip
Normal file
BIN
assets/zips/correct-1.zip
Normal file
Binary file not shown.
BIN
assets/zips/correct-2.zip
Normal file
BIN
assets/zips/correct-2.zip
Normal file
Binary file not shown.
BIN
assets/zips/correct-3.zip
Normal file
BIN
assets/zips/correct-3.zip
Normal file
Binary file not shown.
BIN
assets/zips/correct-big.zip
Normal file
BIN
assets/zips/correct-big.zip
Normal file
Binary file not shown.
BIN
assets/zips/correct-with-404.html.zip
Normal file
BIN
assets/zips/correct-with-404.html.zip
Normal file
Binary file not shown.
BIN
assets/zips/correct-with-503.html.zip
Normal file
BIN
assets/zips/correct-with-503.html.zip
Normal file
Binary file not shown.
BIN
assets/zips/correct-without-404.html.zip
Normal file
BIN
assets/zips/correct-without-404.html.zip
Normal file
Binary file not shown.
BIN
assets/zips/correct-without-503.html.zip
Normal file
BIN
assets/zips/correct-without-503.html.zip
Normal file
Binary file not shown.
0
assets/zips/empty.zip
Normal file
0
assets/zips/empty.zip
Normal file
|
|
@ -2,7 +2,7 @@ version: "3"
|
|||
|
||||
services:
|
||||
database:
|
||||
image: postgres:15
|
||||
image: postgres:16
|
||||
user: postgres
|
||||
restart: always
|
||||
environment:
|
||||
|
|
@ -20,7 +20,7 @@ services:
|
|||
|
||||
|
||||
proxy:
|
||||
image: nginx
|
||||
image: nginx:alpine3.18-slim
|
||||
restart: always
|
||||
environment:
|
||||
- DOLLAR=$
|
||||
|
|
@ -37,6 +37,8 @@ services:
|
|||
- 80:80
|
||||
links:
|
||||
- server
|
||||
profiles:
|
||||
- donotstart
|
||||
|
||||
server:
|
||||
image: clowzed/sero
|
||||
|
|
@ -46,7 +48,9 @@ services:
|
|||
database:
|
||||
condition: service_healthy
|
||||
volumes:
|
||||
- server-files:/app
|
||||
- server-files:/app/sites-uploads
|
||||
ports:
|
||||
- 8080:8080
|
||||
environment:
|
||||
- DATABASE_URL=postgresql://postgres:1234@database/sero
|
||||
- PORT=8080
|
||||
|
|
@ -54,9 +58,14 @@ services:
|
|||
# Empty means no limits
|
||||
- MAX_USERS=1
|
||||
- MAX_SITES_PER_USER=100
|
||||
- MAX_BODY_LIMIT_BYTES=10000000
|
||||
- MAX_BODY_LIMIT_SIZE=10000000 # 10mb
|
||||
- RUST_LOG=none,sero=trace
|
||||
# end of section
|
||||
- JWT_SECRET=mysuperstrongjwtscret
|
||||
- JWT_TTL_SECONDS=120
|
||||
- SQLX_LOGGING=true
|
||||
- UPLOAD_FOLDER=./sites-uploads
|
||||
|
||||
|
||||
volumes:
|
||||
server-files:
|
||||
|
|
|
|||
|
|
@ -6,4 +6,7 @@ edition = "2021"
|
|||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
async-trait = "0.1.80"
|
||||
sea-orm = { version = "0.12" }
|
||||
serde = { version = "1.0.196", features = ["derive"] }
|
||||
utoipa = { version = "4.2.3", features = ["axum_extras", "chrono", "preserve_order"] }
|
||||
|
|
|
|||
|
|
@ -1,16 +1,18 @@
|
|||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.3
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "file")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub subdomain_id: i32,
|
||||
pub id: i64,
|
||||
pub subdomain_id: Option<i64>,
|
||||
pub user_path: String,
|
||||
#[sea_orm(unique)]
|
||||
pub real_path: String,
|
||||
pub obsolete: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
|
@ -20,7 +22,7 @@ pub enum Relation {
|
|||
from = "Column::SubdomainId",
|
||||
to = "super::subdomain::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "Cascade"
|
||||
on_delete = "SetNull"
|
||||
)]
|
||||
Subdomain,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.3
|
||||
|
||||
pub mod prelude;
|
||||
|
||||
pub mod cors;
|
||||
pub mod file;
|
||||
pub mod origin;
|
||||
pub mod subdomain;
|
||||
pub mod user;
|
||||
|
|
|
|||
|
|
@ -1,8 +0,0 @@
|
|||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.3
|
||||
|
||||
pub mod prelude;
|
||||
|
||||
pub mod cors;
|
||||
pub mod file;
|
||||
pub mod subdomain;
|
||||
pub mod user;
|
||||
|
|
@ -1,14 +1,16 @@
|
|||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.3
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[sea_orm(table_name = "cors")]
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize, ToSchema)]
|
||||
#[sea_orm(table_name = "origin")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub subdomain_id: i32,
|
||||
pub origin: String,
|
||||
pub id: i64,
|
||||
pub subdomain_id: i64,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
|
@ -30,9 +32,3 @@ impl Related<super::subdomain::Entity> for Entity {
|
|||
}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
impl Model {
|
||||
pub fn matches(&self, origin: &str) -> bool {
|
||||
self.origin == "*" || self.origin == origin
|
||||
}
|
||||
}
|
||||
|
|
@ -1,19 +1,19 @@
|
|||
pub use super::cors::Entity as CorsEntity;
|
||||
pub use super::file::Entity as FileEntity;
|
||||
pub use super::subdomain::Entity as SubdomainEntity;
|
||||
pub use super::user::Entity as UserEntity;
|
||||
pub use super::{
|
||||
file::Entity as FileEntity, origin::Entity as OriginEntity, subdomain::Entity as SubdomainEntity,
|
||||
user::Entity as UserEntity,
|
||||
};
|
||||
|
||||
pub use super::cors::ActiveModel as ActiveCors;
|
||||
pub use super::file::ActiveModel as ActiveFile;
|
||||
pub use super::subdomain::ActiveModel as ActiveSubdomain;
|
||||
pub use super::user::ActiveModel as ActiveUser;
|
||||
pub use super::{
|
||||
file::Model as FileModel, origin::Model as OriginModel, subdomain::Model as SubdomainModel,
|
||||
user::Model as UserModel,
|
||||
};
|
||||
|
||||
pub use super::cors::Model as Cors;
|
||||
pub use super::file::Model as File;
|
||||
pub use super::subdomain::Model as Subdomain;
|
||||
pub use super::user::Model as User;
|
||||
pub use super::{
|
||||
file::Column as FileColumn, origin::Column as OriginColumn, subdomain::Column as SubdomainColumn,
|
||||
user::Column as UserColumn,
|
||||
};
|
||||
|
||||
pub use super::cors::Column as CorsColumn;
|
||||
pub use super::file::Column as FileColumn;
|
||||
pub use super::subdomain::Column as SubdomainColumn;
|
||||
pub use super::user::Column as UserColumn;
|
||||
pub use super::{
|
||||
file::ActiveModel as FileActiveModel, origin::ActiveModel as OriginActiveModel,
|
||||
subdomain::ActiveModel as SubdomainActiveModel, user::ActiveModel as UserActiveModel,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,13 +1,14 @@
|
|||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.3
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "subdomain")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub owner_id: i32,
|
||||
pub id: i64,
|
||||
pub owner_id: i64,
|
||||
pub enabled: bool,
|
||||
#[sea_orm(unique)]
|
||||
pub name: String,
|
||||
|
|
@ -17,32 +18,32 @@ pub struct Model {
|
|||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {
|
||||
#[sea_orm(has_many = "super::cors::Entity")]
|
||||
Cors,
|
||||
#[sea_orm(has_many = "super::file::Entity")]
|
||||
File,
|
||||
#[sea_orm(has_many = "super::origin::Entity")]
|
||||
Origin,
|
||||
#[sea_orm(
|
||||
belongs_to = "super::user::Entity",
|
||||
from = "Column::OwnerId",
|
||||
to = "super::user::Column::Id",
|
||||
on_update = "NoAction",
|
||||
on_delete = "NoAction"
|
||||
on_delete = "Cascade"
|
||||
)]
|
||||
User,
|
||||
}
|
||||
|
||||
impl Related<super::cors::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Cors.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::file::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::File.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::origin::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Origin.def()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::user::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::User.def()
|
||||
|
|
|
|||
|
|
@ -1,14 +1,17 @@
|
|||
//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.3
|
||||
|
||||
use sea_orm::entity::prelude::*;
|
||||
use std::fmt::Debug;
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)]
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, PartialEq, DeriveEntityModel, Eq, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "user")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub id: i64,
|
||||
#[sea_orm(unique)]
|
||||
pub username: String,
|
||||
pub login: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
|
|
@ -18,6 +21,16 @@ pub enum Relation {
|
|||
Subdomain,
|
||||
}
|
||||
|
||||
impl Debug for Model {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("Model")
|
||||
.field("id", &self.id)
|
||||
.field("login", &self.login)
|
||||
.field("password", &"***")
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl Related<super::subdomain::Entity> for Entity {
|
||||
fn to() -> RelationDef {
|
||||
Relation::Subdomain.def()
|
||||
|
|
|
|||
|
|
@ -2,8 +2,8 @@ pub use sea_orm_migration::prelude::*;
|
|||
|
||||
mod m20230927_162921_create_users;
|
||||
mod m20230929_081415_create_subdomains;
|
||||
mod m20230929_152215_create_files;
|
||||
mod m20231105_171000_create_cors;
|
||||
mod m20230929_152215_create_file;
|
||||
mod m20231105_171000_create_origin;
|
||||
|
||||
pub struct Migrator;
|
||||
|
||||
|
|
@ -13,8 +13,8 @@ impl MigratorTrait for Migrator {
|
|||
vec![
|
||||
Box::new(m20230927_162921_create_users::Migration),
|
||||
Box::new(m20230929_081415_create_subdomains::Migration),
|
||||
Box::new(m20230929_152215_create_files::Migration),
|
||||
Box::new(m20231105_171000_create_cors::Migration),
|
||||
Box::new(m20230929_152215_create_file::Migration),
|
||||
Box::new(m20231105_171000_create_origin::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,27 +13,33 @@ impl MigrationTrait for Migration {
|
|||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(User::Id)
|
||||
.integer()
|
||||
.big_integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(User::Username)
|
||||
.string()
|
||||
.not_null()
|
||||
.unique_key(),
|
||||
)
|
||||
.col(ColumnDef::new(User::Login).string().not_null().unique_key())
|
||||
.col(ColumnDef::new(User::Password).string().not_null())
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
.await?;
|
||||
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.unique()
|
||||
.name("user-login-idx")
|
||||
.table(User::Table)
|
||||
.col(User::Login)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(User::Table).to_owned())
|
||||
.await
|
||||
manager.drop_table(Table::drop().table(User::Table).to_owned()).await
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -41,6 +47,6 @@ impl MigrationTrait for Migration {
|
|||
pub enum User {
|
||||
Table,
|
||||
Id,
|
||||
Username,
|
||||
Login,
|
||||
Password,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,34 +13,36 @@ impl MigrationTrait for Migration {
|
|||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(Subdomain::Id)
|
||||
.integer()
|
||||
.big_integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(ColumnDef::new(Subdomain::OwnerId).integer().not_null())
|
||||
.col(ColumnDef::new(Subdomain::OwnerId).big_integer().not_null())
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.name("FK_user_subdomain")
|
||||
.from(Subdomain::Table, Subdomain::OwnerId)
|
||||
.to(User::Table, User::Id),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Subdomain::Enabled)
|
||||
.boolean()
|
||||
.not_null()
|
||||
.default(true),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Subdomain::Name)
|
||||
.string()
|
||||
.not_null()
|
||||
.unique_key(),
|
||||
.to(User::Table, User::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.col(ColumnDef::new(Subdomain::Enabled).boolean().not_null().default(true))
|
||||
.col(ColumnDef::new(Subdomain::Name).string().not_null().unique_key())
|
||||
.col(ColumnDef::new(Subdomain::ArchivePath).string().unique_key())
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
.await?;
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.unique()
|
||||
.name("subdomain-name-idx")
|
||||
.table(Subdomain::Table)
|
||||
.col(Subdomain::Name)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
|
|
|
|||
|
|
@ -15,34 +15,43 @@ impl MigrationTrait for Migration {
|
|||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(File::Id)
|
||||
.integer()
|
||||
.big_integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(ColumnDef::new(File::SubdomainId).integer().not_null())
|
||||
.col(ColumnDef::new(File::SubdomainId).big_integer())
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.from(File::Table, File::SubdomainId)
|
||||
.to(Subdomain::Table, Subdomain::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade),
|
||||
.on_delete(ForeignKeyAction::SetNull),
|
||||
)
|
||||
.col(ColumnDef::new(File::UserPath).string().not_null())
|
||||
.col(ColumnDef::new(File::RealPath).string().not_null().unique_key())
|
||||
.col(
|
||||
ColumnDef::new(File::RealPath)
|
||||
.string()
|
||||
ColumnDef::new(File::Obsolete)
|
||||
.boolean()
|
||||
.not_null()
|
||||
.unique_key(),
|
||||
.default(Expr::val(false)),
|
||||
)
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
.await?;
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("file-user-path-idx")
|
||||
.table(File::Table)
|
||||
.col(File::UserPath)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(File::Table).to_owned())
|
||||
.await
|
||||
manager.drop_table(Table::drop().table(File::Table).to_owned()).await
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -53,4 +62,5 @@ enum File {
|
|||
SubdomainId,
|
||||
UserPath,
|
||||
RealPath,
|
||||
Obsolete,
|
||||
}
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
use sea_orm_migration::prelude::*;
|
||||
|
||||
use crate::m20230929_081415_create_subdomains::Subdomain;
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
|
@ -11,39 +10,37 @@ impl MigrationTrait for Migration {
|
|||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Cors::Table)
|
||||
.table(Origin::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(Cors::Id)
|
||||
.integer()
|
||||
ColumnDef::new(Origin::Id)
|
||||
.big_integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(ColumnDef::new(Cors::SubdomainId).integer().not_null())
|
||||
.col(ColumnDef::new(Origin::SubdomainId).big_integer().not_null())
|
||||
.foreign_key(
|
||||
ForeignKey::create()
|
||||
.from(Cors::Table, Cors::SubdomainId)
|
||||
.from(Origin::Table, Origin::SubdomainId)
|
||||
.to(Subdomain::Table, Subdomain::Id)
|
||||
.on_delete(ForeignKeyAction::Cascade),
|
||||
)
|
||||
.col(ColumnDef::new(Cors::Origin).string().not_null())
|
||||
.col(ColumnDef::new(Origin::Value).string().not_null())
|
||||
.to_owned(),
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(Cors::Table).to_owned())
|
||||
.await
|
||||
manager.drop_table(Table::drop().table(Origin::Table).to_owned()).await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
pub enum Cors {
|
||||
pub enum Origin {
|
||||
Table,
|
||||
Id,
|
||||
SubdomainId,
|
||||
Origin,
|
||||
Value,
|
||||
}
|
||||
|
|
@ -1,21 +1,18 @@
|
|||
server {
|
||||
|
||||
|
||||
# We check if $subdomain is not empty and if the X-subdomain header is not present in the request
|
||||
# ($http_x_subdomain = "").
|
||||
# If both conditions are met, we set the X-subdomain header using proxy_set_header.
|
||||
# Otherwise, we do nothing, and the existing X-subdomain header (if any) will be preserved.
|
||||
|
||||
listen 80;
|
||||
server_name ~^(?<subdomain>\w*)\.${DOMAIN}.${ZONE}${DOLLAR};
|
||||
|
||||
location / {
|
||||
if ($http_x_subdomain = "") {
|
||||
set $http_x_subdomain $subdomain;
|
||||
if ($subdomain != "" && $http_x_subdomain = "") {
|
||||
proxy_set_header x-subdomain $subdomain;
|
||||
}
|
||||
proxy_set_header X-Subdomain $http_x_subdomain;
|
||||
proxy_pass http://${SERVER}:${SERVER_PORT}/;
|
||||
}
|
||||
}
|
||||
|
||||
server {
|
||||
listen 80;
|
||||
server_name ${DOMAIN}.${ZONE};
|
||||
|
||||
location / {
|
||||
proxy_pass http://${SERVER}:${SERVER_PORT}/;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
1219
openapi.json
Normal file
1219
openapi.json
Normal file
File diff suppressed because it is too large
Load diff
437
openapi.yml
437
openapi.yml
|
|
@ -1,437 +0,0 @@
|
|||
openapi: 3.1.0
|
||||
info:
|
||||
title: sero API
|
||||
description: API of sero server
|
||||
version: 1.0.0
|
||||
|
||||
paths:
|
||||
/api/health:
|
||||
get:
|
||||
summary: Check that server is alive
|
||||
responses:
|
||||
"200":
|
||||
description: Server is alive
|
||||
|
||||
/api/login:
|
||||
post:
|
||||
summary: Authenticate user
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AuthCredentials"
|
||||
responses:
|
||||
"200":
|
||||
description: Authentication successful, returns AuthToken
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AuthToken"
|
||||
"400":
|
||||
description: Bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
"401":
|
||||
description: Authentication failed, unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
"500":
|
||||
description: Server error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
|
||||
/api/registration:
|
||||
post:
|
||||
summary: Register user
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
$ref: "#/components/schemas/AuthCredentials"
|
||||
responses:
|
||||
"200":
|
||||
description: Registration succeeded
|
||||
|
||||
"400":
|
||||
description: Bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
|
||||
"409":
|
||||
description: Registration failed, user has already been registered
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
"500":
|
||||
description: Server error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
/api/enable:
|
||||
post:
|
||||
summary: Enable site
|
||||
parameters:
|
||||
- in: header
|
||||
name: X-Subdomain
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
security:
|
||||
- bearerAuth: []
|
||||
|
||||
responses:
|
||||
"200":
|
||||
description: Site was successfully enabled
|
||||
|
||||
"403":
|
||||
description: Subdomain is owned by another user
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
"401":
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
"404":
|
||||
description: Subdomain was not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
"400":
|
||||
description: Bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
"500":
|
||||
description: Server error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
|
||||
/api/disable:
|
||||
post:
|
||||
summary: Enable site
|
||||
parameters:
|
||||
- in: header
|
||||
name: X-Subdomain
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
security:
|
||||
- bearerAuth: []
|
||||
|
||||
responses:
|
||||
"200":
|
||||
description: Site was successfully disabled
|
||||
|
||||
"403":
|
||||
description: Subdomain is owned by another user
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
"404":
|
||||
description: Subdomain was not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
"401":
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
"400":
|
||||
description: Bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
"500":
|
||||
description: Server error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
/api/teardown:
|
||||
post:
|
||||
summary: Enable site
|
||||
parameters:
|
||||
- in: header
|
||||
name: X-Subdomain
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
security:
|
||||
- bearerAuth: []
|
||||
|
||||
responses:
|
||||
"200":
|
||||
description: Site was deleted
|
||||
"403":
|
||||
description: Subdomain is owned by another user
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
"404":
|
||||
description: Subdomain was not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
"401":
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
"400":
|
||||
description: Bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
"500":
|
||||
description: Server error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
|
||||
/api/download:
|
||||
post:
|
||||
summary: Download site as zip
|
||||
parameters:
|
||||
- in: header
|
||||
name: X-Subdomain
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
security:
|
||||
- bearerAuth: []
|
||||
|
||||
responses:
|
||||
"200":
|
||||
description: Site was successfully downloaded
|
||||
content:
|
||||
application/octet-stream:
|
||||
schema:
|
||||
type: string
|
||||
format: binary
|
||||
|
||||
"403":
|
||||
description: Subdomain is owned by another user
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
"404":
|
||||
description: Subdomain was not found
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
"401":
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
"400":
|
||||
description: Bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
"500":
|
||||
description: Server error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
|
||||
/api/upload:
|
||||
post:
|
||||
summary: Upload site as zip
|
||||
parameters:
|
||||
- in: header
|
||||
name: X-Subdomain
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
content:
|
||||
multipart/form-data:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
archive:
|
||||
type: string
|
||||
format: binary
|
||||
|
||||
responses:
|
||||
"200":
|
||||
description: Site was successfully uploaded
|
||||
|
||||
"403":
|
||||
description: Subdomain is owned by another user
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
"401":
|
||||
description: Unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
"400":
|
||||
description: Bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
"500":
|
||||
description: Server error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
/api/cors/add:
|
||||
post:
|
||||
summary: Add origin
|
||||
parameters:
|
||||
- in: header
|
||||
name: X-Subdomain
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
security:
|
||||
- bearerAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/x-www-form-urlencoded:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Origin"
|
||||
responses:
|
||||
"200":
|
||||
description: Origin was successfully added
|
||||
"400":
|
||||
description: Bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
"401":
|
||||
description: Authentication failed, unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
"403":
|
||||
description: Subdomain is owned by another user
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
"500":
|
||||
description: Server error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
/api/cors/clear:
|
||||
post:
|
||||
summary: Clear related origins
|
||||
parameters:
|
||||
- in: header
|
||||
name: X-Subdomain
|
||||
schema:
|
||||
type: string
|
||||
required: true
|
||||
security:
|
||||
- bearerAuth: []
|
||||
|
||||
|
||||
responses:
|
||||
"200":
|
||||
description: Origins were successfully removed
|
||||
|
||||
"400":
|
||||
description: Bad request
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
"401":
|
||||
description: Authentication failed, unauthorized
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
"403":
|
||||
description: Subdomain is owned by another user
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
"500":
|
||||
description: Server error
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
$ref: "#/components/schemas/Details"
|
||||
|
||||
components:
|
||||
schemas:
|
||||
AuthCredentials:
|
||||
type: object
|
||||
properties:
|
||||
username:
|
||||
type: string
|
||||
password:
|
||||
type: string
|
||||
AuthToken:
|
||||
type: object
|
||||
properties:
|
||||
token:
|
||||
type: string
|
||||
Details:
|
||||
type: object
|
||||
properties:
|
||||
details:
|
||||
type: string
|
||||
Origin:
|
||||
type: object
|
||||
properties:
|
||||
origin:
|
||||
type: string
|
||||
|
||||
securitySchemes:
|
||||
bearerAuth:
|
||||
type: http
|
||||
scheme: bearer
|
||||
bearerFormat: JWT
|
||||
224
readme.md
224
readme.md
|
|
@ -1,6 +1,6 @@
|
|||
<br/>
|
||||
<p align="center">
|
||||
<img src="https://raw.githubusercontent.com/clowzed/sero/12c8a570755b2f0c1fb570167ce7dcc4b0c323e9/logo.svg" alt="Logo" width="80" height="80">
|
||||
<img src="https://raw.githubusercontent.com/clowzed/sero/12c8a570755b2f0c1fb570167ce7dcc4b0c323e9/logo.svg" alt="Logo" width="80" height="80">
|
||||
|
||||
<h3 align="center">sero</h3>
|
||||
|
||||
|
|
@ -16,56 +16,51 @@
|
|||

|
||||
|
||||

|
||||
</div>
|
||||
|
||||
</p>
|
||||
</div>
|
||||
</p>
|
||||
</p>
|
||||
|
||||
# Warning
|
||||
|
||||
> [!CAUTION]
|
||||
>
|
||||
> **_This project was in a huge rewrite and upload tool and docs are not updated!
|
||||
> THis will be fixed very soon._**
|
||||
|
||||
## 📖 Table Of Contents
|
||||
- [📖 Table Of Contents](#-table-of-contents)
|
||||
- [🔧 Tools](#-tools)
|
||||
- [❓ About The Project](#-about-the-project)
|
||||
- [🚀 Features](#-features)
|
||||
- [🔌 Built With](#-built-with)
|
||||
- [⌨️ Getting Started](#️-getting-started)
|
||||
- [Prerequisites](#prerequisites)
|
||||
- [Installation](#installation)
|
||||
- [Clone this repository](#clone-this-repository)
|
||||
- [✏️ Configure DNS records](#️-configure-dns-records)
|
||||
- [✏️ Configure `docker-compose.yml`](#️-configure-docker-composeyml)
|
||||
- [🚀 Deploy](#-deploy)
|
||||
- [✨ Usage](#-usage)
|
||||
- [Installing cli tool](#installing-cli-tool)
|
||||
- [🆕 Creating and initializing folder](#-creating-and-initializing-folder)
|
||||
- [🆕 Creating index.html](#-creating-indexhtml)
|
||||
- [🔍 Inspecting `sero.toml`](#-inspecting-serotoml)
|
||||
- [✏️ Changing `sero.toml`](#️-changing-serotoml)
|
||||
- [⬆️ Registration and uploading](#️-registration-and-uploading)
|
||||
- [Now your site is available at http://oursite.awesome.com/index.html](#now-your-site-is-available-at-httpoursiteawesomecomindexhtml)
|
||||
- [Advanced usage with `new` features](#advanced-usage-with-new-features)
|
||||
- [📍 Roadmap](#-roadmap)
|
||||
- [🧑🤝🧑 Contributing](#-contributing)
|
||||
|
||||
- [Warning!](#warning)
|
||||
- [📖 Table Of Contents](#-table-of-contents)
|
||||
- [Docs](#docs)
|
||||
- [🔧 Tools](#-tools)
|
||||
- [❓ About The Project](#-about-the-project)
|
||||
- [🚀 Features](#-features)
|
||||
- [🔌 Built With](#-built-with)
|
||||
- [📍 Roadmap](#-roadmap)
|
||||
- [🧑🤝🧑 Contributing](#-contributing)
|
||||
- [Creating A Pull Request](#creating-a-pull-request)
|
||||
- [License](#license)
|
||||
- [Authors](#authors)
|
||||
- [License](#license)
|
||||
- [Authors](#authors)
|
||||
|
||||
## Docs
|
||||
|
||||
Read [docs here]("http://sero-docs.clowzed.ru") for fast installation.
|
||||
|
||||
## 🔧 Tools
|
||||
|
||||
**Sero - [this tool is used to upload your site to sero servers](https://github.com/clowzed/seroup)**
|
||||
|
||||
|
||||
|
||||
## ❓ About The Project
|
||||
|
||||
This project is essentially a revamp of the well-known surge.sh platform. While surge.sh is a fantastic tool for publishing web applications, I noticed it lacked certain features that could significantly enhance its utility. Therefore, I decided to create my own version, incorporating those missing elements to provide a more comprehensive and seamless user experience.
|
||||
|
||||
One key feature that it is self-hosted. This gives users more flexibility and control over their projects, allowing them to truly make their work their own. My goal with this project is to create a platform that not only meets but exceeds the needs of web developers, making the process of publishing web applications as hassle-free and efficient as possible. It also has some features that are not in surge.sh
|
||||
|
||||
**So saying shortly this is a simple web server for static websites but with an ability to deploy it with custom subdomain without any configuration using cli tool. On upload it will automatically create subdomain for your site.**
|
||||
|
||||
**This is a cli tool for upload [tool](https://github.com/clowzed/seroup)**
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
- Deploy in seconds without configuration
|
||||
- Enable and disable site `new`
|
||||
- Download site `new`
|
||||
|
|
@ -74,160 +69,17 @@ One key feature that it is self-hosted. This gives users more flexibility and co
|
|||
- Custom 404.html `(on 404 status user will see your 404.html)`
|
||||
- Custom 503.html `new` `(on disabled site)`
|
||||
- Clean urls
|
||||
- CORS
|
||||
- Dynamic CORS Management
|
||||
- `[WIP]` Server events with websocket
|
||||
|
||||
## 🔌 Built With
|
||||
|
||||
- `Rust`
|
||||
- `Sea-orm` - [`sero is added to community examples`](https://github.com/SeaQL/sea-orm/blob/06c632712f3d167df0cda742dd228661b953ab7f/COMMUNITY.md?plain=1#L28)
|
||||
- `Axum` - [`sero is added to community examples`](https://github.com/tokio-rs/axum/blob/d7258bf009194cf2f242694e673759d1dbf8cfc0/ECOSYSTEM.md?plain=1#L78)
|
||||
- `Axum` - [`sero is added to community examples`](https://github.com/tokio-rs/axum/blob/d7258bf009194cf2f242694e673759d1dbf8cfc0/ECOSYSTEM.md?plain=1#L78)
|
||||
- `Postgres`
|
||||
- `Nginx`
|
||||
|
||||
## ⌨️ Getting Started
|
||||
|
||||
|
||||
### Prerequisites
|
||||
|
||||
That is what you will need to deploy this project
|
||||
|
||||
* You need to buy a domain
|
||||
* You need a server
|
||||
* You need access to DNS records
|
||||
* You need to have `docker-compose` installed
|
||||
|
||||
|
||||
### Installation
|
||||
|
||||
#### Clone this repository
|
||||
```bash
|
||||
git clone https://github.com/clowzed/sero
|
||||
```
|
||||
|
||||
#### ✏️ Configure DNS records
|
||||
|
||||
- Add wildcard subdomain to your server.
|
||||
|
||||
It is usually done by adding `TXT` DNS record with value `"*"` pointing to your server
|
||||
|
||||
#### ✏️ Configure `docker-compose.yml`
|
||||
|
||||
Simply open a [docker-compose.yml](https://github.com/clowzed/sero/blob/master/docker-compose.yml) file from this repo with any redactor you link
|
||||
|
||||
1. Configure your domain and zone (for example I bought example.com) (lines 30 and 31)
|
||||
|
||||
|
||||
| environment variable | description |
|
||||
|----------------------|-------------|
|
||||
| DOMAIN | example |
|
||||
| ZONE | com |
|
||||
|
||||
2. Configure desired limits if you want (you can skip this)
|
||||
|
||||
| environment variable | description | already set |
|
||||
|----------------------|----------------------------------------------------|-----------------|
|
||||
| MAX_USERS | Maximum amount of users to be registered | 1 |
|
||||
| MAX_SITES_PER_USER | Maximum amount of sites which each user can upload | 100 |
|
||||
| MAX_BODY_LIMIT_BYTES | Maximum body limit in bytes | 10000000 (10mb) |
|
||||
|
||||
#### 🚀 Deploy
|
||||
|
||||
```bash
|
||||
docker-compose up -d
|
||||
```
|
||||
|
||||
## ✨ Usage
|
||||
|
||||
Let see an example of uploading your site to your sero server.
|
||||
|
||||
Consider our domain is awesome.com
|
||||
|
||||
### Installing cli tool
|
||||
```bash
|
||||
npm i -g @clowzed/sero
|
||||
```
|
||||
|
||||
### 🆕 Creating and initializing folder
|
||||
```bash
|
||||
mkdir our-website
|
||||
cd our-website
|
||||
sero init # This will generate default sero.toml file
|
||||
```
|
||||
|
||||
### 🆕 Creating index.html
|
||||
```bash
|
||||
echo "Hello from our website!" > index.html
|
||||
```
|
||||
|
||||
### 🔍 Inspecting `sero.toml`
|
||||
Tha is how default sero.toml file looks like
|
||||
```toml
|
||||
[credentials]
|
||||
username = "clowzed" # Here will be your hostname
|
||||
password = ""
|
||||
|
||||
[server]
|
||||
url = "http://sero.com/"
|
||||
subdomain = "clowzed"
|
||||
|
||||
[cors]
|
||||
origins = []
|
||||
```
|
||||
|
||||
### ✏️ Changing `sero.toml`
|
||||
So now we need to change url to point to our server.
|
||||
|
||||
We also want to change subdomain for our website
|
||||
```toml
|
||||
[credentials]
|
||||
username = "clowzed"
|
||||
password = ""
|
||||
|
||||
[server]
|
||||
url = "http://awesome.com/"
|
||||
subdomain = "oursite"
|
||||
|
||||
[cors]
|
||||
origins = ["*"] # list of origins you trust
|
||||
```
|
||||
|
||||
### ⬆️ Registration and uploading
|
||||
```bash
|
||||
sero register # We need to call it this only if we've changed username
|
||||
sero upload
|
||||
```
|
||||
|
||||
#### Now your site is available at [http://oursite.awesome.com/index.html]()
|
||||
|
||||
### Advanced usage with `new` features
|
||||
|
||||
1) Disabling site `new`
|
||||
|
||||
You can disable your site using this command.
|
||||
```bash
|
||||
sero disable
|
||||
```
|
||||
This will preserve your subdomain so other users will not be able to borrow it.
|
||||
This will return `503 status code` for any request to site with your subdomain.
|
||||
You can create `503.html` file so it will be returned to user. You can do it for maintenance. The `503.html` file should be at root of your folder
|
||||
|
||||
2) You can enable site with this command. Now it will work as usual. `new`
|
||||
```bash
|
||||
sero enable
|
||||
```
|
||||
3) Download your site `new`
|
||||
|
||||
You can easily download your site as zip with this command
|
||||
```bash
|
||||
sero download
|
||||
```
|
||||
|
||||
4) Delete your site and free subdomain
|
||||
```bash
|
||||
sero teardown
|
||||
```
|
||||
|
||||
|
||||
|
||||
## 📍 Roadmap
|
||||
|
||||
See the [open issues](https://github.com/clowzed/sero/issues) for a list of proposed features (and known issues).
|
||||
|
|
@ -235,10 +87,11 @@ See the [open issues](https://github.com/clowzed/sero/issues) for a list of prop
|
|||
## 🧑🤝🧑 Contributing
|
||||
|
||||
Contributions are what make the open source community such an amazing place to be learn, inspire, and create. Any contributions you make are **greatly appreciated**.
|
||||
* If you have suggestions for adding or removing projects, feel free to [open an issue](https://github.com/clowzed/sero/issues/new) to discuss it, or directly create a pull request after you edit the *README.md* file with necessary changes.
|
||||
* Please make sure you check your spelling and grammar.
|
||||
* Create individual PR for each suggestion.
|
||||
* Please also read through the [Code Of Conduct](https://github.com/clowzed/sero/blob/main/CODE_OF_CONDUCT.md) before posting your first idea as well.
|
||||
|
||||
- If you have suggestions for adding or removing projects, feel free to [open an issue](https://github.com/clowzed/sero/issues/new) to discuss it, or directly create a pull request after you edit the _README.md_ file with necessary changes.
|
||||
- Please make sure you check your spelling and grammar.
|
||||
- Create individual PR for each suggestion.
|
||||
- Please also read through the [Code Of Conduct](https://github.com/clowzed/sero/blob/main/CODE_OF_CONDUCT.md) before posting your first idea as well.
|
||||
|
||||
### Creating A Pull Request
|
||||
|
||||
|
|
@ -246,7 +99,7 @@ Contributions are what make the open source community such an amazing place to b
|
|||
2. Create your feature Branch (`git checkout -b feature/some`)
|
||||
3. Commit your changes (`git commit -m 'implementation of some feature'`)
|
||||
4. Push to the branch (`git push origin feature/some`)
|
||||
5. Open a Pull Request
|
||||
5. Open a Pull Request
|
||||
|
||||
## License
|
||||
|
||||
|
|
@ -254,5 +107,4 @@ Contributions are what make the open source community such an amazing place to b
|
|||
|
||||
## Authors
|
||||
|
||||
* **clowzed**
|
||||
|
||||
- **clowzed**
|
||||
|
|
|
|||
2
rustfmt.toml
Normal file
2
rustfmt.toml
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
imports_granularity = "Crate"
|
||||
max_width = 120
|
||||
5
scripts/generate-entity.sh
Normal file
5
scripts/generate-entity.sh
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
sudo docker stop postgres-migrations
|
||||
sudo docker run -d --rm -P --network host -e POSTGRES_PASSWORD="1234" -e POSTGRES_USER="me" -e POSTGRES_DB="test" --name postgres-migrations postgres
|
||||
sea-orm-cli migrate up --database-url postgresql://me:1234@127.0.0.1:5432/test
|
||||
sea-orm-cli generate entity -o entity/src --database-url postgresql://me:1234@127.0.0.1:5432/test --lib --with-serde both
|
||||
sudo docker stop postgres-migrations
|
||||
33
src/api/auth/login/error.rs
Normal file
33
src/api/auth/login/error.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
use crate::{services::auth::error::ServiceError as AuthServiceError, Details};
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use validator::ValidationErrors;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum LoginError {
|
||||
#[error(transparent)]
|
||||
AuthServiceError(#[from] AuthServiceError),
|
||||
#[error("Login or password does not match validation rules!")]
|
||||
ValidationError(#[from] ValidationErrors),
|
||||
}
|
||||
|
||||
impl From<LoginError> for StatusCode {
|
||||
fn from(value: LoginError) -> Self {
|
||||
match value {
|
||||
LoginError::AuthServiceError(error) => Self::from(error),
|
||||
LoginError::ValidationError(_) => Self::BAD_REQUEST,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for LoginError {
|
||||
fn into_response(self) -> Response {
|
||||
let reason = self.to_string();
|
||||
let status_code: StatusCode = self.into();
|
||||
|
||||
(status_code, Json(Details { reason })).into_response()
|
||||
}
|
||||
}
|
||||
57
src/api/auth/login/handler.rs
Normal file
57
src/api/auth/login/handler.rs
Normal file
|
|
@ -0,0 +1,57 @@
|
|||
use super::{error::LoginError, request::LoginRequest, response::LoginResponse};
|
||||
use crate::{
|
||||
auth::parameters::{JwtGenerationParameters, UserCredentials},
|
||||
services::auth::service::Service as AuthService,
|
||||
state::State as AppState,
|
||||
};
|
||||
use axum::{extract::State, Json};
|
||||
use std::sync::Arc;
|
||||
use validator::Validate;
|
||||
|
||||
/// Login user and receive JWT token.
|
||||
///
|
||||
/// This endpoint allows users to login to sero server. The TTL for token is set by
|
||||
/// the owner of the server by `JWT_TTL` env.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
tag = "Account management",
|
||||
operation_id = "Login",
|
||||
path = "/api/auth/login",
|
||||
request_body = LoginRequest,
|
||||
responses(
|
||||
(status = 200, description = "User was successfully authenticated.", body = LoginResponse),
|
||||
(status = 400, description = "Bad request or bad credentials. See details.", body = Details),
|
||||
(status = 404, description = "Login was not found.", body = Details),
|
||||
(status = 500, description = "Some error occurred on the server.", body = Details),
|
||||
),
|
||||
)]
|
||||
#[tracing::instrument(skip(state))]
|
||||
pub async fn implementation(
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(credentials): Json<LoginRequest>,
|
||||
) -> Result<Json<LoginResponse>, LoginError> {
|
||||
//? I am validating here for
|
||||
//? custom error response
|
||||
//? Safe because of custom Debug impl
|
||||
tracing::trace!(?credentials, "Validating provided credentials...");
|
||||
credentials.validate()?;
|
||||
tracing::trace!(?credentials, "Credentials are valid!");
|
||||
|
||||
let credentials = UserCredentials {
|
||||
login: credentials.login,
|
||||
password: credentials.password,
|
||||
};
|
||||
|
||||
let user = AuthService::login(credentials, state.connection()).await?;
|
||||
tracing::trace!(?user, "User was successfully found!");
|
||||
|
||||
let parameters = JwtGenerationParameters {
|
||||
secret: state.configuration().jwt_secret(),
|
||||
ttl: state.configuration().jwt_ttl_seconds(),
|
||||
};
|
||||
|
||||
let token = AuthService::generate_jwt(user.id, parameters)?;
|
||||
tracing::trace!("Token was successfully generated!");
|
||||
|
||||
Ok(Json(LoginResponse { token }))
|
||||
}
|
||||
6
src/api/auth/login/mod.rs
Normal file
6
src/api/auth/login/mod.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
pub mod error;
|
||||
pub mod handler;
|
||||
pub mod request;
|
||||
pub mod response;
|
||||
#[cfg(test)]
|
||||
pub mod tests;
|
||||
84
src/api/auth/login/request.rs
Normal file
84
src/api/auth/login/request.rs
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
use std::fmt::{self, Debug};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::{schema, ToSchema};
|
||||
use validator::{Validate, ValidationError};
|
||||
|
||||
#[derive(Deserialize, Serialize, Validate, ToSchema)]
|
||||
pub struct LoginRequest {
|
||||
/// The username used for authentication.
|
||||
/// It must adhere to the following criteria:
|
||||
/// - It can contain letters (a-z), numbers (0-9), and periods (.).
|
||||
/// - It cannot contain any of the following characters: & = ' - + , < >
|
||||
/// - It cannot have multiple periods (.) consecutively.
|
||||
/// - Minimum length of 5 characters.
|
||||
/// - Maximum length of 40 characters.
|
||||
#[validate(
|
||||
length(min = 5, max = 40),
|
||||
custom(
|
||||
function = validate_login,
|
||||
message = "Login can contain letters (a-z), numbers (0-9), and periods (.),
|
||||
and cannot contain any of the following characters: & = ' + , < > or multiple periods (.)"
|
||||
))]
|
||||
#[schema(min_length = 5, max_length = 40)]
|
||||
pub login: String,
|
||||
|
||||
/// The password used for authentication.
|
||||
/// It must meet the following requirements:
|
||||
/// - Minimum length of 12 characters.
|
||||
/// - Maximum length of 40 characters.
|
||||
/// - A combination of letters, numbers, and symbols.
|
||||
#[validate(
|
||||
length(min = 12, max = 40),
|
||||
custom(function = validate_password,
|
||||
message = "Minimum length of 12 characters and maximum length of 40 characters and a combination of
|
||||
letters, numbers, and symbols.")
|
||||
)]
|
||||
#[schema(min_length = 12, max_length = 40)]
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
fn validate_login(login: &str) -> Result<(), ValidationError> {
|
||||
let invalid_chars = "&='+,<>";
|
||||
let invalid_double_period = "..";
|
||||
|
||||
if login.chars().any(|c| invalid_chars.contains(c)) || login.contains(invalid_double_period) {
|
||||
return Err(ValidationError::new(
|
||||
"Rules for login: Login can contain letters (a-z), numbers (0-9), and periods (.),
|
||||
and cannot contain any of the following characters: & = ' + , < > or multiple periods (.)",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_password(password: &str) -> Result<(), ValidationError> {
|
||||
let mut has_digit = false;
|
||||
let mut has_special_char = false;
|
||||
|
||||
for c in password.chars() {
|
||||
if !has_digit && c.is_ascii_digit() {
|
||||
has_digit = true;
|
||||
} else if c.is_ascii_punctuation() || c.is_ascii_whitespace() {
|
||||
has_special_char = true;
|
||||
}
|
||||
|
||||
if has_digit && has_special_char {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
Err(ValidationError::new(
|
||||
"Rules for password: Minimum length of 12 characters and maximum length of 40 characters and a combination of
|
||||
letters, numbers, and symbols.",
|
||||
))
|
||||
}
|
||||
|
||||
impl Debug for LoginRequest {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("LoginRequest")
|
||||
.field("login", &self.login)
|
||||
.field("password", &"***")
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
10
src/api/auth/login/response.rs
Normal file
10
src/api/auth/login/response.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
/// The JWT token generated for authentication purposes.
|
||||
#[derive(Deserialize, Serialize, ToSchema, Debug)]
|
||||
#[schema(example = json!({"token": "ferwfwerfwer.fwerfwerfwerfwer.fwerfewfr"}))]
|
||||
pub struct LoginResponse {
|
||||
/// Token in JWT format
|
||||
pub token: String,
|
||||
}
|
||||
27
src/api/auth/login/tests/bad_credentials.rs
Normal file
27
src/api/auth/login/tests/bad_credentials.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use crate::{
|
||||
api::auth::login::{request::LoginRequest, tests::call::test::login},
|
||||
app,
|
||||
};
|
||||
use axum::http::StatusCode;
|
||||
use axum_test::TestServer as TestClient;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[tokio::test]
|
||||
async fn bad_credentials() {
|
||||
dotenvy::from_filename_override(".env.tests").ok();
|
||||
|
||||
let (app, _) = app().await.expect("Failed to initialize application!");
|
||||
let client = TestClient::new(app).expect("Failed to run server for testing");
|
||||
|
||||
let invalid_credentials = LoginRequest {
|
||||
login: Uuid::new_v4().into(),
|
||||
password: "".into(),
|
||||
};
|
||||
|
||||
let invalid_user_login_response = login(&client, &invalid_credentials).await;
|
||||
|
||||
assert!(invalid_user_login_response.is_err_and(|error| error.0 == StatusCode::BAD_REQUEST));
|
||||
}
|
||||
}
|
||||
24
src/api/auth/login/tests/call.rs
Normal file
24
src/api/auth/login/tests/call.rs
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
#[cfg(test)]
|
||||
pub mod test {
|
||||
use crate::{
|
||||
api::{
|
||||
auth::login::{request::LoginRequest, response::LoginResponse},
|
||||
tests::post,
|
||||
},
|
||||
Details,
|
||||
};
|
||||
use axum::http::StatusCode;
|
||||
use axum_test::TestServer as TestClient;
|
||||
|
||||
pub async fn login(
|
||||
client: &TestClient,
|
||||
credentials: &LoginRequest,
|
||||
) -> Result<LoginResponse, (StatusCode, Details)> {
|
||||
let response = post(client, "/api/auth/login", Some(credentials)).await;
|
||||
|
||||
match response.status_code().is_success() {
|
||||
true => Ok(response.json::<LoginResponse>()),
|
||||
false => Err((response.status_code(), response.json())),
|
||||
}
|
||||
}
|
||||
}
|
||||
42
src/api/auth/login/tests/correct.rs
Normal file
42
src/api/auth/login/tests/correct.rs
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use crate::{
|
||||
api::auth::{
|
||||
login::{request::LoginRequest, tests::call::test::login},
|
||||
registration::{request::RegistrationRequest, tests::call::tests::registration},
|
||||
},
|
||||
app,
|
||||
};
|
||||
use axum_test::TestServer as TestClient;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[tokio::test]
|
||||
async fn correct() {
|
||||
dotenvy::from_filename_override(".env.tests").ok();
|
||||
|
||||
let (app, _) = app().await.expect("Failed to initialize application!");
|
||||
let client = TestClient::new(app).expect("Failed to run server for testing");
|
||||
|
||||
let first_user_login = Uuid::new_v4();
|
||||
let first_user_password = Uuid::new_v4();
|
||||
|
||||
//* Registration
|
||||
let first_user_registration_request = RegistrationRequest {
|
||||
login: first_user_login.into(),
|
||||
password: first_user_password.into(),
|
||||
};
|
||||
|
||||
let first_user_registration_response = registration(&client, &first_user_registration_request).await;
|
||||
assert!(first_user_registration_response.is_ok());
|
||||
|
||||
//* Login
|
||||
let first_user_login_request: LoginRequest = LoginRequest {
|
||||
login: first_user_login.into(),
|
||||
password: first_user_password.into(),
|
||||
};
|
||||
let first_user_login_response = login(&client, &first_user_login_request).await;
|
||||
assert!(first_user_login_response.is_ok());
|
||||
|
||||
assert!(!first_user_login_response.unwrap().token.is_empty());
|
||||
}
|
||||
}
|
||||
4
src/api/auth/login/tests/mod.rs
Normal file
4
src/api/auth/login/tests/mod.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
pub mod bad_credentials;
|
||||
pub mod call;
|
||||
pub mod correct;
|
||||
pub mod not_found;
|
||||
27
src/api/auth/login/tests/not_found.rs
Normal file
27
src/api/auth/login/tests/not_found.rs
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use crate::{
|
||||
api::auth::login::{request::LoginRequest, tests::call::test::login},
|
||||
app,
|
||||
};
|
||||
use axum::http::StatusCode;
|
||||
use axum_test::TestServer as TestClient;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[tokio::test]
|
||||
async fn not_found() {
|
||||
dotenvy::from_filename_override(".env.tests").ok();
|
||||
|
||||
let (app, _) = app().await.expect("Failed to initialize application!");
|
||||
let client = TestClient::new(app).expect("Failed to run server for testing");
|
||||
|
||||
let first_user_login_request: LoginRequest = LoginRequest {
|
||||
login: Uuid::new_v4().into(),
|
||||
password: Uuid::new_v4().into(),
|
||||
};
|
||||
|
||||
let first_user_login_response = login(&client, &first_user_login_request).await;
|
||||
|
||||
assert!(first_user_login_response.is_err_and(|error| error.0 == StatusCode::NOT_FOUND));
|
||||
}
|
||||
}
|
||||
12
src/api/auth/mod.rs
Normal file
12
src/api/auth/mod.rs
Normal file
|
|
@ -0,0 +1,12 @@
|
|||
use crate::state::State as AppState;
|
||||
use axum::{routing::post, Router};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub mod login;
|
||||
pub mod registration;
|
||||
|
||||
pub fn router() -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
.route("/login", post(login::handler::implementation))
|
||||
.route("/registration", post(registration::handler::implementation))
|
||||
}
|
||||
37
src/api/auth/registration/error.rs
Normal file
37
src/api/auth/registration/error.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
use crate::{services::auth::error::ServiceError as AuthServiceError, Details};
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use sea_orm::DbErr;
|
||||
use validator::ValidationErrors;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum RegistrationError {
|
||||
#[error(transparent)]
|
||||
AuthServiceError(#[from] AuthServiceError),
|
||||
#[error(transparent)]
|
||||
DatabaseError(#[from] DbErr),
|
||||
#[error("Login or password does not match validation rules!")]
|
||||
ValidationError(#[from] ValidationErrors),
|
||||
}
|
||||
|
||||
impl From<RegistrationError> for StatusCode {
|
||||
fn from(value: RegistrationError) -> Self {
|
||||
match value {
|
||||
RegistrationError::AuthServiceError(error) => Self::from(error),
|
||||
RegistrationError::DatabaseError(_error) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
RegistrationError::ValidationError(_) => Self::BAD_REQUEST,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for RegistrationError {
|
||||
fn into_response(self) -> Response {
|
||||
let reason = self.to_string();
|
||||
let status_code: StatusCode = self.into();
|
||||
|
||||
(status_code, Json(Details { reason })).into_response()
|
||||
}
|
||||
}
|
||||
60
src/api/auth/registration/handler.rs
Normal file
60
src/api/auth/registration/handler.rs
Normal file
|
|
@ -0,0 +1,60 @@
|
|||
use super::{error::RegistrationError, request::RegistrationRequest, response::RegistrationResponse};
|
||||
use crate::{
|
||||
auth::parameters::UserCredentials, extractors::*, services::auth::service::Service as AuthService,
|
||||
state::State as AppState,
|
||||
};
|
||||
use axum::{extract::State, http::StatusCode, response::IntoResponse, Json};
|
||||
use sea_orm::TransactionTrait;
|
||||
use std::sync::Arc;
|
||||
use validator::Validate;
|
||||
|
||||
/// Register new user for sero server.
|
||||
///
|
||||
/// This endpoint creates new user for sero server. The amount of users is checked
|
||||
/// by [RegistrationGuard]. The amount of allowed users is determined by `MAX_USERS` env.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
tag = "Account management",
|
||||
operation_id = "Registration",
|
||||
path = "/api/auth/registration",
|
||||
request_body = RegistrationRequest,
|
||||
responses(
|
||||
(status = 200, description = "User was successfully registered.", body = RegistrationResponse),
|
||||
(status = 400, description = "Bad request or bad credentials. See details.", body = Details),
|
||||
(status = 409, description = "Login has already been registered.", body = Details),
|
||||
(status = 500, description = "Some error occurred on the server.", body = Details),
|
||||
),
|
||||
)]
|
||||
#[tracing::instrument(skip(state))]
|
||||
pub async fn implementation(
|
||||
_: RegistrationGuard,
|
||||
State(state): State<Arc<AppState>>,
|
||||
Json(credentials): Json<RegistrationRequest>,
|
||||
) -> Result<impl IntoResponse, RegistrationError> {
|
||||
//? I am validating here for
|
||||
//? custom error response
|
||||
//? Safe because of custom Debug impl
|
||||
tracing::trace!(?credentials, "Validating provided credentials...");
|
||||
credentials.validate()?;
|
||||
tracing::trace!(?credentials, "Credentials are valid!");
|
||||
let transaction = state.connection().begin().await?;
|
||||
|
||||
let credentials = UserCredentials {
|
||||
login: credentials.login,
|
||||
password: credentials.password,
|
||||
};
|
||||
|
||||
let user_id = AuthService::registration(credentials, &transaction).await?.id;
|
||||
tracing::trace!(%user_id, "User was created! Committing changes...");
|
||||
|
||||
transaction.commit().await?;
|
||||
tracing::trace!("Successfully committed!");
|
||||
|
||||
Ok((
|
||||
StatusCode::CREATED,
|
||||
// Leave this one in case of api additions
|
||||
// to match REST
|
||||
//[(header::LOCATION, format!("/api/user/{user_id}"))],
|
||||
Json(RegistrationResponse { id: user_id }),
|
||||
))
|
||||
}
|
||||
6
src/api/auth/registration/mod.rs
Normal file
6
src/api/auth/registration/mod.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
pub mod error;
|
||||
pub mod handler;
|
||||
pub mod request;
|
||||
pub mod response;
|
||||
#[cfg(test)]
|
||||
pub mod tests;
|
||||
84
src/api/auth/registration/request.rs
Normal file
84
src/api/auth/registration/request.rs
Normal file
|
|
@ -0,0 +1,84 @@
|
|||
use std::fmt::{self, Debug};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::{schema, ToSchema};
|
||||
use validator::{Validate, ValidationError};
|
||||
|
||||
#[derive(Deserialize, Serialize, Validate, ToSchema)]
|
||||
pub struct RegistrationRequest {
|
||||
/// The username used for authentication.
|
||||
/// It must adhere to the following criteria:
|
||||
/// - It can contain letters (a-z), numbers (0-9), and periods (.).
|
||||
/// - It cannot contain any of the following characters: & = ' - + , < >
|
||||
/// - It cannot have multiple periods (.) consecutively.
|
||||
/// - Minimum length of 5 characters.
|
||||
/// - Maximum length of 40 characters.
|
||||
#[validate(
|
||||
length(min = 5, max = 40),
|
||||
custom(
|
||||
function = validate_login,
|
||||
message = "Login can contain letters (a-z), numbers (0-9), and periods (.),
|
||||
and cannot contain any of the following characters: & = ' + , < > or multiple periods (.)"
|
||||
))]
|
||||
#[schema(min_length = 5, max_length = 40)]
|
||||
pub login: String,
|
||||
|
||||
/// The password used for authentication.
|
||||
/// It must meet the following requirements:
|
||||
/// - Minimum length of 12 characters.
|
||||
/// - Maximum length of 40 characters.
|
||||
/// - A combination of letters, numbers, and symbols.
|
||||
#[validate(
|
||||
length(min = 12, max = 40),
|
||||
custom(function = validate_password,
|
||||
message = "Minimum length of 12 characters and maximum length of 40 characters and a combination of
|
||||
letters, numbers, and symbols.")
|
||||
)]
|
||||
#[schema(min_length = 12, max_length = 40)]
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
fn validate_login(login: &str) -> Result<(), ValidationError> {
|
||||
let invalid_chars = "&='+,<>";
|
||||
let invalid_double_period = "..";
|
||||
|
||||
if login.chars().any(|c| invalid_chars.contains(c)) || login.contains(invalid_double_period) {
|
||||
return Err(ValidationError::new(
|
||||
"Rules for login: Login can contain letters (a-z), numbers (0-9), and periods (.),
|
||||
and cannot contain any of the following characters: & = ' + , < > or multiple periods (.)",
|
||||
));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn validate_password(password: &str) -> Result<(), ValidationError> {
|
||||
let mut has_digit = false;
|
||||
let mut has_special_char = false;
|
||||
|
||||
for c in password.chars() {
|
||||
if !has_digit && c.is_ascii_digit() {
|
||||
has_digit = true;
|
||||
} else if c.is_ascii_punctuation() || c.is_ascii_whitespace() {
|
||||
has_special_char = true;
|
||||
}
|
||||
|
||||
if has_digit && has_special_char {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
Err(ValidationError::new(
|
||||
"Rules for password: Minimum length of 12 characters and maximum length of 40 characters and a combination of
|
||||
letters, numbers, and symbols.",
|
||||
))
|
||||
}
|
||||
|
||||
impl Debug for RegistrationRequest {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.debug_struct("RegistrationRequest")
|
||||
.field("login", &self.login)
|
||||
.field("password", &"***")
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
9
src/api/auth/registration/response.rs
Normal file
9
src/api/auth/registration/response.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Serialize, Deserialize, ToSchema, Debug)]
|
||||
#[schema(example = json!({"id" : 1293983717}))]
|
||||
pub struct RegistrationResponse {
|
||||
/// Auto generated id of a registered user
|
||||
pub id: i64,
|
||||
}
|
||||
39
src/api/auth/registration/tests/bad_credentials.rs
Normal file
39
src/api/auth/registration/tests/bad_credentials.rs
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use crate::{
|
||||
api::auth::registration::{request::RegistrationRequest, tests::call::tests::registration},
|
||||
app,
|
||||
};
|
||||
use axum::http::StatusCode;
|
||||
use axum_test::TestServer as TestClient;
|
||||
use entity::prelude::*;
|
||||
use sea_orm::EntityTrait;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[tokio::test]
|
||||
async fn check_invalid_credentials_registration() {
|
||||
dotenvy::from_filename_override(".env.tests").ok();
|
||||
|
||||
//? Set maximum users allowed for registration
|
||||
std::env::set_var("MAX_USERS", "1");
|
||||
|
||||
let (app, state) = app().await.expect("Failed to initialize application!");
|
||||
//? Perform drop for users
|
||||
UserEntity::delete_many()
|
||||
.exec(state.connection())
|
||||
.await
|
||||
.expect("Failed to delete all users");
|
||||
|
||||
let client = TestClient::new(app).expect("Failed to run server for testing");
|
||||
|
||||
let invalid_credentials_registration_request = RegistrationRequest {
|
||||
login: Uuid::new_v4().into(),
|
||||
password: "".into(),
|
||||
};
|
||||
|
||||
let invalid_credentials_registration_response =
|
||||
registration(&client, &invalid_credentials_registration_request).await;
|
||||
|
||||
assert!(invalid_credentials_registration_response.is_err_and(|error| error.0 == StatusCode::BAD_REQUEST));
|
||||
}
|
||||
}
|
||||
23
src/api/auth/registration/tests/call.rs
Normal file
23
src/api/auth/registration/tests/call.rs
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use crate::{
|
||||
api::{
|
||||
auth::registration::{request::RegistrationRequest, response::RegistrationResponse},
|
||||
tests::post,
|
||||
},
|
||||
Details,
|
||||
};
|
||||
use axum::http::StatusCode;
|
||||
use axum_test::TestServer as TestClient;
|
||||
|
||||
pub async fn registration(
|
||||
client: &TestClient,
|
||||
credentials: &RegistrationRequest,
|
||||
) -> Result<RegistrationResponse, (StatusCode, Details)> {
|
||||
let response = post(client, "/api/auth/registration", Some(credentials)).await;
|
||||
match response.status_code().is_success() {
|
||||
true => Ok(response.json::<RegistrationResponse>()),
|
||||
false => Err((response.status_code(), response.json())),
|
||||
}
|
||||
}
|
||||
}
|
||||
37
src/api/auth/registration/tests/correct.rs
Normal file
37
src/api/auth/registration/tests/correct.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use crate::{
|
||||
api::auth::registration::{request::RegistrationRequest, tests::call::tests::registration},
|
||||
app,
|
||||
};
|
||||
use axum_test::TestServer as TestClient;
|
||||
use entity::prelude::*;
|
||||
use sea_orm::EntityTrait;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[tokio::test]
|
||||
async fn correct_registration() {
|
||||
dotenvy::from_filename_override(".env.tests").ok();
|
||||
|
||||
//? Set maximum users allowed for registration
|
||||
std::env::set_var("MAX_USERS", "1");
|
||||
|
||||
let (app, state) = app().await.expect("Failed to initialize application!");
|
||||
//? Perform drop for users
|
||||
UserEntity::delete_many()
|
||||
.exec(state.connection())
|
||||
.await
|
||||
.expect("Failed to delete all users");
|
||||
|
||||
let client = TestClient::new(app).expect("Failed to run server for testing");
|
||||
|
||||
let first_user_registration_request = RegistrationRequest {
|
||||
login: Uuid::new_v4().into(),
|
||||
password: Uuid::new_v4().into(),
|
||||
};
|
||||
|
||||
let first_user_registration_response = registration(&client, &first_user_registration_request).await;
|
||||
|
||||
assert!(first_user_registration_response.is_ok());
|
||||
}
|
||||
}
|
||||
37
src/api/auth/registration/tests/guard.rs
Normal file
37
src/api/auth/registration/tests/guard.rs
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use crate::{
|
||||
api::auth::registration::{request::RegistrationRequest, tests::call::tests::registration},
|
||||
app,
|
||||
};
|
||||
use axum::http::StatusCode;
|
||||
use axum_test::TestServer as TestClient;
|
||||
use entity::prelude::*;
|
||||
use sea_orm::EntityTrait;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[tokio::test]
|
||||
async fn guard() {
|
||||
dotenvy::from_filename_override(".env.tests").ok();
|
||||
|
||||
std::env::set_var("MAX_USERS", "0");
|
||||
|
||||
let (app, state) = app().await.expect("Failed to initialize application!");
|
||||
//? Perform drop for users
|
||||
UserEntity::delete_many()
|
||||
.exec(state.connection())
|
||||
.await
|
||||
.expect("Failed to delete all users");
|
||||
|
||||
let client = TestClient::new(app).expect("Failed to run server for testing");
|
||||
|
||||
let first_user_registration_request = RegistrationRequest {
|
||||
login: Uuid::new_v4().into(),
|
||||
password: Uuid::new_v4().into(),
|
||||
};
|
||||
|
||||
let first_user_registration_response = registration(&client, &first_user_registration_request).await;
|
||||
|
||||
assert!(first_user_registration_response.is_err_and(|error| error.0 == StatusCode::FORBIDDEN));
|
||||
}
|
||||
}
|
||||
5
src/api/auth/registration/tests/mod.rs
Normal file
5
src/api/auth/registration/tests/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
pub mod bad_credentials;
|
||||
pub mod call;
|
||||
pub mod correct;
|
||||
pub mod guard;
|
||||
pub mod occupied;
|
||||
41
src/api/auth/registration/tests/occupied.rs
Normal file
41
src/api/auth/registration/tests/occupied.rs
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use crate::{
|
||||
api::auth::registration::{request::RegistrationRequest, tests::call::tests::registration},
|
||||
app,
|
||||
};
|
||||
use axum::http::StatusCode;
|
||||
use axum_test::TestServer as TestClient;
|
||||
use entity::prelude::*;
|
||||
use sea_orm::EntityTrait;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[tokio::test]
|
||||
async fn occupied() {
|
||||
dotenvy::from_filename_override(".env.tests").ok();
|
||||
|
||||
//? Set maximum users allowed for registration
|
||||
std::env::set_var("MAX_USERS", "2");
|
||||
|
||||
let (app, state) = app().await.expect("Failed to initialize application!");
|
||||
//? Perform drop for users
|
||||
UserEntity::delete_many()
|
||||
.exec(state.connection())
|
||||
.await
|
||||
.expect("Failed to delete all users");
|
||||
|
||||
let client = TestClient::new(app).expect("Failed to run server for testing");
|
||||
|
||||
let first_user_registration_request = RegistrationRequest {
|
||||
login: Uuid::new_v4().into(),
|
||||
password: Uuid::new_v4().into(),
|
||||
};
|
||||
|
||||
let first_user_registration_response = registration(&client, &first_user_registration_request).await;
|
||||
assert!(first_user_registration_response.is_ok());
|
||||
|
||||
let first_user_second_attempt_registration_response =
|
||||
registration(&client, &first_user_registration_request).await;
|
||||
assert!(first_user_second_attempt_registration_response.is_err_and(|error| error.0 == StatusCode::CONFLICT));
|
||||
}
|
||||
}
|
||||
48
src/api/mod.rs
Normal file
48
src/api/mod.rs
Normal file
|
|
@ -0,0 +1,48 @@
|
|||
use crate::state::State as AppState;
|
||||
use axum::{http::StatusCode, routing::get, Router};
|
||||
use std::sync::Arc;
|
||||
|
||||
pub mod auth;
|
||||
pub mod origin;
|
||||
pub mod site;
|
||||
|
||||
pub fn router() -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
.nest("/auth", auth::router())
|
||||
.nest("/origin", origin::router())
|
||||
.nest("/site", site::router())
|
||||
.route("/health", get(|| async { StatusCode::OK }))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use axum_test::{TestRequest, TestServer as TestClient};
|
||||
use serde::Serialize;
|
||||
|
||||
macro_rules! impl_call {
|
||||
($method:ident) => {
|
||||
pub fn $method<T, U>(client: &TestClient, url: U, data: Option<T>) -> TestRequest
|
||||
where
|
||||
T: Serialize,
|
||||
U: AsRef<str>,
|
||||
{
|
||||
let mut builder = client.$method(url.as_ref());
|
||||
if let Some(data) = data {
|
||||
builder = builder.json(&data);
|
||||
}
|
||||
builder
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
impl_call!(post);
|
||||
impl_call!(patch);
|
||||
impl_call!(delete);
|
||||
|
||||
pub fn get<U>(client: &TestClient, url: U) -> TestRequest
|
||||
where
|
||||
U: AsRef<str>,
|
||||
{
|
||||
client.get(url.as_ref())
|
||||
}
|
||||
}
|
||||
29
src/api/origin/create/error.rs
Normal file
29
src/api/origin/create/error.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
use crate::{services::origin::error::ServiceError as OriginServiceError, Details};
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum AddOriginError {
|
||||
#[error(transparent)]
|
||||
OriginServiceError(#[from] OriginServiceError),
|
||||
}
|
||||
|
||||
impl From<AddOriginError> for StatusCode {
|
||||
fn from(value: AddOriginError) -> Self {
|
||||
match value {
|
||||
AddOriginError::OriginServiceError(error) => Self::from(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for AddOriginError {
|
||||
fn into_response(self) -> Response {
|
||||
let reason = self.to_string();
|
||||
let status_code: StatusCode = self.into();
|
||||
|
||||
(status_code, Json(Details { reason })).into_response()
|
||||
}
|
||||
}
|
||||
68
src/api/origin/create/handler.rs
Normal file
68
src/api/origin/create/handler.rs
Normal file
|
|
@ -0,0 +1,68 @@
|
|||
use super::{error::AddOriginError, request::AddOriginRequest, response::AddOriginResponse};
|
||||
use crate::{extractors::*, services::origin::service::Service as CorsService, state::State as AppState};
|
||||
use axum::{
|
||||
extract::State,
|
||||
http::{header, StatusCode},
|
||||
response::IntoResponse,
|
||||
Json,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Adds a new origin to a specified subdomain for dynamic CORS (Cross-Origin Resource Sharing) management.
|
||||
///
|
||||
/// This endpoint allows users to add origins that are permitted to access resources
|
||||
/// on their specified subdomains. The action is authenticated using a JWT, and the subdomain must
|
||||
/// be owned by the user making the request. This will be checked by the server.
|
||||
#[utoipa::path(
|
||||
post,
|
||||
tag = "Origins Management and Dynamic Access Control",
|
||||
operation_id = "Create origin",
|
||||
path = "/api/origin",
|
||||
request_body = AddOriginRequest,
|
||||
params(
|
||||
("x-subdomain" = String,
|
||||
Header,
|
||||
description = "'x-subdomain' header represents the name of the subdomain on which the action is to be performed."),
|
||||
),
|
||||
responses(
|
||||
(status = 201, description = "The origin was successfully added.", body = AddOriginResponse),
|
||||
(status = 400, description = "The 'x-subdomain' header is missing or contains invalid characters.", body = Details),
|
||||
(status = 401, description = "Unauthorized: The JWT in the header is invalid or expired.", body = Details),
|
||||
(status = 403, description = "Forbidden: The subdomain is owned by another user.", body = Details),
|
||||
(status = 404, description = "Not Found: The login or subdomain was not found. See details for more information.", body = Details),
|
||||
(status = 500, description = "Internal Server Error: An error occurred on the server.", body = Details),
|
||||
),
|
||||
security(("Bearer-JWT" = []))
|
||||
)]
|
||||
#[tracing::instrument(skip(state))]
|
||||
pub async fn implementation(
|
||||
State(state): State<Arc<AppState>>,
|
||||
SubdomainOwned { user, subdomain }: SubdomainOwned,
|
||||
Json(payload): Json<AddOriginRequest>,
|
||||
) -> Result<impl IntoResponse, AddOriginError> {
|
||||
tracing::trace!(
|
||||
%payload.origin,
|
||||
%subdomain.name,
|
||||
%subdomain.id,
|
||||
%user.id,
|
||||
"Adding origin for subdomain...",
|
||||
);
|
||||
|
||||
let added_origin = CorsService::add_origin_for(subdomain.id, payload.origin, state.connection()).await?;
|
||||
tracing::trace!(
|
||||
?added_origin,
|
||||
%subdomain.name,
|
||||
%subdomain.id,
|
||||
%user.id,
|
||||
"Origin was successfully added to subdomain!",
|
||||
);
|
||||
|
||||
let id = added_origin.id;
|
||||
let origin = added_origin.value;
|
||||
|
||||
Ok((
|
||||
StatusCode::CREATED,
|
||||
[(header::LOCATION, format!("/api/origin/{id}"))],
|
||||
Json(AddOriginResponse { id, origin }),
|
||||
))
|
||||
}
|
||||
6
src/api/origin/create/mod.rs
Normal file
6
src/api/origin/create/mod.rs
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
pub mod error;
|
||||
pub mod handler;
|
||||
pub mod request;
|
||||
pub mod response;
|
||||
#[cfg(test)]
|
||||
pub mod tests;
|
||||
9
src/api/origin/create/request.rs
Normal file
9
src/api/origin/create/request.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, ToSchema)]
|
||||
#[schema(example = json!({"origin": "https://example.com/"}))]
|
||||
pub struct AddOriginRequest {
|
||||
/// Origin to be added
|
||||
pub origin: String,
|
||||
}
|
||||
13
src/api/origin/create/response.rs
Normal file
13
src/api/origin/create/response.rs
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, ToSchema, PartialEq)]
|
||||
#[schema(example = json!({"id" : "42", "origin": "https://example.com/"}))]
|
||||
pub struct AddOriginResponse {
|
||||
/// Automatically generated id for new origin
|
||||
/// This can be used for further management
|
||||
pub id: i64,
|
||||
/// This duplicates origin from response payload
|
||||
/// to match REST specification
|
||||
pub origin: String,
|
||||
}
|
||||
40
src/api/origin/create/tests/call.rs
Normal file
40
src/api/origin/create/tests/call.rs
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use crate::{
|
||||
api::{
|
||||
origin::create::{request::AddOriginRequest, response::AddOriginResponse},
|
||||
tests::post,
|
||||
},
|
||||
Details,
|
||||
};
|
||||
use axum::http::{HeaderName, HeaderValue, StatusCode};
|
||||
use axum_test::TestServer as TestClient;
|
||||
use std::fmt::Display;
|
||||
|
||||
pub async fn create<T, S, O>(
|
||||
client: &TestClient,
|
||||
token: T,
|
||||
subdomain: S,
|
||||
origin: O,
|
||||
) -> Result<AddOriginResponse, (StatusCode, Details)>
|
||||
where
|
||||
T: AsRef<str> + Display,
|
||||
S: AsRef<str> + Display,
|
||||
O: Into<String>,
|
||||
{
|
||||
let origin = AddOriginRequest { origin: origin.into() };
|
||||
|
||||
let response = post(client, "/api/origin", Some(origin))
|
||||
.add_header(
|
||||
HeaderName::from_static("x-subdomain"),
|
||||
HeaderValue::from_str(subdomain.as_ref()).expect("Failed to convert subdomain name to header value!"),
|
||||
)
|
||||
.authorization_bearer(token)
|
||||
.await;
|
||||
|
||||
match response.status_code().is_success() {
|
||||
true => Ok(response.json()),
|
||||
false => Err((response.status_code(), response.json())),
|
||||
}
|
||||
}
|
||||
}
|
||||
85
src/api/origin/create/tests/correct.rs
Normal file
85
src/api/origin/create/tests/correct.rs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use crate::{
|
||||
api::{
|
||||
auth::{
|
||||
login::{request::LoginRequest, tests::call::test::login},
|
||||
registration::{request::RegistrationRequest, tests::call::tests::registration},
|
||||
},
|
||||
origin::{create::tests::call::tests::create, tests::preflight},
|
||||
site::{page::tests::call::tests::page, upload::tests::call::tests::upload},
|
||||
},
|
||||
app,
|
||||
};
|
||||
use axum_test::TestServer as TestClient;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[tokio::test]
|
||||
async fn correct() {
|
||||
dotenvy::from_filename_override(".env.tests").ok();
|
||||
|
||||
let (app, _state) = app().await.expect("Failed to initialize application!");
|
||||
let app_cloned = app.clone();
|
||||
|
||||
let client = TestClient::new(app_cloned).expect("Failed to run server for testing");
|
||||
|
||||
let first_user_login = Uuid::new_v4();
|
||||
let first_user_password = Uuid::new_v4();
|
||||
|
||||
//* Registration
|
||||
let first_user_registration_request = RegistrationRequest {
|
||||
login: first_user_login.into(),
|
||||
password: first_user_password.into(),
|
||||
};
|
||||
let first_user_registration_response = registration(&client, &first_user_registration_request).await;
|
||||
assert!(first_user_registration_response.is_ok());
|
||||
|
||||
//* Login
|
||||
let first_user_login_request = LoginRequest {
|
||||
login: first_user_login.into(),
|
||||
password: first_user_password.into(),
|
||||
};
|
||||
let first_user_login_response = login(&client, &first_user_login_request).await;
|
||||
assert!(first_user_login_response.is_ok());
|
||||
let first_user_token = first_user_login_response.expect("to never fail").token;
|
||||
|
||||
let first_random_subdomain = Uuid::new_v4().to_string();
|
||||
|
||||
let first_correct_zip_path = "./assets/zips/correct-1.zip";
|
||||
|
||||
//* Correct upload
|
||||
let first_correct_upload_response = upload(
|
||||
&client,
|
||||
&first_user_token,
|
||||
&first_random_subdomain,
|
||||
first_correct_zip_path,
|
||||
)
|
||||
.await;
|
||||
assert_eq!(first_correct_upload_response, Ok(()));
|
||||
|
||||
//* Test that page is correct
|
||||
let first_user_correct_zip_index_response = page(&client, "/some/index.html", &first_random_subdomain).await;
|
||||
assert!(first_user_correct_zip_index_response.status_code().is_success());
|
||||
|
||||
//* Add some origin
|
||||
let first_subdomain_add_origin_response =
|
||||
create(&client, &first_user_token, &first_random_subdomain, "some").await;
|
||||
assert!(first_subdomain_add_origin_response.is_ok());
|
||||
|
||||
//* Check origin was added
|
||||
let preflight_response = preflight(&client, &first_random_subdomain, "some").await;
|
||||
let preflight_response_allowed_origin = preflight_response
|
||||
.headers()
|
||||
.get(axum::http::header::ACCESS_CONTROL_ALLOW_ORIGIN);
|
||||
|
||||
assert!(preflight_response_allowed_origin.is_some());
|
||||
|
||||
//* Check origin which was not allowed
|
||||
let preflight_response = preflight(&client, &first_random_subdomain, "another").await;
|
||||
let preflight_response_allowed_origin = preflight_response
|
||||
.headers()
|
||||
.get(axum::http::header::ACCESS_CONTROL_ALLOW_ORIGIN);
|
||||
|
||||
assert!(preflight_response_allowed_origin.is_none());
|
||||
}
|
||||
}
|
||||
2
src/api/origin/create/tests/mod.rs
Normal file
2
src/api/origin/create/tests/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub mod call;
|
||||
pub mod correct;
|
||||
33
src/api/origin/delete/error.rs
Normal file
33
src/api/origin/delete/error.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
use crate::{services::origin::error::ServiceError as OriginServiceError, Details};
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use sea_orm::DbErr;
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum DeleteOriginError {
|
||||
#[error(transparent)]
|
||||
OriginServiceError(#[from] OriginServiceError),
|
||||
#[error(transparent)]
|
||||
DatabaseError(#[from] DbErr),
|
||||
}
|
||||
|
||||
impl From<DeleteOriginError> for StatusCode {
|
||||
fn from(value: DeleteOriginError) -> Self {
|
||||
match value {
|
||||
DeleteOriginError::OriginServiceError(error) => Self::from(error),
|
||||
DeleteOriginError::DatabaseError(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for DeleteOriginError {
|
||||
fn into_response(self) -> Response {
|
||||
let reason = self.to_string();
|
||||
let status_code: StatusCode = self.into();
|
||||
|
||||
(status_code, Json(Details { reason })).into_response()
|
||||
}
|
||||
}
|
||||
72
src/api/origin/delete/handler.rs
Normal file
72
src/api/origin/delete/handler.rs
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
use super::error::DeleteOriginError;
|
||||
use crate::{extractors::*, services::origin::service::Service as CorsService, state::State as AppState};
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
http::StatusCode,
|
||||
response::IntoResponse,
|
||||
};
|
||||
use sea_orm::TransactionTrait;
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Delete origin by id for specified subdomain for dynamic CORS (Cross-Origin Resource Sharing) management.
|
||||
///
|
||||
/// This endpoint allows users to delete origin by id that is permitted to access resources
|
||||
/// on their specified subdomains. The action is authenticated using a JWT, and the subdomain must
|
||||
/// be owned by the user making the request. This will be checked by the server.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
tag = "Origins Management and Dynamic Access Control",
|
||||
operation_id = "Delete origin by id",
|
||||
path = "/api/origin/{id}",
|
||||
params(
|
||||
("x-subdomain" = String,
|
||||
Header,
|
||||
description = "'x-subdomain' header represents the name of the subdomain on which the action is to be performed."),
|
||||
("id" = i64, Path, description = "Id of the origin to delete"),
|
||||
),
|
||||
responses(
|
||||
(status = 204, description = "Origin was successfully deleted for subdomain."),
|
||||
(status = 400, description = "The 'x-subdomain' header is missing or contains invalid characters.", body = Details),
|
||||
(status = 401, description = "Unauthorized: The JWT in the header is invalid or expired.", body = Details),
|
||||
(status = 403, description = "Forbidden: The origin is owned by another user.", body = Details),
|
||||
(status = 404, description = "Not Found: The login or subdomain or origin was not found. See details for more information.", body = Details),
|
||||
(status = 500, description = "Internal Server Error: An error occurred on the server.", body = Details),
|
||||
),
|
||||
security(("Bearer-JWT" = []))
|
||||
)]
|
||||
#[tracing::instrument(skip(state))]
|
||||
pub async fn implementation(
|
||||
State(state): State<Arc<AppState>>,
|
||||
SubdomainOwned { user, subdomain }: SubdomainOwned,
|
||||
Path(origin_id): Path<i64>,
|
||||
) -> Result<impl IntoResponse, DeleteOriginError> {
|
||||
let transaction = state.connection().begin().await?;
|
||||
tracing::trace!(
|
||||
%origin_id,
|
||||
%subdomain.name,
|
||||
%subdomain.id,
|
||||
%user.id,
|
||||
"Deleting origin for subdomain...",
|
||||
);
|
||||
|
||||
let rows_affected = CorsService::delete_origin_of(subdomain.id, origin_id, &transaction).await?;
|
||||
|
||||
tracing::trace!(
|
||||
%origin_id,
|
||||
%subdomain.name,
|
||||
%subdomain.id,
|
||||
%rows_affected,
|
||||
%user.id,
|
||||
"Origin was successfully deleted for subdomain. Committing changes...",
|
||||
);
|
||||
|
||||
transaction.commit().await?;
|
||||
tracing::trace!( %origin_id,
|
||||
%subdomain.name,
|
||||
%subdomain.id,
|
||||
%user.id,
|
||||
%rows_affected,
|
||||
"Origin was successfully deleted for subdomain. Changes were successfully committed!");
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
4
src/api/origin/delete/mod.rs
Normal file
4
src/api/origin/delete/mod.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
pub mod error;
|
||||
pub mod handler;
|
||||
#[cfg(test)]
|
||||
pub mod tests;
|
||||
26
src/api/origin/delete/tests/call.rs
Normal file
26
src/api/origin/delete/tests/call.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use crate::{api::tests::delete as delete_helper, Details};
|
||||
use axum::http::{HeaderName, HeaderValue, StatusCode};
|
||||
use axum_test::TestServer as TestClient;
|
||||
use std::fmt::Display;
|
||||
|
||||
pub async fn delete<T, S>(client: &TestClient, token: T, subdomain: S, id: i64) -> Result<(), (StatusCode, Details)>
|
||||
where
|
||||
T: Display,
|
||||
S: AsRef<str> + Display,
|
||||
{
|
||||
let response = delete_helper(client, &format!("/api/origin/{id}"), Option::<()>::None)
|
||||
.add_header(
|
||||
HeaderName::from_static("x-subdomain"),
|
||||
HeaderValue::from_str(subdomain.as_ref()).expect("Failed to convert subdomain name to header value!"),
|
||||
)
|
||||
.authorization_bearer(token)
|
||||
.await;
|
||||
|
||||
match response.status_code().is_success() {
|
||||
true => Ok(()),
|
||||
false => Err((response.status_code(), response.json())),
|
||||
}
|
||||
}
|
||||
}
|
||||
97
src/api/origin/delete/tests/correct.rs
Normal file
97
src/api/origin/delete/tests/correct.rs
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use crate::{
|
||||
api::{
|
||||
auth::{
|
||||
login::{request::LoginRequest, tests::call::test::login},
|
||||
registration::{request::RegistrationRequest, tests::call::tests::registration},
|
||||
},
|
||||
origin::{create::tests::call::tests::create, delete::tests::call::tests::delete, tests::preflight},
|
||||
site::{page::tests::call::tests::page, upload::tests::call::tests::upload},
|
||||
},
|
||||
app,
|
||||
};
|
||||
use axum_test::TestServer as TestClient;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[tokio::test]
|
||||
async fn correct() {
|
||||
dotenvy::from_filename_override(".env.tests").ok();
|
||||
|
||||
let (app, _state) = app().await.expect("Failed to initialize application!");
|
||||
let app_cloned = app.clone();
|
||||
|
||||
let client = TestClient::new(app_cloned).expect("Failed to run server for testing");
|
||||
|
||||
let first_user_login = Uuid::new_v4();
|
||||
let first_user_password = Uuid::new_v4();
|
||||
|
||||
//* Registration
|
||||
let first_user_registration_request = RegistrationRequest {
|
||||
login: first_user_login.into(),
|
||||
password: first_user_password.into(),
|
||||
};
|
||||
let first_user_registration_response = registration(&client, &first_user_registration_request).await;
|
||||
assert!(first_user_registration_response.is_ok());
|
||||
|
||||
//* Login
|
||||
let first_user_login_request = LoginRequest {
|
||||
login: first_user_login.into(),
|
||||
password: first_user_password.into(),
|
||||
};
|
||||
let first_user_login_response = login(&client, &first_user_login_request).await;
|
||||
assert!(first_user_login_response.is_ok());
|
||||
let first_user_token = first_user_login_response.expect("to never fail").token;
|
||||
|
||||
let first_random_subdomain = Uuid::new_v4().to_string();
|
||||
|
||||
let first_correct_zip_path = "./assets/zips/correct-1.zip";
|
||||
|
||||
//* Correct upload
|
||||
let first_correct_upload_response = upload(
|
||||
&client,
|
||||
&first_user_token,
|
||||
&first_random_subdomain,
|
||||
first_correct_zip_path,
|
||||
)
|
||||
.await;
|
||||
assert!(first_correct_upload_response.is_ok());
|
||||
|
||||
//* Test that page is correct
|
||||
let first_user_correct_zip_index_response = page(&client, "/some/index.html", &first_random_subdomain).await;
|
||||
assert!(first_user_correct_zip_index_response.status_code().is_success());
|
||||
|
||||
//* Add some origin
|
||||
let first_subdomain_add_origin_response =
|
||||
create(&client, &first_user_token, &first_random_subdomain, "some").await;
|
||||
assert!(first_subdomain_add_origin_response.is_ok());
|
||||
|
||||
let first_subdomain_added_origin = first_subdomain_add_origin_response.expect("never fail");
|
||||
|
||||
//* Check origin was added
|
||||
let preflight_response = preflight(&client, &first_random_subdomain, "some").await;
|
||||
let preflight_response_allowed_origin = preflight_response
|
||||
.headers()
|
||||
.get(axum::http::header::ACCESS_CONTROL_ALLOW_ORIGIN);
|
||||
|
||||
assert!(preflight_response_allowed_origin.is_some());
|
||||
|
||||
//* Delete
|
||||
let first_subdomain_delete_first_origin_response = delete(
|
||||
&client,
|
||||
&first_user_token,
|
||||
&first_random_subdomain,
|
||||
first_subdomain_added_origin.id,
|
||||
)
|
||||
.await;
|
||||
assert!(first_subdomain_delete_first_origin_response.is_ok());
|
||||
|
||||
//* Check origin again
|
||||
let preflight_response = preflight(&client, &first_random_subdomain, "some").await;
|
||||
let preflight_response_allowed_origin = preflight_response
|
||||
.headers()
|
||||
.get(axum::http::header::ACCESS_CONTROL_ALLOW_ORIGIN);
|
||||
|
||||
assert!(preflight_response_allowed_origin.is_none());
|
||||
}
|
||||
}
|
||||
3
src/api/origin/delete/tests/mod.rs
Normal file
3
src/api/origin/delete/tests/mod.rs
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
pub mod call;
|
||||
pub mod correct;
|
||||
pub mod only_one;
|
||||
100
src/api/origin/delete/tests/only_one.rs
Normal file
100
src/api/origin/delete/tests/only_one.rs
Normal file
|
|
@ -0,0 +1,100 @@
|
|||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use crate::{
|
||||
api::{
|
||||
auth::{
|
||||
login::{request::LoginRequest, tests::call::test::login},
|
||||
registration::{request::RegistrationRequest, tests::call::tests::registration},
|
||||
},
|
||||
origin::{create::tests::call::tests::create, delete::tests::call::tests::delete, tests::preflight},
|
||||
site::{page::tests::call::tests::page, upload::tests::call::tests::upload},
|
||||
},
|
||||
app,
|
||||
};
|
||||
use axum_test::TestServer as TestClient;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[tokio::test]
|
||||
async fn only_one() {
|
||||
dotenvy::from_filename_override(".env.tests").ok();
|
||||
|
||||
let (app, _state) = app().await.expect("Failed to initialize application!");
|
||||
let client = TestClient::new(app).expect("Failed to run server for testing");
|
||||
|
||||
let first_user_login = Uuid::new_v4();
|
||||
let first_user_password = Uuid::new_v4();
|
||||
|
||||
//* Registration
|
||||
let first_user_registration_request = RegistrationRequest {
|
||||
login: first_user_login.into(),
|
||||
password: first_user_password.into(),
|
||||
};
|
||||
let first_user_registration_response = registration(&client, &first_user_registration_request).await;
|
||||
assert!(first_user_registration_response.is_ok());
|
||||
|
||||
//* Login
|
||||
let first_user_login_request = LoginRequest {
|
||||
login: first_user_login.into(),
|
||||
password: first_user_password.into(),
|
||||
};
|
||||
let first_user_login_response = login(&client, &first_user_login_request).await;
|
||||
assert!(first_user_login_response.is_ok());
|
||||
let first_user_token = first_user_login_response.expect("to never fail").token;
|
||||
|
||||
let first_random_subdomain = Uuid::new_v4().to_string();
|
||||
|
||||
let first_correct_zip_path = "./assets/zips/correct-1.zip";
|
||||
|
||||
//* Correct upload
|
||||
let first_correct_upload_response = upload(
|
||||
&client,
|
||||
&first_user_token,
|
||||
&first_random_subdomain,
|
||||
first_correct_zip_path,
|
||||
)
|
||||
.await;
|
||||
assert!(first_correct_upload_response.is_ok());
|
||||
|
||||
//* Test that page is correct
|
||||
let first_user_correct_zip_index_response = page(&client, "/some/index.html", &first_random_subdomain).await;
|
||||
assert!(first_user_correct_zip_index_response.status_code().is_success());
|
||||
|
||||
//* Add some origin
|
||||
let first_subdomain_first_add_origin_response =
|
||||
create(&client, &first_user_token, &first_random_subdomain, "some").await;
|
||||
assert!(first_subdomain_first_add_origin_response.is_ok());
|
||||
|
||||
//* Add another origin
|
||||
let first_subdomain_second_add_origin_response =
|
||||
create(&client, &first_user_token, &first_random_subdomain, "another").await;
|
||||
assert!(first_subdomain_second_add_origin_response.is_ok());
|
||||
|
||||
let first_subdomain_first_added_origin = first_subdomain_first_add_origin_response.expect("never fail");
|
||||
|
||||
//* Check origin was added
|
||||
let preflight_response = preflight(&client, &first_random_subdomain, "some").await;
|
||||
let preflight_response_allowed_origin = preflight_response
|
||||
.headers()
|
||||
.get(axum::http::header::ACCESS_CONTROL_ALLOW_ORIGIN);
|
||||
|
||||
assert!(preflight_response_allowed_origin.is_some());
|
||||
|
||||
//* Delete
|
||||
let first_subdomain_delete_first_origin_response = delete(
|
||||
&client,
|
||||
&first_user_token,
|
||||
&first_random_subdomain,
|
||||
first_subdomain_first_added_origin.id,
|
||||
)
|
||||
.await;
|
||||
assert!(first_subdomain_delete_first_origin_response.is_ok());
|
||||
|
||||
//* Check origin again
|
||||
let preflight_response = preflight(&client, &first_random_subdomain, "another").await;
|
||||
let preflight_response_allowed_origin = preflight_response
|
||||
.headers()
|
||||
.get(axum::http::header::ACCESS_CONTROL_ALLOW_ORIGIN);
|
||||
|
||||
assert!(preflight_response_allowed_origin.is_some());
|
||||
}
|
||||
}
|
||||
29
src/api/origin/list/error.rs
Normal file
29
src/api/origin/list/error.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
use crate::{services::origin::error::ServiceError as OriginServiceError, Details};
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum ListOriginsError {
|
||||
#[error(transparent)]
|
||||
OriginServiceError(#[from] OriginServiceError),
|
||||
}
|
||||
|
||||
impl From<ListOriginsError> for StatusCode {
|
||||
fn from(value: ListOriginsError) -> Self {
|
||||
match value {
|
||||
ListOriginsError::OriginServiceError(error) => Self::from(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for ListOriginsError {
|
||||
fn into_response(self) -> Response {
|
||||
let reason = self.to_string();
|
||||
let status_code: StatusCode = self.into();
|
||||
|
||||
(status_code, Json(Details { reason })).into_response()
|
||||
}
|
||||
}
|
||||
54
src/api/origin/list/handler.rs
Normal file
54
src/api/origin/list/handler.rs
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
use super::{error::ListOriginsError, response::ListOriginsResponse};
|
||||
use crate::{extractors::*, services::origin::service::Service as CorsService, state::State as AppState};
|
||||
use axum::{extract::State, response::IntoResponse, Json};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// List all origins for specified subdomain for dynamic CORS (Cross-Origin Resource Sharing) management.
|
||||
///
|
||||
/// This endpoint allows users to list all origins that are permitted to access resources
|
||||
/// on their specified subdomains. The action is authenticated using a JWT, and the subdomain must
|
||||
/// be owned by the user making the request. This will be checked by the server.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
tag = "Origins Management and Dynamic Access Control",
|
||||
operation_id = "Get all origins",
|
||||
path = "/api/origin",
|
||||
params(
|
||||
("x-subdomain" = String,
|
||||
Header,
|
||||
description = "'x-subdomain' header represents the name of the subdomain on which the action is to be performed."),
|
||||
),
|
||||
responses(
|
||||
(status = 201, description = "Origins were successfully retrieved for subdomain.", body = ListOriginsResponse),
|
||||
(status = 400, description = "The 'x-subdomain' header is missing or contains invalid characters.", body = Details),
|
||||
(status = 401, description = "Unauthorized: The JWT in the header is invalid or expired.", body = Details),
|
||||
(status = 403, description = "Forbidden: The subdomain is owned by another user.", body = Details),
|
||||
(status = 404, description = "Not Found: The login or subdomain was not found. See details for more information.", body = Details),
|
||||
(status = 500, description = "Internal Server Error: An error occurred on the server.", body = Details),
|
||||
),
|
||||
security(("Bearer-JWT" = []))
|
||||
)]
|
||||
#[tracing::instrument(skip(state))]
|
||||
pub async fn implementation(
|
||||
State(state): State<Arc<AppState>>,
|
||||
SubdomainOwned { user, subdomain }: SubdomainOwned,
|
||||
) -> Result<impl IntoResponse, ListOriginsError> {
|
||||
tracing::trace!(
|
||||
%subdomain.name,
|
||||
%subdomain.id,
|
||||
%user.id,
|
||||
"Retrieving origins list for subdomain...",
|
||||
);
|
||||
|
||||
let origins = CorsService::retrieve_origins_for(subdomain.id, state.connection()).await?;
|
||||
|
||||
tracing::trace!(
|
||||
%subdomain.name,
|
||||
%subdomain.id,
|
||||
%user.id,
|
||||
amount = origins.len(),
|
||||
"Origins list was successfully retrieved!",
|
||||
);
|
||||
|
||||
Ok(Json(ListOriginsResponse { origins }))
|
||||
}
|
||||
5
src/api/origin/list/mod.rs
Normal file
5
src/api/origin/list/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
pub mod error;
|
||||
pub mod handler;
|
||||
pub mod response;
|
||||
#[cfg(test)]
|
||||
pub mod tests;
|
||||
10
src/api/origin/list/response.rs
Normal file
10
src/api/origin/list/response.rs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
use entity::prelude::OriginModel;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, ToSchema)]
|
||||
#[schema(example = json!({"origins": [{"id": 42, "subdomain_id": 1, "value": "https://example.com"}]}))]
|
||||
pub struct ListOriginsResponse {
|
||||
/// List of retrieved origins
|
||||
pub origins: Vec<OriginModel>,
|
||||
}
|
||||
33
src/api/origin/list/tests/call.rs
Normal file
33
src/api/origin/list/tests/call.rs
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use crate::{
|
||||
api::{origin::list::response::ListOriginsResponse, tests::get},
|
||||
Details,
|
||||
};
|
||||
use axum::http::{HeaderName, HeaderValue, StatusCode};
|
||||
use axum_test::TestServer as TestClient;
|
||||
use std::fmt::Display;
|
||||
|
||||
pub async fn list<T, S>(
|
||||
client: &TestClient,
|
||||
token: T,
|
||||
subdomain: S,
|
||||
) -> Result<ListOriginsResponse, (StatusCode, Details)>
|
||||
where
|
||||
T: Display,
|
||||
S: AsRef<str> + Display,
|
||||
{
|
||||
let response = get(client, "/api/origin")
|
||||
.add_header(
|
||||
HeaderName::from_static("x-subdomain"),
|
||||
HeaderValue::from_str(subdomain.as_ref()).expect("Failed to convert subdomain name to header value!"),
|
||||
)
|
||||
.authorization_bearer(token)
|
||||
.await;
|
||||
|
||||
match response.status_code().is_success() {
|
||||
true => Ok(response.json()),
|
||||
false => Err((response.status_code(), response.json())),
|
||||
}
|
||||
}
|
||||
}
|
||||
94
src/api/origin/list/tests/correct.rs
Normal file
94
src/api/origin/list/tests/correct.rs
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use crate::{
|
||||
api::{
|
||||
auth::{
|
||||
login::{request::LoginRequest, tests::call::test::login},
|
||||
registration::{request::RegistrationRequest, tests::call::tests::registration},
|
||||
},
|
||||
origin::{create::tests::call::tests::create, list::tests::call::tests::list, tests::preflight},
|
||||
site::{page::tests::call::tests::page, upload::tests::call::tests::upload},
|
||||
},
|
||||
app,
|
||||
};
|
||||
use axum_test::TestServer as TestClient;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[tokio::test]
|
||||
async fn correct() {
|
||||
dotenvy::from_filename_override(".env.tests").ok();
|
||||
|
||||
let (app, _state) = app().await.expect("Failed to initialize application!");
|
||||
let app_cloned = app.clone();
|
||||
|
||||
let client = TestClient::new(app_cloned).expect("Failed to run server for testing");
|
||||
|
||||
let first_user_login = Uuid::new_v4();
|
||||
let first_user_password = Uuid::new_v4();
|
||||
|
||||
//* Registration
|
||||
let first_user_registration_request = RegistrationRequest {
|
||||
login: first_user_login.into(),
|
||||
password: first_user_password.into(),
|
||||
};
|
||||
let first_user_registration_response = registration(&client, &first_user_registration_request).await;
|
||||
assert!(first_user_registration_response.is_ok());
|
||||
|
||||
//* Login
|
||||
let first_user_login_request = LoginRequest {
|
||||
login: first_user_login.into(),
|
||||
password: first_user_password.into(),
|
||||
};
|
||||
let first_user_login_response = login(&client, &first_user_login_request).await;
|
||||
assert!(first_user_login_response.is_ok());
|
||||
let first_user_token = first_user_login_response.expect("to never fail").token;
|
||||
|
||||
let first_random_subdomain = Uuid::new_v4().to_string();
|
||||
|
||||
let first_correct_zip_path = "./assets/zips/correct-1.zip";
|
||||
|
||||
//* Correct upload
|
||||
let first_correct_upload_response = upload(
|
||||
&client,
|
||||
&first_user_token,
|
||||
&first_random_subdomain,
|
||||
first_correct_zip_path,
|
||||
)
|
||||
.await;
|
||||
assert!(first_correct_upload_response.is_ok());
|
||||
|
||||
//* Test that page is correct
|
||||
let first_user_correct_zip_index_response = page(&client, "/some/index.html", &first_random_subdomain).await;
|
||||
assert!(first_user_correct_zip_index_response.status_code().is_success());
|
||||
|
||||
//* Add some origin
|
||||
|
||||
let origins = ["some", "another", "third"];
|
||||
|
||||
for origin in origins {
|
||||
let first_subdomain_add_origin_response =
|
||||
create(&client, &first_user_token, &first_random_subdomain, origin).await;
|
||||
assert!(first_subdomain_add_origin_response.is_ok());
|
||||
|
||||
let preflight_response = preflight(&client, &first_random_subdomain, origin).await;
|
||||
let preflight_response_allowed_origin = preflight_response
|
||||
.headers()
|
||||
.get(axum::http::header::ACCESS_CONTROL_ALLOW_ORIGIN);
|
||||
|
||||
assert!(preflight_response_allowed_origin.is_some());
|
||||
}
|
||||
|
||||
let origins_list = list(&client, &first_user_token, &first_random_subdomain).await;
|
||||
|
||||
assert!(origins_list.is_ok());
|
||||
|
||||
let origins_list = origins_list.expect("never fail");
|
||||
|
||||
assert!(origins_list
|
||||
.origins
|
||||
.iter()
|
||||
.map(|o| &o.value)
|
||||
.all(|value| origins.contains(&value.as_str())));
|
||||
assert_eq!(origins_list.origins.len(), origins.len());
|
||||
}
|
||||
}
|
||||
2
src/api/origin/list/tests/mod.rs
Normal file
2
src/api/origin/list/tests/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub mod call;
|
||||
pub mod correct;
|
||||
66
src/api/origin/mod.rs
Normal file
66
src/api/origin/mod.rs
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
use crate::state::State as AppState;
|
||||
use axum::{
|
||||
routing::{delete, get, post},
|
||||
Router,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use utoipa::ToSchema;
|
||||
|
||||
pub mod create;
|
||||
pub mod delete;
|
||||
pub mod list;
|
||||
pub mod purge;
|
||||
pub mod retrieve;
|
||||
|
||||
pub fn router() -> Router<Arc<AppState>> {
|
||||
Router::new()
|
||||
.route("/", post(create::handler::implementation))
|
||||
.route("/", get(list::handler::implementation))
|
||||
.route("/", delete(purge::handler::implementation))
|
||||
.route("/:id", delete(delete::handler::implementation))
|
||||
.route("/:id", get(retrieve::handler::implementation))
|
||||
}
|
||||
|
||||
// We need this as utoipa
|
||||
// currently does not support types
|
||||
// from external crates
|
||||
#[derive(ToSchema)]
|
||||
#[schema(as = OriginModel)]
|
||||
pub struct OriginModelSchema {
|
||||
pub id: i64,
|
||||
pub subdomain_id: i64,
|
||||
pub value: String,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use axum::http::{HeaderName, HeaderValue};
|
||||
use axum_test::{TestResponse, TestServer};
|
||||
|
||||
//? That is a common call of preflight
|
||||
//? for testing in all origins handlers
|
||||
pub async fn preflight<S, O>(app: &TestServer, subdomain: S, origin: O) -> TestResponse
|
||||
where
|
||||
S: AsRef<str>,
|
||||
O: AsRef<str>,
|
||||
{
|
||||
app.method(axum::http::Method::OPTIONS, "/")
|
||||
.add_header(
|
||||
HeaderName::from_static("x-subdomain"),
|
||||
HeaderValue::from_str(subdomain.as_ref()).expect("Failed to convert subdomain name to header value!"),
|
||||
)
|
||||
.add_header(
|
||||
axum::http::header::ORIGIN,
|
||||
HeaderValue::from_str(origin.as_ref()).expect("Failed to convert origin to header value!"),
|
||||
)
|
||||
.add_header(
|
||||
axum::http::header::ACCESS_CONTROL_REQUEST_METHOD,
|
||||
HeaderValue::from_static("GET"),
|
||||
)
|
||||
.add_header(
|
||||
axum::http::header::ACCESS_CONTROL_REQUEST_HEADERS,
|
||||
HeaderValue::from_static("x-subdomain"),
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
29
src/api/origin/purge/error.rs
Normal file
29
src/api/origin/purge/error.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
use crate::{services::origin::error::ServiceError as OriginServiceError, Details};
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum DeleteOriginsError {
|
||||
#[error(transparent)]
|
||||
OriginServiceError(#[from] OriginServiceError),
|
||||
}
|
||||
|
||||
impl From<DeleteOriginsError> for StatusCode {
|
||||
fn from(value: DeleteOriginsError) -> Self {
|
||||
match value {
|
||||
DeleteOriginsError::OriginServiceError(error) => Self::from(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for DeleteOriginsError {
|
||||
fn into_response(self) -> Response {
|
||||
let reason = self.to_string();
|
||||
let status_code: StatusCode = self.into();
|
||||
|
||||
(status_code, Json(Details { reason })).into_response()
|
||||
}
|
||||
}
|
||||
53
src/api/origin/purge/handler.rs
Normal file
53
src/api/origin/purge/handler.rs
Normal file
|
|
@ -0,0 +1,53 @@
|
|||
use super::error::DeleteOriginsError;
|
||||
use crate::{extractors::*, services::origin::service::Service as CorsService, state::State as AppState};
|
||||
use axum::{extract::State, http::StatusCode, response::IntoResponse};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Delete all origins for specified subdomain for dynamic CORS (Cross-Origin Resource Sharing) management.
|
||||
///
|
||||
/// This endpoint allows users to delete all origins that are permitted to access resources
|
||||
/// on their specified subdomains. The action is authenticated using a JWT, and the subdomain must
|
||||
/// be owned by the user making the request. This will be checked by the server.
|
||||
#[utoipa::path(
|
||||
delete,
|
||||
tag = "Origins Management and Dynamic Access Control",
|
||||
operation_id = "Delete all origins",
|
||||
path = "/api/origin",
|
||||
params(
|
||||
("x-subdomain" = String,
|
||||
Header,
|
||||
description = "'x-subdomain' header represents the name of the subdomain on which the action is to be performed."),
|
||||
),
|
||||
responses(
|
||||
(status = 204, description = "Origins were successfully deleted for subdomain."),
|
||||
(status = 400, description = "The 'x-subdomain' header is missing or contains invalid characters.", body = Details),
|
||||
(status = 401, description = "Unauthorized: The JWT in the header is invalid or expired.", body = Details),
|
||||
(status = 403, description = "Forbidden: The subdomain is owned by another user.", body = Details),
|
||||
(status = 404, description = "Not Found: The login or subdomain was not found. See details for more information.", body = Details),
|
||||
(status = 500, description = "Internal Server Error: An error occurred on the server.", body = Details),
|
||||
),
|
||||
security(("Bearer-JWT" = []))
|
||||
)]
|
||||
#[tracing::instrument(skip(state))]
|
||||
pub async fn implementation(
|
||||
State(state): State<Arc<AppState>>,
|
||||
SubdomainOwned { user, subdomain }: SubdomainOwned,
|
||||
) -> Result<impl IntoResponse, DeleteOriginsError> {
|
||||
tracing::trace!(
|
||||
%subdomain.name,
|
||||
%subdomain.id,
|
||||
%user.id,
|
||||
"Deleting all origins for subdomain...",
|
||||
);
|
||||
|
||||
let rows_affected = CorsService::delete_origins_for(subdomain.id, state.connection()).await?;
|
||||
tracing::trace!(
|
||||
%rows_affected,
|
||||
%subdomain.name,
|
||||
%subdomain.id,
|
||||
%user.id,
|
||||
"Origins were successfully deleted!",
|
||||
);
|
||||
|
||||
Ok(StatusCode::NO_CONTENT)
|
||||
}
|
||||
4
src/api/origin/purge/mod.rs
Normal file
4
src/api/origin/purge/mod.rs
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
pub mod error;
|
||||
pub mod handler;
|
||||
#[cfg(test)]
|
||||
pub mod tests;
|
||||
26
src/api/origin/purge/tests/call.rs
Normal file
26
src/api/origin/purge/tests/call.rs
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use crate::{api::tests::delete as delete_helper, Details};
|
||||
use axum::http::{HeaderName, HeaderValue, StatusCode};
|
||||
use axum_test::TestServer as TestClient;
|
||||
use std::fmt::Display;
|
||||
|
||||
pub async fn purge<T, S>(client: &TestClient, token: T, subdomain: S) -> Result<(), (StatusCode, Details)>
|
||||
where
|
||||
T: Display,
|
||||
S: AsRef<str> + Display,
|
||||
{
|
||||
let response = delete_helper(client, "/api/origin", Option::<()>::None)
|
||||
.add_header(
|
||||
HeaderName::from_static("x-subdomain"),
|
||||
HeaderValue::from_str(subdomain.as_ref()).expect("Failed to convert subdomain name to header value!"),
|
||||
)
|
||||
.authorization_bearer(token)
|
||||
.await;
|
||||
|
||||
match response.status_code().is_success() {
|
||||
true => Ok(()),
|
||||
false => Err((response.status_code(), response.json())),
|
||||
}
|
||||
}
|
||||
}
|
||||
103
src/api/origin/purge/tests/correct.rs
Normal file
103
src/api/origin/purge/tests/correct.rs
Normal file
|
|
@ -0,0 +1,103 @@
|
|||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use crate::{
|
||||
api::{
|
||||
auth::{
|
||||
login::{request::LoginRequest, tests::call::test::login},
|
||||
registration::{request::RegistrationRequest, tests::call::tests::registration},
|
||||
},
|
||||
origin::{
|
||||
create::tests::call::tests::create, list::tests::call::tests::list, purge::tests::call::tests::purge,
|
||||
tests::preflight,
|
||||
},
|
||||
site::{page::tests::call::tests::page, upload::tests::call::tests::upload},
|
||||
},
|
||||
app,
|
||||
};
|
||||
use axum_test::TestServer as TestClient;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[tokio::test]
|
||||
async fn correct() {
|
||||
dotenvy::from_filename_override(".env.tests").ok();
|
||||
|
||||
let (app, _state) = app().await.expect("Failed to initialize application!");
|
||||
let app_cloned = app.clone();
|
||||
|
||||
let client = TestClient::new(app_cloned).expect("Failed to run server for testing");
|
||||
|
||||
let first_user_login = Uuid::new_v4();
|
||||
let first_user_password = Uuid::new_v4();
|
||||
|
||||
//* Registration
|
||||
let first_user_registration_request = RegistrationRequest {
|
||||
login: first_user_login.into(),
|
||||
password: first_user_password.into(),
|
||||
};
|
||||
let first_user_registration_response = registration(&client, &first_user_registration_request).await;
|
||||
assert!(first_user_registration_response.is_ok());
|
||||
|
||||
//* Login
|
||||
let first_user_login_request = LoginRequest {
|
||||
login: first_user_login.into(),
|
||||
password: first_user_password.into(),
|
||||
};
|
||||
let first_user_login_response = login(&client, &first_user_login_request).await;
|
||||
assert!(first_user_login_response.is_ok());
|
||||
let first_user_token = first_user_login_response.expect("to never fail").token;
|
||||
|
||||
let first_random_subdomain = Uuid::new_v4().to_string();
|
||||
|
||||
let first_correct_zip_path = "./assets/zips/correct-1.zip";
|
||||
|
||||
//* Correct upload
|
||||
let first_correct_upload_response = upload(
|
||||
&client,
|
||||
&first_user_token,
|
||||
&first_random_subdomain,
|
||||
first_correct_zip_path,
|
||||
)
|
||||
.await;
|
||||
assert!(first_correct_upload_response.is_ok());
|
||||
|
||||
//* Test that page is correct
|
||||
let first_user_correct_zip_index_response = page(&client, "/some/index.html", &first_random_subdomain).await;
|
||||
assert!(first_user_correct_zip_index_response.status_code().is_success());
|
||||
|
||||
//* Add some origin
|
||||
|
||||
let origins = ["some", "another", "third"];
|
||||
|
||||
for origin in origins {
|
||||
let first_subdomain_add_origin_response =
|
||||
create(&client, &first_user_token, &first_random_subdomain, origin).await;
|
||||
assert!(first_subdomain_add_origin_response.is_ok());
|
||||
|
||||
let preflight_response = preflight(&client, &first_random_subdomain, origin).await;
|
||||
let preflight_response_allowed_origin = preflight_response
|
||||
.headers()
|
||||
.get(axum::http::header::ACCESS_CONTROL_ALLOW_ORIGIN);
|
||||
|
||||
assert!(preflight_response_allowed_origin.is_some());
|
||||
}
|
||||
|
||||
let origins_list = list(&client, &first_user_token, &first_random_subdomain).await;
|
||||
assert!(origins_list.is_ok());
|
||||
|
||||
let origins_list = origins_list.expect("never fail");
|
||||
|
||||
assert!(origins_list
|
||||
.origins
|
||||
.iter()
|
||||
.map(|o| &o.value)
|
||||
.all(|value| origins.contains(&value.as_str())));
|
||||
assert_eq!(origins_list.origins.len(), origins.len());
|
||||
|
||||
let first_subdomain_purge_response = purge(&client, &first_user_token, &first_random_subdomain).await;
|
||||
assert!(first_subdomain_purge_response.is_ok());
|
||||
|
||||
let origins_list = list(&client, &first_user_token, &first_random_subdomain).await;
|
||||
assert!(origins_list.is_ok());
|
||||
assert!(origins_list.expect("never fail").origins.is_empty());
|
||||
}
|
||||
}
|
||||
2
src/api/origin/purge/tests/mod.rs
Normal file
2
src/api/origin/purge/tests/mod.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
pub mod call;
|
||||
pub mod correct;
|
||||
29
src/api/origin/retrieve/error.rs
Normal file
29
src/api/origin/retrieve/error.rs
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
use crate::{services::origin::error::ServiceError as OriginServiceError, Details};
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
|
||||
#[derive(thiserror::Error, Debug)]
|
||||
pub enum GetOriginError {
|
||||
#[error(transparent)]
|
||||
OriginServiceError(#[from] OriginServiceError),
|
||||
}
|
||||
|
||||
impl From<GetOriginError> for StatusCode {
|
||||
fn from(value: GetOriginError) -> Self {
|
||||
match value {
|
||||
GetOriginError::OriginServiceError(error) => Self::from(error),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for GetOriginError {
|
||||
fn into_response(self) -> Response {
|
||||
let reason = self.to_string();
|
||||
let status_code: StatusCode = self.into();
|
||||
|
||||
(status_code, Json(Details { reason })).into_response()
|
||||
}
|
||||
}
|
||||
59
src/api/origin/retrieve/handler.rs
Normal file
59
src/api/origin/retrieve/handler.rs
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
use super::{error::GetOriginError, response::GetOriginResponse};
|
||||
use crate::{extractors::*, services::origin::service::Service as CorsService, state::State as AppState};
|
||||
use axum::{
|
||||
extract::{Path, State},
|
||||
response::IntoResponse,
|
||||
Json,
|
||||
};
|
||||
use std::sync::Arc;
|
||||
|
||||
/// Get specified origin [by id] for specified subdomain for dynamic CORS (Cross-Origin Resource Sharing) management.
|
||||
///
|
||||
/// This endpoint allows users to get specified origin by id that is permitted to access resources
|
||||
/// on specified subdomain. The action is authenticated using a JWT, and the subdomain must
|
||||
/// be owned by the user making the request. This will be checked by the server.
|
||||
#[utoipa::path(
|
||||
get,
|
||||
tag = "Origins Management and Dynamic Access Control",
|
||||
operation_id = "Get origin by id",
|
||||
path = "/api/origin/{id}",
|
||||
params(
|
||||
("x-subdomain" = String,
|
||||
Header,
|
||||
description = "'x-subdomain' header represents the name of the subdomain on which the action is to be performed."),
|
||||
("id" = i64, Path, description = "Id of the origin to retrieve"),
|
||||
),
|
||||
responses(
|
||||
(status = 200, description = "Origin was successfully retrieved.", body = GetOriginResponse),
|
||||
(status = 400, description = "The 'x-subdomain' header is missing or contains invalid characters.", body = Details),
|
||||
(status = 401, description = "Unauthorized: The JWT in the header is invalid or expired.", body = Details),
|
||||
(status = 403, description = "Forbidden: The subdomain is owned by another user.", body = Details),
|
||||
(status = 404, description = "Not Found: The login or subdomain or origin was not found. See details for more information.", body = Details),
|
||||
(status = 500, description = "Internal Server Error: An error occurred on the server.", body = Details),
|
||||
),
|
||||
security(("Bearer-JWT" = []))
|
||||
)]
|
||||
#[tracing::instrument(skip(state))]
|
||||
pub async fn implementation(
|
||||
State(state): State<Arc<AppState>>,
|
||||
SubdomainOwned { user, subdomain }: SubdomainOwned,
|
||||
Path(origin_id): Path<i64>,
|
||||
) -> Result<impl IntoResponse, GetOriginError> {
|
||||
tracing::trace!(
|
||||
%origin_id,
|
||||
%subdomain.name,
|
||||
%subdomain.id,
|
||||
%user.id,
|
||||
"Retrieving origin by id for subdomain...",
|
||||
);
|
||||
let origin = CorsService::fetch_origin_of(subdomain.id, origin_id, state.connection()).await?;
|
||||
tracing::trace!(
|
||||
?origin,
|
||||
%subdomain.name,
|
||||
%subdomain.id,
|
||||
%user.id,
|
||||
"Origin was successfully retrieved by id",
|
||||
);
|
||||
|
||||
Ok(Json(GetOriginResponse { origin }))
|
||||
}
|
||||
5
src/api/origin/retrieve/mod.rs
Normal file
5
src/api/origin/retrieve/mod.rs
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
pub mod error;
|
||||
pub mod handler;
|
||||
pub mod response;
|
||||
#[cfg(test)]
|
||||
pub mod tests;
|
||||
9
src/api/origin/retrieve/response.rs
Normal file
9
src/api/origin/retrieve/response.rs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
use entity::origin::Model as OriginModel;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use utoipa::ToSchema;
|
||||
|
||||
#[derive(Debug, Deserialize, Serialize, ToSchema)]
|
||||
#[schema(example = json!({"origin": {"id": 42, "subdomain_id": 1, "value": "https://example.com"}}))]
|
||||
pub struct GetOriginResponse {
|
||||
pub origin: OriginModel,
|
||||
}
|
||||
34
src/api/origin/retrieve/tests/call.rs
Normal file
34
src/api/origin/retrieve/tests/call.rs
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use crate::{
|
||||
api::{origin::retrieve::response::GetOriginResponse, tests::get},
|
||||
Details,
|
||||
};
|
||||
use axum::http::{HeaderName, HeaderValue, StatusCode};
|
||||
use axum_test::TestServer as TestClient;
|
||||
use std::fmt::Display;
|
||||
|
||||
pub async fn retrieve<T, S>(
|
||||
client: &TestClient,
|
||||
token: T,
|
||||
subdomain: S,
|
||||
id: i64,
|
||||
) -> Result<GetOriginResponse, (StatusCode, Details)>
|
||||
where
|
||||
T: Display,
|
||||
S: AsRef<str> + Display,
|
||||
{
|
||||
let response = get(client, &format!("/api/origin/{id}"))
|
||||
.add_header(
|
||||
HeaderName::from_static("x-subdomain"),
|
||||
HeaderValue::from_str(subdomain.as_ref()).expect("Failed to convert subdomain name to header value!"),
|
||||
)
|
||||
.authorization_bearer(token)
|
||||
.await;
|
||||
|
||||
match response.status_code().is_success() {
|
||||
true => Ok(response.json()),
|
||||
false => Err((response.status_code(), response.json())),
|
||||
}
|
||||
}
|
||||
}
|
||||
85
src/api/origin/retrieve/tests/correct.rs
Normal file
85
src/api/origin/retrieve/tests/correct.rs
Normal file
|
|
@ -0,0 +1,85 @@
|
|||
#[cfg(test)]
|
||||
pub mod tests {
|
||||
use crate::{
|
||||
api::{
|
||||
auth::{
|
||||
login::{request::LoginRequest, tests::call::test::login},
|
||||
registration::{request::RegistrationRequest, tests::call::tests::registration},
|
||||
},
|
||||
origin::{create::tests::call::tests::create, retrieve::tests::call::tests::retrieve, tests::preflight},
|
||||
site::{page::tests::call::tests::page, upload::tests::call::tests::upload},
|
||||
},
|
||||
app,
|
||||
};
|
||||
use axum_test::TestServer as TestClient;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[tokio::test]
|
||||
async fn correct() {
|
||||
dotenvy::from_filename_override(".env.tests").ok();
|
||||
|
||||
let (app, _state) = app().await.expect("Failed to initialize application!");
|
||||
let app_cloned = app.clone();
|
||||
|
||||
let client = TestClient::new(app_cloned).expect("Failed to run server for testing");
|
||||
|
||||
let first_user_login = Uuid::new_v4();
|
||||
let first_user_password = Uuid::new_v4();
|
||||
|
||||
//* Registration
|
||||
let first_user_registration_request = RegistrationRequest {
|
||||
login: first_user_login.into(),
|
||||
password: first_user_password.into(),
|
||||
};
|
||||
let first_user_registration_response = registration(&client, &first_user_registration_request).await;
|
||||
assert!(first_user_registration_response.is_ok());
|
||||
|
||||
//* Login
|
||||
let first_user_login_request = LoginRequest {
|
||||
login: first_user_login.into(),
|
||||
password: first_user_password.into(),
|
||||
};
|
||||
let first_user_login_response = login(&client, &first_user_login_request).await;
|
||||
assert!(first_user_login_response.is_ok());
|
||||
let first_user_token = first_user_login_response.expect("to never fail").token;
|
||||
|
||||
let first_random_subdomain = Uuid::new_v4().to_string();
|
||||
|
||||
let first_correct_zip_path = "./assets/zips/correct-1.zip";
|
||||
|
||||
//* Correct upload
|
||||
let first_correct_upload_response = upload(
|
||||
&client,
|
||||
&first_user_token,
|
||||
&first_random_subdomain,
|
||||
first_correct_zip_path,
|
||||
)
|
||||
.await;
|
||||
assert!(first_correct_upload_response.is_ok());
|
||||
|
||||
//* Test that page is correct
|
||||
let first_user_correct_zip_index_response = page(&client, "/some/index.html", &first_random_subdomain).await;
|
||||
assert!(first_user_correct_zip_index_response.status_code().is_success());
|
||||
|
||||
//* Add some origin
|
||||
let first_subdomain_add_origin_response =
|
||||
create(&client, &first_user_token, &first_random_subdomain, "some").await;
|
||||
assert!(first_subdomain_add_origin_response.is_ok());
|
||||
|
||||
let origin_id = first_subdomain_add_origin_response.expect("never fail").id;
|
||||
|
||||
let preflight_response = preflight(&client, &first_random_subdomain, "some").await;
|
||||
let preflight_response_allowed_origin = preflight_response
|
||||
.headers()
|
||||
.get(axum::http::header::ACCESS_CONTROL_ALLOW_ORIGIN);
|
||||
|
||||
assert!(preflight_response_allowed_origin.is_some());
|
||||
|
||||
let retrieved_origin_response = retrieve(&client, &first_user_token, &first_random_subdomain, origin_id).await;
|
||||
assert!(retrieved_origin_response.is_ok());
|
||||
|
||||
let retrieved_origin = retrieved_origin_response.expect("never fail");
|
||||
assert_eq!(retrieved_origin.origin.id, origin_id);
|
||||
assert_eq!(retrieved_origin.origin.value, "some");
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue