├── .dockerignore ├── .env-sample ├── .github ├── FUNDING.yml └── workflows │ ├── clippy-fmt.yml │ ├── coverage.yml │ └── linux.yml ├── .gitignore ├── Cargo.lock ├── Cargo.toml ├── Dockerfile ├── LICENSE.md ├── Makefile ├── README.md ├── build.rs ├── config └── default.toml ├── database ├── db-core │ ├── .gitignore │ ├── Cargo.toml │ └── src │ │ ├── errors.rs │ │ ├── lib.rs │ │ ├── ops.rs │ │ └── tests.rs ├── db-sqlx-postgres │ ├── .gitignore │ ├── Cargo.toml │ ├── migrations │ │ ├── 20220213111215_gists_users.sql │ │ ├── 20220213112330_gists_gists.sql │ │ └── 20220214072325_gists_gists_comments_views.sql │ ├── sqlx-data.json │ └── src │ │ ├── errors.rs │ │ ├── lib.rs │ │ └── tests.rs ├── db-sqlx-sqlite │ ├── .gitignore │ ├── Cargo.toml │ ├── migrations │ │ ├── 20220213111519_gists_users.sql │ │ ├── 20220213113228_gists_gists.sql │ │ └── 20220214091158_gists_gists_comments_views.sql │ ├── sqlx-data.json │ └── src │ │ ├── errors.rs │ │ ├── lib.rs │ │ └── tests.rs └── migrator │ ├── .gitignore │ ├── Cargo.lock │ ├── Cargo.toml │ ├── sqlx-data.json │ └── src │ └── main.rs ├── docs └── ecosystem.md ├── scripts └── ci.sh ├── sqlx-data.json ├── src ├── api │ ├── mod.rs │ └── v1 │ │ ├── account │ │ ├── mod.rs │ │ └── test.rs │ │ ├── auth.rs │ │ ├── gists.rs │ │ ├── meta.rs │ │ ├── mod.rs │ │ ├── routes.rs │ │ └── tests │ │ ├── auth.rs │ │ ├── mod.rs │ │ └── protected.rs ├── data │ ├── api │ │ ├── mod.rs │ │ └── v1 │ │ │ ├── account.rs │ │ │ ├── auth.rs │ │ │ ├── gists.rs │ │ │ ├── mod.rs │ │ │ ├── render_html.rs │ │ │ └── tests │ │ │ ├── accounts.rs │ │ │ ├── auth.rs │ │ │ └── mod.rs │ └── mod.rs ├── db.rs ├── demo.rs ├── errors.rs ├── main.rs ├── pages │ ├── auth │ │ ├── login.rs │ │ ├── mod.rs │ │ ├── register.rs │ │ └── test.rs │ ├── errors.rs │ ├── gists │ │ ├── mod.rs │ │ ├── new.rs │ │ ├── tests.rs │ │ └── view.rs │ ├── mod.rs │ └── routes.rs ├── routes.rs ├── settings.rs ├── static_assets │ ├── filemap.rs │ ├── mod.rs │ └── static_files.rs ├── tests.rs └── utils.rs ├── static └── cache │ └── css │ └── main.css ├── templates ├── components │ ├── base.html │ ├── comments │ │ ├── index.html │ │ └── new.html │ ├── error.html │ ├── footer.html │ └── nav │ │ ├── auth.html │ │ ├── base.html │ │ └── pub.html └── pages │ ├── auth │ ├── base.html │ ├── demo.html │ ├── login.html │ └── register.html │ └── gists │ ├── base.html │ ├── explore.html │ ├── new │ └── index.html │ └── view │ ├── _filename.html │ ├── _meta.html │ ├── _text.html │ └── index.html └── website ├── .gitignore ├── bin └── zola ├── config.toml ├── static └── css │ └── main.css └── templates ├── footer.html └── index.html /.dockerignore: -------------------------------------------------------------------------------- 1 | /target 2 | */**/target 3 | tarpaulin-report.html 4 | .env 5 | .env-sample 6 | cobertura.xml 7 | coverage/ 8 | node_modules/ 9 | /static/cache/bundle/* 10 | scripts/creds.py 11 | -------------------------------------------------------------------------------- /.env-sample: -------------------------------------------------------------------------------- 1 | export POSTGRES_DATABASE_URL="postgres://postgres:password@localhost:5432/postgres" 2 | export SQLITE_TMP="$(pwd)/database/db-sqlx-sqlite/tmp" 3 | export SQLITE_DATABASE_URL="sqlite://$SQLITE_TMP/admin.db" 4 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | # github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] 4 | # patreon: # Replace with a single Patreon username 5 | open_collective: # Replace with a single Open Collective username 6 | # ko_fi: # Replace with a single Ko-fi username 7 | # tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel 8 | # community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry 9 | liberapay: realaravinth 10 | issuehunt: # Replace with a single IssueHunt username 11 | # otechie: # Replace with a single Otechie username 12 | custom: ['https://batsense.net/donate'] 13 | -------------------------------------------------------------------------------- /.github/workflows/clippy-fmt.yml: -------------------------------------------------------------------------------- 1 | name: Lint 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened] 6 | push: 7 | branches: 8 | - master 9 | 10 | jobs: 11 | fmt: 12 | runs-on: ubuntu-latest 13 | steps: 14 | - uses: actions/checkout@v2 15 | 16 | - name: Install Rust 17 | uses: actions-rs/toolchain@v1 18 | with: 19 | toolchain: stable 20 | components: rustfmt 21 | - name: Check with rustfmt 22 | uses: actions-rs/cargo@v1 23 | with: 24 | command: fmt 25 | args: --all -- --check 26 | 27 | clippy: 28 | runs-on: ubuntu-latest 29 | steps: 30 | - uses: actions/checkout@v2 31 | 32 | - name: Install Rust 33 | uses: actions-rs/toolchain@v1 34 | with: 35 | toolchain: stable 36 | components: clippy 37 | override: true 38 | 39 | - name: Check with Clippy 40 | uses: actions-rs/clippy-check@v1 41 | with: 42 | token: ${{ secrets.GITHUB_TOKEN }} 43 | args: --workspace --tests --all-features 44 | -------------------------------------------------------------------------------- /.github/workflows/coverage.yml: -------------------------------------------------------------------------------- 1 | name: Coverage 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened] 6 | push: 7 | branches: 8 | - master 9 | 10 | jobs: 11 | build_and_test: 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | version: 16 | - stable 17 | 18 | services: 19 | postgres: 20 | image: postgres 21 | env: 22 | POSTGRES_PASSWORD: password 23 | POSTGRES_USER: postgres 24 | POSTGRES_DB: postgres 25 | options: >- 26 | --health-cmd pg_isready 27 | --health-interval 10s 28 | --health-timeout 5s 29 | --health-retries 5 30 | ports: 31 | - 5432:5432 32 | 33 | name: ${{ matrix.version }} - x86_64-unknown-linux-gnu 34 | runs-on: ubuntu-latest 35 | 36 | steps: 37 | - uses: actions/checkout@v2 38 | - name: ⚡ Cache 39 | uses: actions/cache@v2 40 | with: 41 | path: | 42 | ~/.cargo/registry 43 | ~/.cargo/git 44 | target 45 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 46 | 47 | - name: Install ${{ matrix.version }} 48 | uses: actions-rs/toolchain@v1 49 | with: 50 | toolchain: ${{ matrix.version }}-x86_64-unknown-linux-gnu 51 | profile: minimal 52 | override: true 53 | 54 | - name: load env 55 | run: | 56 | source .env-sample \ 57 | && echo "POSTGRES_DATABASE_URL=$POSTGRES_DATABASE_URL" >> $GITHUB_ENV \ 58 | && echo "SQLITE_DATABASE_URL=$SQLITE_DATABASE_URL" >> $GITHUB_ENV 59 | 60 | - name: run migrations 61 | run: make migrate 62 | env: 63 | GIT_HASH: 8e77345f1597e40c2e266cb4e6dee74888918a61 # dummy value 64 | POSTGRES_DATABASE_URL: "${{ env.POSTGRES_DATABASE_URL }}" 65 | SQLITE_DATABASE_URL: "${{ env.SQLITE_DATABASE_URL }}" 66 | 67 | - name: Generate coverage file 68 | if: matrix.version == 'stable' && (github.ref == 'refs/heads/master' || github.event_name == 'pull_request') 69 | uses: actions-rs/tarpaulin@v0.1 70 | env: 71 | # GIT_HASH is dummy value. I guess build.rs is skipped in tarpaulin 72 | # execution so this value is required for preventing meta tests from 73 | # panicking 74 | GIT_HASH: 8e77345f1597e40c2e266cb4e6dee74888918a61 75 | POSTGRES_DATABASE_URL: "${{ env.POSTGRES_DATABASE_URL }}" 76 | SQLITE_DATABASE_URL: "${{ env.SQLITE_DATABASE_URL }}" 77 | with: 78 | args: "--all-features --no-fail-fast --workspace=database/db-sqlx-postgres,database/db-sqlx-sqlite,. -t 1200" 79 | 80 | - name: Upload to Codecov 81 | if: matrix.version == 'stable' && (github.ref == 'refs/heads/master' || github.event_name == 'pull_request') 82 | uses: codecov/codecov-action@v2 83 | -------------------------------------------------------------------------------- /.github/workflows/linux.yml: -------------------------------------------------------------------------------- 1 | name: Build 2 | 3 | on: 4 | pull_request: 5 | types: [opened, synchronize, reopened] 6 | push: 7 | branches: 8 | - master 9 | 10 | jobs: 11 | build_and_test: 12 | strategy: 13 | fail-fast: false 14 | matrix: 15 | version: 16 | - stable 17 | # - nightly 18 | 19 | name: ${{ matrix.version }} - x86_64-unknown-linux-gnu 20 | runs-on: ubuntu-latest 21 | 22 | services: 23 | postgres: 24 | image: postgres 25 | env: 26 | POSTGRES_PASSWORD: password 27 | POSTGRES_USER: postgres 28 | POSTGRES_DB: postgres 29 | options: >- 30 | --health-cmd pg_isready 31 | --health-interval 10s 32 | --health-timeout 5s 33 | --health-retries 5 34 | ports: 35 | - 5432:5432 36 | 37 | steps: 38 | - uses: actions/checkout@v2 39 | - name: ⚡ Cache 40 | uses: actions/cache@v2 41 | with: 42 | path: | 43 | /var/lib/docker 44 | ~/.cargo/registry 45 | ~/.cargo/git 46 | target 47 | key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} 48 | 49 | - name: Install Zola 50 | run: ./scripts/ci.sh install 51 | 52 | - name: Login to DockerHub 53 | if: (github.ref == 'refs/heads/master' || github.event_name == 'push') && github.repository == 'realaravinth/gitpad' 54 | uses: docker/login-action@v1 55 | with: 56 | username: ${{ secrets.DOCKERHUB_USERNAME }} 57 | password: ${{ secrets.DOCKERHUB_TOKEN }} 58 | 59 | - name: Install ${{ matrix.version }} 60 | uses: actions-rs/toolchain@v1 61 | with: 62 | toolchain: ${{ matrix.version }}-x86_64-unknown-linux-gnu 63 | profile: minimal 64 | override: true 65 | 66 | - name: load env 67 | run: | 68 | source .env-sample \ 69 | && echo "POSTGRES_DATABASE_URL=$POSTGRES_DATABASE_URL" >> $GITHUB_ENV \ 70 | && echo "SQLITE_DATABASE_URL=$SQLITE_DATABASE_URL" >> $GITHUB_ENV 71 | 72 | - name: build 73 | run: make 74 | env: 75 | POSTGRES_DATABASE_URL: "${{ env.POSTGRES_DATABASE_URL }}" 76 | SQLITE_DATABASE_URL: "${{ env.SQLITE_DATABASE_URL }}" 77 | 78 | - name: build docker images 79 | if: matrix.version == 'stable' 80 | run: make docker 81 | 82 | - name: publish docker images 83 | if: matrix.version == 'stable' && (github.ref == 'refs/heads/master' || github.event_name == 'push') && github.repository == 'realaravinth/gitpad' 84 | run: make docker-publish 85 | 86 | - name: run migrations 87 | run: make migrate 88 | env: 89 | GIT_HASH: 8e77345f1597e40c2e266cb4e6dee74888918a61 # dummy value 90 | POSTGRES_DATABASE_URL: "${{ env.POSTGRES_DATABASE_URL }}" 91 | SQLITE_DATABASE_URL: "${{ env.SQLITE_DATABASE_URL }}" 92 | 93 | - name: run tests 94 | timeout-minutes: 40 95 | run: make test 96 | env: 97 | GIT_HASH: 8e77345f1597e40c2e266cb4e6dee74888918a61 # dummy value 98 | POSTGRES_DATABASE_URL: "${{ env.POSTGRES_DATABASE_URL }}" 99 | SQLITE_DATABASE_URL: "${{ env.SQLITE_DATABASE_URL }}" 100 | 101 | - name: generate documentation 102 | if: matrix.version == 'stable' && (github.ref == 'refs/heads/master' || github.event_name == 'push') && github.repository == 'realaravinth/gitpad' 103 | run: make doc 104 | env: 105 | GIT_HASH: 8e77345f1597e40c2e266cb4e6dee74888918a61 # dummy value 106 | POSTGRES_DATABASE_URL: "${{ env.POSTGRES_DATABASE_URL }}" 107 | SQLITE_DATABASE_URL: "${{ env.SQLITE_DATABASE_URL }}" 108 | 109 | - name: Deploy to GitHub Pages 110 | if: matrix.version == 'stable' && (github.ref == 'refs/heads/master' || github.event_name == 'push') && github.repository == 'realaravinth/gitpad' 111 | uses: JamesIves/github-pages-deploy-action@3.7.1 112 | with: 113 | branch: gh-pages 114 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 115 | FOLDER: deploy-static 116 | 117 | - name: deploy 118 | if: ${{ github.event_name == 'push' && github.ref == 'refs/heads/master' && github.repository == 'realaravinth/realaravinth' }} 119 | run: >- 120 | curl --location --request POST "https://deploy.batsense.net/api/v1/update" --header 'Content-Type: application/json' --data-raw "{ \"secret\": \"${{ secrets.DEPLOY_TOKEN }}\", \"branch\": \"gh-pages\" }" 121 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .env 3 | tarpaulin-report.html 4 | **/tmp/ 5 | assets 6 | src/cache_buster_data.json 7 | deploy-static 8 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | authors = ["realaravinth "] 3 | build = "build.rs" 4 | description = "Self-Hosted GitHub Gists" 5 | documentation = "https://github.con/realaravinth/gitpad" 6 | edition = "2021" 7 | homepage = "https://github.com/realaravinth/gitpad" 8 | license = "AGPLv3 or later version" 9 | name = "gitpad" 10 | readme = "https://github.com/realaravinth/gitpad/blob/master/README.md" 11 | repository = "https://github.com/realaravinth/gitpad" 12 | version = "0.1.0" 13 | [build-dependencies] 14 | mime = "0.3.16" 15 | serde_json = "1" 16 | 17 | [build-dependencies.cache-buster] 18 | git = "https://github.com/realaravinth/cache-buster" 19 | 20 | [dependencies] 21 | actix-http = "3.0.0-rc.2" 22 | actix-identity = "0.4.0-beta.8" 23 | actix-rt = "2.6.0" 24 | actix-web = "4.0.0-rc.3" 25 | config = "0.11" 26 | derive_more = "0.99" 27 | futures = "0.3.21" 28 | git2 = "0.13.25" 29 | lazy_static = "1.4" 30 | log = "0.4" 31 | mime = "0.3.16" 32 | mime_guess = "2.0.3" 33 | num_cpus = "1.13" 34 | num_enum = "0.5.6" 35 | pretty_env_logger = "0.4" 36 | pulldown-cmark = "*" 37 | rand = "0.8.4" 38 | rust-embed = "6.3.0" 39 | serde_json = "1" 40 | syntect = "*" 41 | url = "2.2" 42 | urlencoding = "2.1.0" 43 | 44 | [dependencies.actix-auth-middleware] 45 | branch = "v4" 46 | features = ["actix_identity_backend"] 47 | git = "https://github.com/realaravinth/actix-auth-middleware" 48 | version = "0.2" 49 | 50 | [dependencies.argon2-creds] 51 | branch = "master" 52 | git = "https://github.com/realaravinth/argon2-creds" 53 | 54 | [dependencies.cache-buster] 55 | git = "https://github.com/realaravinth/cache-buster" 56 | 57 | [dependencies.db-core] 58 | path = "./database/db-core" 59 | 60 | [dependencies.db-sqlx-postgres] 61 | path = "./database/db-sqlx-postgres" 62 | 63 | [dependencies.db-sqlx-sqlite] 64 | path = "./database/db-sqlx-sqlite" 65 | 66 | [dependencies.my-codegen] 67 | git = "https://github.com/realaravinth/actix-web" 68 | package = "actix-web-codegen" 69 | 70 | [dependencies.serde] 71 | features = ["derive"] 72 | version = "1" 73 | 74 | [dependencies.sqlx] 75 | features = ["runtime-actix-rustls", "uuid", "postgres", "time", "offline", "sqlite"] 76 | version = "0.5.10" 77 | 78 | [dependencies.tera] 79 | default-features = false 80 | version = "1.15.0" 81 | 82 | [dependencies.tokio] 83 | features = ["fs"] 84 | version = "1.16.1" 85 | 86 | [dependencies.validator] 87 | features = ["derive"] 88 | version = "0.14.0" 89 | 90 | [dev-dependencies] 91 | actix-rt = "2" 92 | 93 | [workspace] 94 | exclude = ["database/migrator"] 95 | members = [".", "database/db-core", "database/db-sqlx-postgres", "database/db-sqlx-sqlite"] 96 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | #FROM node:16.11-bullseye-slim as frontend 2 | #RUN apt-get update && apt-get install -y make 3 | #COPY package.json yarn.lock /src/ 4 | #WORKDIR /src 5 | #RUN yarn install 6 | #COPY . . 7 | #RUN make frontend 8 | 9 | FROM rust:slim as rust 10 | WORKDIR /src 11 | RUN apt-get update && apt-get install -y git pkg-config libssl-dev 12 | #RUN mkdir -p \ 13 | # /src/database/db-core/src \ 14 | # /src/database/db-sqlx-postgres/src \ 15 | # /src/database/db-sqlx-sqlite/src \ 16 | # /src/database/migrator/src \ 17 | # /src/src \ 18 | # /src/assets \ 19 | # /src/static 20 | #RUN touch \ 21 | # /src/src/main.rs \ 22 | # /src/database/db-core/src/lib.rs \ 23 | # /src/database/db-sqlx-postgres/src/lib.rs \ 24 | # /src/database/db-sqlx-sqlite/src/lib.rs \ 25 | # /src/database/migrator/src/main.rs \ 26 | # /src/database/migrator/src/main.rs 27 | #COPY Cargo.toml Cargo.lock /src/ 28 | #COPY ./database/db-core/Cargo.toml /src/database/db-core/ 29 | #COPY ./database/db-sqlx-postgres/Cargo.toml /src/database/db-sqlx-postgres/ 30 | #COPY ./database/db-sqlx-postgres/migrations/ /src/database/db-sqlx-postgres/ 31 | #COPY ./database/db-sqlx-sqlite/Cargo.toml /src/database/db-sqlx-sqlite/ 32 | #COPY ./database/db-sqlx-sqlite/migrations/ /src/database/db-sqlx-sqlite/ 33 | #COPY ./database/migrator/Cargo.toml /src/database/migrator/ 34 | #RUN cargo build --release || true 35 | #COPY database/ /src/ 36 | COPY . /src/ 37 | RUN cargo build --release 38 | 39 | FROM debian:bullseye-slim 40 | RUN useradd -ms /bin/bash -u 1001 gitpad 41 | WORKDIR /home/gitpad 42 | COPY --from=rust /src/target/release/gitpad /usr/local/bin/ 43 | COPY --from=rust /src/config/default.toml /etc/gitpad/config.toml 44 | USER gitpad 45 | LABEL org.opencontainers.image.source https://github.com/realaravinth/gitpad 46 | LABEL org.opencontainers.image.authors "Aravinth Manivannan" 47 | LABEL org.opencontainers.image.license "AGPL-3.0-or-later" 48 | LABEL org.opencontainers.image.title "GitPad" 49 | LABEL org.opencontainers.image.description "Self-hosted GitHub Gists" 50 | CMD [ "/usr/local/bin/gitpad" ] 51 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | STATIC_DIST = ./deploy-static 2 | WEBSITE = website 3 | WEBSITE_DIST = $(WEBSITE)/public 4 | 5 | default: ## Debug build 6 | cargo build 7 | 8 | clean: ## Clean all build artifacts and dependencies 9 | @-/bin/rm -rf target/ 10 | @-/bin/rm -rf database/migrator/target/ 11 | @-/bin/rm -rf database/*/target/ 12 | @-/bin/rm -rf database/*/tmp/ 13 | @-/bin/rm -rf $(WEBSITE) 14 | @-/bin/rm -rf $(STATIC_DIST) 15 | @cargo clean 16 | 17 | coverage: migrate ## Generate coverage report in HTML format 18 | cargo tarpaulin -t 1200 --out Html --skip-clean --all-features --no-fail-fast --workspace=database/db-sqlx-postgres,database/db-sqlx-sqlite,. 19 | 20 | dev-env: ## Download development dependencies 21 | cargo fetch 22 | 23 | doc: ## Prepare documentation 24 | @-/bin/rm -rf $(STATIC_DIST) || true 25 | @cargo doc --no-deps --workspace --all-features 26 | @-mkdir -p $(WEBSITE)/static/doc || true 27 | cp -r target/doc $(WEBSITE)/static/doc 28 | @./scripts/ci.sh build 29 | mkdir -p $(STATIC_DIST) 30 | cp -r $(WEBSITE_DIST)/* $(STATIC_DIST) 31 | 32 | docker: ## Build docker images 33 | docker build -t realaravinth/gitpad:master -t realaravinth/gitpad:latest . 34 | 35 | docker-publish: docker ## Build and publish docker images 36 | docker push realaravinth/gitpad:master 37 | docker push realaravinth/gitpad:latest 38 | 39 | lint: ## Lint codebase 40 | cargo fmt -v --all -- --emit files 41 | cargo clippy --workspace --tests --all-features 42 | 43 | release: ## Release build 44 | cargo build --release 45 | 46 | run: default ## Run debug build 47 | cargo run 48 | 49 | migrate: ## run migrations 50 | @-rm -rf database/db-sqlx-sqlite/tmp && mkdir database/db-sqlx-sqlite/tmp 51 | cd database/migrator && cargo run 52 | 53 | sqlx-offline-data: ## prepare sqlx offline data 54 | cargo sqlx prepare --database-url=${POSTGRES_DATABASE_URL} -- --bin gitpad \ 55 | --all-features 56 | 57 | test: migrate ## Run tests 58 | cd database/db-sqlx-postgres &&\ 59 | DATABASE_URL=${POSTGRES_DATABASE_URL}\ 60 | cargo test --no-fail-fast 61 | cd database/db-sqlx-sqlite &&\ 62 | DATABASE_URL=${SQLITE_DATABASE_URL}\ 63 | cargo test --no-fail-fast 64 | cargo test 65 | 66 | xml-test-coverage: migrate ## Generate cobertura.xml test coverage 67 | cargo tarpaulin -t 1200 --out Xml --skip-clean --all-features --no-fail-fast --workspace=database/db-sqlx-postgres,database/db-sqlx-sqlite,. 68 | 69 | help: ## Prints help for targets with comments 70 | @cat $(MAKEFILE_LIST) | grep -E '^[a-zA-Z_-]+:.*?## .*$$' | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-30s\033[0m %s\n", $$1, $$2}' 71 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 |

GitPad

3 |

4 | 5 | **Self-Hosted GitHub Gists** 6 | 7 |

8 | 9 | [![Docker](https://img.shields.io/docker/pulls/realaravinth/gitpad)](https://hub.docker.com/r/realaravinth/gitpad) 10 | [![Build](https://github.com/realaravinth/gitpad/actions/workflows/linux.yml/badge.svg)](https://github.com/realaravinth/gitpad/actions/workflows/linux.yml) 11 | [![dependency status](https://deps.rs/repo/github/realaravinth/gitpad/status.svg)](https://deps.rs/repo/github/realaravinth/gitpad) 12 | [![codecov](https://codecov.io/gh/realaravinth/gitpad/branch/master/graph/badge.svg)](https://codecov.io/gh/realaravinth/gitpad) 13 | [![Chat](https://img.shields.io/badge/matrix-+gitpad:matrix.batsense.net-purple?style=flat-square)](https://matrix.to/#/#gitpad:matrix.batsense.net) 14 | 15 |
16 | 17 | ## Features 18 | 19 | - [x] Upload code snippets 20 | - [x] Syntax Highlighting 21 | - [x] Comments 22 | - [x] Versioning through Git 23 | - [ ] Fork gists 24 | - [x] Gist privacy: public, unlisted, private 25 | - [ ] Git clone via HTTP and SSH 26 | - [ ] Activity Pub implementation for publishing native gists and commenting 27 | - [ ] Gitea OAuth integration 28 | 29 | ## Why? 30 | 31 | Gists are nice, while there are wonderful forges like 32 | [Gitea](https://gitea.io), there isn't a libre pastebin implementation that 33 | can rival GitHub Gists. 34 | 35 | ## Usage 36 | 37 | 1. All configuration is done through 38 | [./config/default.toml](./config/default.toml)(can be moved to 39 | `/etc/gitpad/config.toml`). 40 | -------------------------------------------------------------------------------- /build.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2021 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | use std::process::Command; 18 | 19 | use cache_buster::{BusterBuilder, NoHashCategory}; 20 | 21 | fn main() { 22 | let output = Command::new("git") 23 | .args(&["rev-parse", "HEAD"]) 24 | .output() 25 | .expect("error in git command, is git installed?"); 26 | let git_hash = String::from_utf8(output.stdout).unwrap(); 27 | println!("cargo:rustc-env=GIT_HASH={}", git_hash); 28 | 29 | cache_bust(); 30 | } 31 | 32 | fn cache_bust() { 33 | // until APPLICATION_WASM gets added to mime crate 34 | // PR: https://github.com/hyperium/mime/pull/138 35 | // let types = vec![ 36 | // mime::IMAGE_PNG, 37 | // mime::IMAGE_SVG, 38 | // mime::IMAGE_JPEG, 39 | // mime::IMAGE_GIF, 40 | // mime::APPLICATION_JAVASCRIPT, 41 | // mime::TEXT_CSS, 42 | // ]; 43 | 44 | println!("cargo:rerun-if-changed=static/cache"); 45 | let no_hash = vec![NoHashCategory::FileExtentions(vec!["wasm"])]; 46 | 47 | let config = BusterBuilder::default() 48 | .source("./static/cache/") 49 | .result("./assets") 50 | .no_hash(no_hash) 51 | .follow_links(true) 52 | .build() 53 | .unwrap(); 54 | 55 | config.process().unwrap(); 56 | } 57 | -------------------------------------------------------------------------------- /config/default.toml: -------------------------------------------------------------------------------- 1 | log = "info" # possible values: "info", "warn", "trace", "error", "debug" 2 | source_code = "https://github.com/realaravinth/gitpad" 3 | allow_registration = true # allow registration on server 4 | allow_demo = true # allow demo on server 5 | admin_email = "admin@gitpad.example.com" 6 | 7 | [server] 8 | # The port at which you want authentication to listen to 9 | # takes a number, choose from 1000-10000 if you dont know what you are doing 10 | port = 7000 11 | #IP address. Enter 0.0.0.0 to listen on all availale addresses 12 | ip= "0.0.0.0" 13 | # enter your hostname, eg: example.com 14 | domain = "localhost" 15 | proxy_has_tls = false 16 | cookie_secret = "k&y8G#J&2gesW&N6hNauy63vgRzq9ZLPb39" 17 | #workers = 2 18 | 19 | [database] 20 | # This section deals with the database location and how to access it 21 | # Please note that at the moment, we have support for only postgresqa. 22 | # Example, if you are Batman, your config would be: 23 | # hostname = "batcave.org" 24 | # port = "5432" 25 | # username = "batman" 26 | # password = "somereallycomplicatedBatmanpassword" 27 | hostname = "localhost" 28 | port = "5432" 29 | username = "postgres" 30 | password = "password" 31 | name = "postgres" 32 | pool = 4 33 | database_type = "postgres" 34 | 35 | 36 | [repository] 37 | root = "/tmp/gitpad.batsense.net" 38 | -------------------------------------------------------------------------------- /database/db-core/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .env 3 | -------------------------------------------------------------------------------- /database/db-core/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "db-core" 3 | version = "0.1.0" 4 | edition = "2021" 5 | homepage = "https://github.com/realaravinth/gitpad" 6 | repository = "https://github.com/realaravinth/gitpad" 7 | documentation = "https://github.con/realaravinth/gitpad" 8 | readme = "https://github.com/realaravinth/gitpad/blob/master/README.md" 9 | license = "AGPLv3 or later version" 10 | authors = ["realaravinth "] 11 | 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | async-trait = "0.1.51" 17 | thiserror = "1.0.30" 18 | serde = { version = "1", features = ["derive"]} 19 | 20 | [features] 21 | default = [] 22 | test = [] 23 | 24 | [dev-dependencies] 25 | serde_json = "1" 26 | -------------------------------------------------------------------------------- /database/db-core/src/errors.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | //! represents all the ways a trait can fail using this crate 18 | use std::error::Error as StdError; 19 | 20 | //use derive_more::{error, Error as DeriveError}; 21 | use thiserror::Error; 22 | 23 | /// Error data structure grouping various error subtypes 24 | #[derive(Debug, Error)] 25 | pub enum DBError { 26 | /// username is already taken 27 | #[error("Username not available")] 28 | DuplicateUsername, 29 | 30 | /// user secret is already taken 31 | #[error("User secret not available")] 32 | DuplicateSecret, 33 | 34 | /// email is already taken 35 | #[error("Email not available")] 36 | DuplicateEmail, 37 | 38 | /// Gist public ID taken 39 | #[error("Gist ID not available")] 40 | GistIDTaken, 41 | 42 | /// Account with specified characteristics not found 43 | #[error("Account with specified characteristics not found")] 44 | AccountNotFound, 45 | 46 | /// errors that are specific to a database implementation 47 | #[error("{0}")] 48 | DBError(#[source] BoxDynError), 49 | 50 | /// email is already taken 51 | #[error("Unknown privacy specifier {}", _0)] 52 | UnknownVisibilitySpecifier(String), 53 | 54 | /// Gist with specified characteristics not found 55 | #[error("Gist with specified characteristics not found")] 56 | GistNotFound, 57 | 58 | /// Comment with specified characteristics not found 59 | #[error("Comment with specified characteristics not found")] 60 | CommentNotFound, 61 | } 62 | 63 | /// Convenience type alias for grouping driver-specific errors 64 | pub type BoxDynError = Box; 65 | 66 | /// Generic result data structure 67 | pub type DBResult = std::result::Result; 68 | -------------------------------------------------------------------------------- /database/db-core/src/ops.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | //! meta operations like migration and connecting to a database 18 | use crate::dev::*; 19 | 20 | /// Database operations trait(migrations, pool creation and fetching connection from pool) 21 | pub trait DBOps: GetConnection + Migrate {} 22 | 23 | /// Get database connection 24 | #[async_trait] 25 | pub trait GetConnection { 26 | /// database connection type 27 | type Conn; 28 | /// database specific error-type 29 | /// get connection from connection pool 30 | async fn get_conn(&self) -> DBResult; 31 | } 32 | 33 | /// Create databse connection 34 | #[async_trait] 35 | pub trait Connect { 36 | /// database specific pool-type 37 | type Pool: GPDatabse; 38 | /// database specific error-type 39 | /// create connection pool 40 | async fn connect(self) -> DBResult; 41 | } 42 | 43 | /// database migrations 44 | #[async_trait] 45 | pub trait Migrate: GPDatabse { 46 | /// database specific error-type 47 | /// run migrations 48 | async fn migrate(&self) -> DBResult<()>; 49 | } 50 | -------------------------------------------------------------------------------- /database/db-sqlx-postgres/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .env 3 | -------------------------------------------------------------------------------- /database/db-sqlx-postgres/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "db-sqlx-postgres" 3 | version = "0.1.0" 4 | edition = "2021" 5 | homepage = "https://github.com/realaravinth/gitpad" 6 | repository = "https://github.com/realaravinth/gitpad" 7 | documentation = "https://github.con/realaravinth/gitpad" 8 | readme = "https://github.com/realaravinth/gitpad/blob/master/README.md" 9 | license = "AGPLv3 or later version" 10 | authors = ["realaravinth "] 11 | include = ["./mgrations/"] 12 | 13 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 14 | 15 | [dependencies] 16 | db-core = {path = "../db-core"} 17 | sqlx = { version = "0.5.10", features = [ "postgres", "time", "offline", "runtime-actix-rustls"] } 18 | async-trait = "0.1.51" 19 | 20 | [dev-dependencies] 21 | actix-rt = "2" 22 | sqlx = { version = "0.5.10", features = [ "runtime-actix-rustls", "postgres", "time", "offline" ] } 23 | db-core = {path = "../db-core", features = ["test"]} 24 | -------------------------------------------------------------------------------- /database/db-sqlx-postgres/migrations/20220213111215_gists_users.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS gists_users ( 2 | username VARCHAR(100) NOT NULL UNIQUE, 3 | email VARCHAR(100) UNIQUE DEFAULT NULL, 4 | email_verified BOOLEAN DEFAULT NULL, 5 | secret varchar(50) NOT NULL UNIQUE, 6 | password TEXT NOT NULL, 7 | ID SERIAL PRIMARY KEY NOT NULL 8 | ); 9 | -------------------------------------------------------------------------------- /database/db-sqlx-postgres/migrations/20220213112330_gists_gists.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS gists_visibility ( 2 | name VARCHAR(15) NOT NULL UNIQUE, 3 | ID SERIAL PRIMARY KEY NOT NULL 4 | ); 5 | 6 | INSERT INTO gists_visibility (name) VALUES('private') ON CONFLICT (name) DO NOTHING; 7 | INSERT INTO gists_visibility (name) VALUES('unlisted') ON CONFLICT (name) DO NOTHING; 8 | INSERT INTO gists_visibility (name) VALUES('public') ON CONFLICT (name) DO NOTHING; 9 | 10 | CREATE TABLE IF NOT EXISTS gists_gists ( 11 | owner_id INTEGER NOT NULL references gists_users(ID) ON DELETE CASCADE, 12 | visibility INTEGER NOT NULL references gists_visibility(ID), 13 | description TEXT DEFAULT NULL, 14 | created timestamptz NOT NULL, 15 | updated timestamptz NOT NULL, 16 | public_id VARCHAR(32) UNIQUE NOT NULL, 17 | ID SERIAL PRIMARY KEY NOT NULL 18 | ); 19 | 20 | CREATE INDEX ON gists_gists(public_id); 21 | 22 | CREATE TABLE IF NOT EXISTS gists_comments ( 23 | owner_id INTEGER NOT NULL references gists_users(ID) ON DELETE CASCADE, 24 | gist_id INTEGER NOT NULL references gists_gists(ID) ON DELETE CASCADE, 25 | comment TEXT DEFAULT NULL, 26 | created timestamptz NOT NULL DEFAULT now(), 27 | ID SERIAL PRIMARY KEY NOT NULL 28 | ); 29 | -------------------------------------------------------------------------------- /database/db-sqlx-postgres/migrations/20220214072325_gists_gists_comments_views.sql: -------------------------------------------------------------------------------- 1 | CREATE OR REPLACE VIEW gists_gists_view AS 2 | SELECT 3 | gists.description, 4 | gists.created, 5 | gists.updated, 6 | gists.public_id, 7 | gists_users.username as owner, 8 | gists_visibility.name as visibility 9 | FROM gists_gists gists 10 | INNER JOIN gists_visibility ON gists_visibility.ID = gists.visibility 11 | INNER JOIN gists_users ON gists_users.ID = gists.owner_id; 12 | 13 | 14 | CREATE OR REPLACE VIEW gists_comments_view AS 15 | SELECT 16 | gists_comments.ID, 17 | gists_comments.comment, 18 | gists_comments.created, 19 | gists_gists.public_id as gist_public_id, 20 | gists_gists.ID as gist_id, 21 | gists_users.username as owner 22 | FROM gists_comments gists_comments 23 | INNER JOIN gists_users ON gists_users.ID = gists_comments.owner_id 24 | INNER JOIN gists_gists ON gists_gists.ID = gists_comments.gist_id; 25 | -------------------------------------------------------------------------------- /database/db-sqlx-postgres/src/errors.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | //! Error-handling utilities 18 | use std::borrow::Cow; 19 | 20 | use db_core::dev::*; 21 | use sqlx::Error; 22 | 23 | /// map postgres errors to [DBError](DBError) types 24 | pub fn map_register_err(e: Error) -> DBError { 25 | if let Error::Database(err) = e { 26 | if err.code() == Some(Cow::from("23505")) { 27 | let msg = err.message(); 28 | if msg.contains("gists_users_username_key") { 29 | DBError::DuplicateUsername 30 | } else if msg.contains("gists_users_email_key") { 31 | DBError::DuplicateEmail 32 | } else if msg.contains("gists_users_secret_key") { 33 | DBError::DuplicateSecret 34 | } else if msg.contains("gists_gists_public_id") { 35 | DBError::GistIDTaken 36 | } else { 37 | DBError::DBError(Box::new(Error::Database(err))) 38 | } 39 | } else { 40 | DBError::DBError(Box::new(Error::Database(err))) 41 | } 42 | } else { 43 | DBError::DBError(Box::new(e)) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /database/db-sqlx-postgres/src/tests.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | use sqlx::postgres::PgPoolOptions; 18 | use std::env; 19 | 20 | use crate::*; 21 | 22 | use db_core::tests::*; 23 | 24 | #[actix_rt::test] 25 | async fn everyting_works() { 26 | const EMAIL: &str = "postgresuser@foo.com"; 27 | const EMAIL2: &str = "postgresuse2r@foo.com"; 28 | const NAME: &str = "postgresuser"; 29 | const NAME2: &str = "postgresuser2"; 30 | const NAME3: &str = "postgresuser3"; 31 | const NAME4: &str = "postgresuser4"; 32 | const NAME5: &str = "postgresuser5"; 33 | const NAME6: &str = "postgresuser6"; 34 | const NAME7: &str = "postgresuser7"; 35 | const PASSWORD: &str = "pasdfasdfasdfadf"; 36 | const SECRET1: &str = "postgressecret1"; 37 | const SECRET2: &str = "postgressecret2"; 38 | const SECRET3: &str = "postgressecret3"; 39 | const SECRET4: &str = "postgressecret4"; 40 | 41 | let url = env::var("POSTGRES_DATABASE_URL").unwrap(); 42 | let pool_options = PgPoolOptions::new().max_connections(2); 43 | let connection_options = ConnectionOptions::Fresh(Fresh { pool_options, url }); 44 | let db = connection_options.connect().await.unwrap(); 45 | 46 | db.migrate().await.unwrap(); 47 | email_register_works(&db, EMAIL, NAME, PASSWORD, SECRET1, NAME5).await; 48 | username_register_works(&db, NAME2, PASSWORD, SECRET2).await; 49 | duplicate_secret_guard_works(&db, NAME3, PASSWORD, NAME4, SECRET3, SECRET2).await; 50 | duplicate_username_and_email(&db, NAME6, NAME7, EMAIL2, PASSWORD, SECRET4, NAME, EMAIL).await; 51 | let creds = Creds { 52 | username: NAME.into(), 53 | password: SECRET4.into(), 54 | }; 55 | db.update_password(&creds).await.unwrap(); 56 | } 57 | 58 | #[actix_rt::test] 59 | async fn visibility_test() { 60 | let url = env::var("POSTGRES_DATABASE_URL").unwrap(); 61 | let pool_options = PgPoolOptions::new().max_connections(2); 62 | let connection_options = ConnectionOptions::Fresh(Fresh { pool_options, url }); 63 | let db = connection_options.connect().await.unwrap(); 64 | 65 | db.migrate().await.unwrap(); 66 | visibility_works(&db).await; 67 | } 68 | 69 | #[actix_rt::test] 70 | async fn gist_test() { 71 | const NAME: &str = "postgisttest"; 72 | const PASSWORD: &str = "pasdfasdfasdfadf"; 73 | const SECRET: &str = "postgisttestsecret"; 74 | const PUBLIC_ID: &str = "postgisttestsecret"; 75 | 76 | let url = env::var("POSTGRES_DATABASE_URL").unwrap(); 77 | let pool_options = PgPoolOptions::new().max_connections(2); 78 | let connection_options = ConnectionOptions::Fresh(Fresh { pool_options, url }); 79 | let db = connection_options.connect().await.unwrap(); 80 | 81 | db.migrate().await.unwrap(); 82 | gists_work(&db, NAME, PASSWORD, SECRET, PUBLIC_ID).await; 83 | } 84 | -------------------------------------------------------------------------------- /database/db-sqlx-sqlite/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .env 3 | -------------------------------------------------------------------------------- /database/db-sqlx-sqlite/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "db-sqlx-sqlite" 3 | version = "0.1.0" 4 | edition = "2021" 5 | homepage = "https://github.com/realaravinth/gitpad" 6 | repository = "https://github.com/realaravinth/gitpad" 7 | documentation = "https://github.con/realaravinth/gitpad" 8 | readme = "https://github.com/realaravinth/gitpad/blob/master/README.md" 9 | license = "AGPLv3 or later version" 10 | authors = ["realaravinth "] 11 | include = ["./mgrations/"] 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [dependencies] 15 | sqlx = { version = "0.5.10", features = [ "sqlite", "time", "offline", "runtime-actix-rustls" ] } 16 | db-core = {path = "../db-core"} 17 | async-trait = "0.1.51" 18 | 19 | [dev-dependencies] 20 | actix-rt = "2" 21 | sqlx = { version = "0.5.10", features = [ "runtime-actix-rustls", "postgres", "time", "offline" ] } 22 | db-core = {path = "../db-core", features = ["test"]} 23 | -------------------------------------------------------------------------------- /database/db-sqlx-sqlite/migrations/20220213111519_gists_users.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS gists_users ( 2 | username VARCHAR(100) NOT NULL UNIQUE, 3 | email VARCHAR(100) UNIQUE DEFAULT NULL, 4 | email_verified BOOLEAN DEFAULT NULL, 5 | secret varchar(50) NOT NULL UNIQUE, 6 | password VARCHAR(150) NOT NULL, 7 | ID INTEGER PRIMARY KEY NOT NULL 8 | ); 9 | -------------------------------------------------------------------------------- /database/db-sqlx-sqlite/migrations/20220213113228_gists_gists.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE IF NOT EXISTS gists_visibility ( 2 | name VARCHAR(15) NOT NULL UNIQUE, 3 | ID INTEGER PRIMARY KEY NOT NULL 4 | ); 5 | 6 | INSERT OR IGNORE INTO gists_visibility (name) VALUES('private'); 7 | INSERT OR IGNORE INTO gists_visibility (name) VALUES('unlisted'); 8 | INSERT OR IGNORE INTO gists_visibility (name) VALUES('public'); 9 | 10 | CREATE TABLE IF NOT EXISTS gists_gists ( 11 | owner_id INTEGER NOT NULL references gists_users(ID) ON DELETE CASCADE, 12 | description TEXT DEFAULT NULL, 13 | created INTEGER NOT NULL, 14 | updated INTEGER NOT NULL, 15 | visibility INTEGER NOT NULL references gists_visibility(ID), 16 | public_id VARCHAR(32) UNIQUE NOT NULL, 17 | ID INTEGER PRIMARY KEY NOT NULL 18 | ); 19 | 20 | 21 | CREATE UNIQUE INDEX IF NOT EXISTS public_id_index ON gists_gists (public_id); 22 | 23 | CREATE TABLE IF NOT EXISTS gists_comments ( 24 | owner_id INTEGER NOT NULL references gists_users(ID) ON DELETE CASCADE, 25 | gist_id INTEGER NOT NULL references gists_gists(ID) ON DELETE CASCADE, 26 | comment TEXT DEFAULT NULL, 27 | created INTEGER NOT NULL, 28 | ID INTEGER PRIMARY KEY NOT NULL 29 | ); 30 | -------------------------------------------------------------------------------- /database/db-sqlx-sqlite/migrations/20220214091158_gists_gists_comments_views.sql: -------------------------------------------------------------------------------- 1 | DROP VIEW IF EXISTS gists_gists_view; 2 | CREATE VIEW gists_gists_view AS 3 | SELECT 4 | gists.description, 5 | gists.created, 6 | gists.updated, 7 | gists.public_id, 8 | gists_users.username as owner, 9 | gists_visibility.name as visibility 10 | FROM gists_gists gists 11 | INNER JOIN gists_visibility ON gists_visibility.ID = gists.visibility 12 | INNER JOIN gists_users ON gists_users.ID = gists.owner_id; 13 | 14 | 15 | DROP VIEW IF EXISTS gists_comments_view; 16 | CREATE VIEW gists_comments_view AS 17 | SELECT 18 | gists_comments.ID, 19 | gists_comments.comment, 20 | gists_comments.created, 21 | gists_gists.public_id as gist_public_id, 22 | gists_gists.ID as gist_id, 23 | gists_users.username as owner 24 | FROM gists_comments gists_comments 25 | INNER JOIN gists_users ON gists_users.ID = gists_comments.owner_id 26 | INNER JOIN gists_gists ON gists_gists.ID = gists_comments.gist_id; 27 | -------------------------------------------------------------------------------- /database/db-sqlx-sqlite/src/errors.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | use std::borrow::Cow; 18 | 19 | use db_core::dev::*; 20 | use sqlx::Error; 21 | 22 | pub fn map_register_err(e: Error) -> DBError { 23 | if let Error::Database(err) = e { 24 | if err.code() == Some(Cow::from("2067")) { 25 | let msg = err.message(); 26 | println!("{}", msg); 27 | if msg.contains("gists_users.username") { 28 | DBError::DuplicateUsername 29 | } else if msg.contains("gists_users.email") { 30 | DBError::DuplicateEmail 31 | } else if msg.contains("gists_users.secret") { 32 | DBError::DuplicateSecret 33 | } else if msg.contains("gists_gists.public_id") { 34 | DBError::GistIDTaken 35 | } else { 36 | DBError::DBError(Box::new(Error::Database(err))) 37 | } 38 | } else { 39 | DBError::DBError(Box::new(Error::Database(err))) 40 | } 41 | } else { 42 | DBError::DBError(Box::new(e)) 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /database/db-sqlx-sqlite/src/tests.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | use sqlx::sqlite::SqlitePoolOptions; 18 | use std::env; 19 | 20 | use crate::*; 21 | 22 | use db_core::tests::*; 23 | 24 | #[actix_rt::test] 25 | async fn everyting_works() { 26 | const EMAIL: &str = "sqliteuser@foo.com"; 27 | const EMAIL2: &str = "sqliteuse2r@foo.com"; 28 | const NAME: &str = "sqliteuser"; 29 | const NAME2: &str = "sqliteuser2"; 30 | const NAME3: &str = "sqliteuser3"; 31 | const NAME4: &str = "sqliteuser4"; 32 | const NAME5: &str = "sqliteuser5"; 33 | const NAME6: &str = "sqliteuser6"; 34 | const NAME7: &str = "sqliteuser7"; 35 | const PASSWORD: &str = "pasdfasdfasdfadf"; 36 | const SECRET1: &str = "sqlitesecret1"; 37 | const SECRET2: &str = "sqlitesecret2"; 38 | const SECRET3: &str = "sqlitesecret3"; 39 | const SECRET4: &str = "sqlitesecret4"; 40 | 41 | let url = env::var("SQLITE_DATABASE_URL").expect("Set SQLITE_DATABASE_URL env var"); 42 | let pool_options = SqlitePoolOptions::new().max_connections(2); 43 | let connection_options = ConnectionOptions::Fresh(Fresh { pool_options, url }); 44 | let db = connection_options.connect().await.unwrap(); 45 | 46 | db.migrate().await.unwrap(); 47 | email_register_works(&db, EMAIL, NAME, PASSWORD, SECRET1, NAME5).await; 48 | username_register_works(&db, NAME2, PASSWORD, SECRET2).await; 49 | duplicate_secret_guard_works(&db, NAME3, PASSWORD, NAME4, SECRET3, SECRET2).await; 50 | duplicate_username_and_email(&db, NAME6, NAME7, EMAIL2, PASSWORD, SECRET4, NAME, EMAIL).await; 51 | let creds = Creds { 52 | username: NAME.into(), 53 | password: SECRET4.into(), 54 | }; 55 | db.update_password(&creds).await.unwrap(); 56 | } 57 | 58 | #[actix_rt::test] 59 | async fn visibility_test() { 60 | let url = env::var("SQLITE_DATABASE_URL").expect("Set SQLITE_DATABASE_URL env var"); 61 | let pool_options = SqlitePoolOptions::new().max_connections(2); 62 | let connection_options = ConnectionOptions::Fresh(Fresh { pool_options, url }); 63 | let db = connection_options.connect().await.unwrap(); 64 | 65 | db.migrate().await.unwrap(); 66 | visibility_works(&db).await; 67 | } 68 | 69 | #[actix_rt::test] 70 | async fn gist_test() { 71 | const NAME: &str = "postgisttest"; 72 | const PASSWORD: &str = "pasdfasdfasdfadf"; 73 | const SECRET: &str = "postgisttestsecret"; 74 | const PUBLIC_ID: &str = "postgisttestsecret"; 75 | 76 | let url = env::var("SQLITE_DATABASE_URL").expect("Set SQLITE_DATABASE_URL env var"); 77 | let pool_options = SqlitePoolOptions::new().max_connections(2); 78 | let connection_options = ConnectionOptions::Fresh(Fresh { pool_options, url }); 79 | let db = connection_options.connect().await.unwrap(); 80 | 81 | db.migrate().await.unwrap(); 82 | gists_work(&db, NAME, PASSWORD, SECRET, PUBLIC_ID).await; 83 | } 84 | -------------------------------------------------------------------------------- /database/migrator/.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | .env 3 | -------------------------------------------------------------------------------- /database/migrator/Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "migrator" 3 | version = "0.1.0" 4 | edition = "2021" 5 | homepage = "https://github.com/realaravinth/gitpad" 6 | repository = "https://github.com/realaravinth/gitpad" 7 | documentation = "https://github.con/realaravinth/gitpad" 8 | readme = "https://github.com/realaravinth/gitpad/blob/master/README.md" 9 | license = "AGPLv3 or later version" 10 | authors = ["realaravinth "] 11 | 12 | # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html 13 | 14 | [workspace] 15 | 16 | [dependencies] 17 | sqlx = { version = "0.5.10", features = [ "runtime-actix-rustls", "sqlite", "postgres", "time", "offline" ] } 18 | actix-rt = "2" 19 | -------------------------------------------------------------------------------- /database/migrator/sqlx-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "db": "SQLite" 3 | } -------------------------------------------------------------------------------- /database/migrator/src/main.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | use std::env; 18 | 19 | use sqlx::migrate::MigrateDatabase; 20 | use sqlx::postgres::PgPoolOptions; 21 | use sqlx::sqlite::SqlitePoolOptions; 22 | use sqlx::Sqlite; 23 | 24 | #[cfg(not(tarpaulin_include))] 25 | #[actix_rt::main] 26 | async fn main() { 27 | //TODO featuregate sqlite and postgres 28 | postgres_migrate().await; 29 | sqlite_migrate().await; 30 | } 31 | 32 | async fn postgres_migrate() { 33 | let db_url = env::var("POSTGRES_DATABASE_URL").expect("set POSTGRES_DATABASE_URL env var"); 34 | let db = PgPoolOptions::new() 35 | .max_connections(2) 36 | .connect(&db_url) 37 | .await 38 | .expect("Unable to form database pool"); 39 | 40 | sqlx::migrate!("../db-sqlx-postgres/migrations/") 41 | .run(&db) 42 | .await 43 | .unwrap(); 44 | } 45 | 46 | async fn sqlite_migrate() { 47 | let db_url = env::var("SQLITE_DATABASE_URL").expect("Set SQLITE_DATABASE_URL env var"); 48 | 49 | if !matches!(Sqlite::database_exists(&db_url).await, Ok(true)) { 50 | Sqlite::create_database(&db_url).await.unwrap(); 51 | } 52 | 53 | let db = SqlitePoolOptions::new() 54 | .max_connections(2) 55 | .connect(&db_url) 56 | .await 57 | .expect("Unable to form database pool"); 58 | 59 | sqlx::migrate!("../db-sqlx-sqlite/migrations/") 60 | .run(&db) 61 | .await 62 | .unwrap(); 63 | } 64 | -------------------------------------------------------------------------------- /docs/ecosystem.md: -------------------------------------------------------------------------------- 1 | # Ecosystem 2 | 3 | ## Overview 4 | 5 | Brief overview of similar pastebin implementations 6 | 7 | ### 1. paste.sr.ht 8 | 9 | paste.sr.ht is a pastebin implementation from the 10 | Sourcehut features with focus on simplicity. Lack of certain features 11 | maybe thought of as a concious decision to keep the program and the user 12 | interface as simple as possible. 13 | 14 | - [x] Free Software 15 | - [x] Syntax highlighting 16 | - [x] Revision history 17 | - [x] Renders history 18 | - [x] Supports multiple files per paste 19 | - [x] API to create and delete files(REST, GraphQL still WIP at the time of 20 | writing this) 21 | - [ ] Commenting on pastes 22 | 23 | ### 2. gists.github.com 24 | 25 | - [ ] Free Software 26 | - [x] Syntax highlighting 27 | - [x] Revision history 28 | - [x] Renders history 29 | - [x] Every paste is a Git repository 30 | - [x] Multiple files per paste but subdirectories are not permitted 31 | - [x] Supports forking 32 | - [x] Commenting on pastes 33 | -------------------------------------------------------------------------------- /scripts/ci.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Used in CI workflow: install Zola binary from GitHub 3 | # Copyright © 2021 Aravinth Manivannan 4 | # 5 | # This program is free software: you can redistribute it and/or modify 6 | # it under the terms of the GNU Affero General Public License as 7 | # published by the Free Software Foundation, either version 3 of the 8 | # License, or (at your option) any later version. 9 | # 10 | # This program is distributed in the hope that it will be useful, 11 | # but WITHOUT ANY WARRANTY; without even the implied warranty of 12 | # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 13 | # GNU Affero General Public License for more details. 14 | # 15 | # You should have received a copy of the GNU Affero General Public License 16 | # along with this program. If not, see . 17 | 18 | set -euo pipefail 19 | 20 | readonly TARBALL=zola.tar.gz 21 | readonly SOURCE="https://github.com/getzola/zola/releases/download/v0.15.3/zola-v0.15.3-x86_64-unknown-linux-gnu.tar.gz" 22 | 23 | readonly BIN_PATH=bin 24 | readonly BIN=$BIN_PATH/zola 25 | 26 | readonly DIST=public 27 | readonly WEBSITE=website 28 | 29 | cd $WEBSITE 30 | 31 | help() { 32 | cat << EOF 33 | ci.sh: CI build script 34 | USAGE: 35 | ci.sh 36 | OPTIONS: 37 | b build build website 38 | c clean clean dependencies and build artifacts 39 | h help print this help menu 40 | i install install build dependencies 41 | u url make urls relative 42 | z zola invoke zola 43 | EOF 44 | } 45 | 46 | check_arg(){ 47 | if [ -z $1 ] 48 | then 49 | help 50 | exit 1 51 | fi 52 | } 53 | 54 | match_arg() { 55 | if [ $1 == $2 ] || [ $1 == $3 ] 56 | then 57 | return 0 58 | else 59 | return 1 60 | fi 61 | } 62 | 63 | download() { 64 | echo "Downloading Zola" 65 | wget --quiet --output-document=$TARBALL $SOURCE 66 | tar -xvzf $TARBALL > /dev/null 67 | rm $TARBALL 68 | echo "Downloaded zola into $BIN" 69 | } 70 | 71 | init() { 72 | if [ ! -d $BIN_PATH ] 73 | then 74 | mkdir $BIN_PATH 75 | fi 76 | 77 | if [ ! -f $BIN ] 78 | then 79 | cd $BIN_PATH 80 | download 81 | fi 82 | } 83 | 84 | run() { 85 | $BIN "${@:1}" 86 | } 87 | 88 | build() { 89 | run build 90 | } 91 | 92 | no_absolute_url() { 93 | sed -i 's/https:\/\/batsense.net//g' $(find public -type f | grep html) 94 | } 95 | 96 | clean() { 97 | rm -rf $BIN_PATH || true 98 | rm -rf $DIST || true 99 | echo "Workspace cleaned" 100 | } 101 | 102 | check_arg $1 103 | 104 | if match_arg $1 'i' 'install' 105 | then 106 | init 107 | elif match_arg $1 'c' 'clean' 108 | then 109 | clean 110 | elif match_arg $1 'b' 'build' 111 | then 112 | build 113 | elif match_arg $1 'h' 'help' 114 | then 115 | help 116 | elif match_arg $1 'u' 'url' 117 | then 118 | no_absolute_url 119 | elif match_arg $1 'z' 'zola' 120 | then 121 | $BIN "${@:3}" 122 | else 123 | echo "Error: $1 is not an option" 124 | help 125 | exit 1 126 | fi 127 | 128 | exit 0 129 | -------------------------------------------------------------------------------- /sqlx-data.json: -------------------------------------------------------------------------------- 1 | { 2 | "db": "PostgreSQL" 3 | } -------------------------------------------------------------------------------- /src/api/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | pub mod v1; 18 | -------------------------------------------------------------------------------- /src/api/v1/account/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2021 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | use actix_identity::Identity; 19 | use actix_web::{web, HttpResponse, Responder}; 20 | use serde::{Deserialize, Serialize}; 21 | 22 | use crate::data::api::v1::account::*; 23 | use crate::data::api::v1::auth::Password; 24 | use crate::errors::*; 25 | use crate::AppData; 26 | 27 | #[cfg(test)] 28 | pub mod test; 29 | 30 | pub use super::auth; 31 | 32 | #[derive(Clone, Debug, Deserialize, Serialize)] 33 | pub struct AccountCheckPayload { 34 | pub val: String, 35 | } 36 | 37 | pub fn services(cfg: &mut actix_web::web::ServiceConfig) { 38 | cfg.service(username_exists); 39 | cfg.service(set_username); 40 | cfg.service(email_exists); 41 | cfg.service(set_email); 42 | cfg.service(delete_account); 43 | cfg.service(update_user_password); 44 | cfg.service(get_secret); 45 | cfg.service(update_user_secret); 46 | } 47 | 48 | #[derive(Clone, Debug, Deserialize, Serialize)] 49 | pub struct Email { 50 | pub email: String, 51 | } 52 | 53 | #[derive(Clone, Debug, Deserialize, Serialize)] 54 | pub struct Username { 55 | pub username: String, 56 | } 57 | 58 | /// update username 59 | #[my_codegen::post( 60 | path = "crate::V1_API_ROUTES.account.update_username", 61 | wrap = "super::get_auth_middleware()" 62 | )] 63 | async fn set_username( 64 | id: Identity, 65 | payload: web::Json, 66 | data: AppData, 67 | db: crate::DB, 68 | ) -> ServiceResult { 69 | let username = id.identity().unwrap(); 70 | 71 | let new_name = data 72 | .update_username(&(**db), &username, &payload.username) 73 | .await?; 74 | 75 | id.forget(); 76 | id.remember(new_name); 77 | 78 | Ok(HttpResponse::Ok()) 79 | } 80 | 81 | #[my_codegen::post(path = "crate::V1_API_ROUTES.account.username_exists")] 82 | async fn username_exists( 83 | payload: web::Json, 84 | data: AppData, 85 | db: crate::DB, 86 | ) -> ServiceResult { 87 | Ok(HttpResponse::Ok().json(data.username_exists(&(**db), &payload.val).await?)) 88 | } 89 | 90 | #[my_codegen::post(path = "crate::V1_API_ROUTES.account.email_exists")] 91 | pub async fn email_exists( 92 | payload: web::Json, 93 | data: AppData, 94 | db: crate::DB, 95 | ) -> ServiceResult { 96 | Ok(HttpResponse::Ok().json(data.email_exists(&(**db), &payload.val).await?)) 97 | } 98 | 99 | /// update email 100 | #[my_codegen::post( 101 | path = "crate::V1_API_ROUTES.account.update_email", 102 | wrap = "super::get_auth_middleware()" 103 | )] 104 | async fn set_email( 105 | id: Identity, 106 | payload: web::Json, 107 | data: AppData, 108 | db: crate::DB, 109 | ) -> ServiceResult { 110 | let username = id.identity().unwrap(); 111 | data.set_email(&(**db), &username, &payload.email).await?; 112 | Ok(HttpResponse::Ok()) 113 | } 114 | 115 | #[my_codegen::post( 116 | path = "crate::V1_API_ROUTES.account.delete", 117 | wrap = "super::get_auth_middleware()" 118 | )] 119 | async fn delete_account( 120 | id: Identity, 121 | payload: web::Json, 122 | data: AppData, 123 | db: crate::DB, 124 | ) -> ServiceResult { 125 | let username = id.identity().unwrap(); 126 | 127 | data.delete_user(&(**db), &username, &payload.password) 128 | .await?; 129 | id.forget(); 130 | Ok(HttpResponse::Ok()) 131 | } 132 | 133 | #[my_codegen::post( 134 | path = "crate::V1_API_ROUTES.account.update_password", 135 | wrap = "super::get_auth_middleware()" 136 | )] 137 | async fn update_user_password( 138 | id: Identity, 139 | data: AppData, 140 | db: crate::DB, 141 | payload: web::Json, 142 | ) -> ServiceResult { 143 | let username = id.identity().unwrap(); 144 | let payload = payload.into_inner(); 145 | data.change_password(&(**db), &username, &payload).await?; 146 | 147 | Ok(HttpResponse::Ok()) 148 | } 149 | 150 | #[my_codegen::get( 151 | path = "crate::V1_API_ROUTES.account.get_secret", 152 | wrap = "super::get_auth_middleware()" 153 | )] 154 | async fn get_secret(id: Identity, data: AppData, db: crate::DB) -> ServiceResult { 155 | let username = id.identity().unwrap(); 156 | let secret = data.get_secret(&(**db), &username).await?; 157 | Ok(HttpResponse::Ok().json(secret)) 158 | } 159 | 160 | #[my_codegen::post( 161 | path = "crate::V1_API_ROUTES.account.update_secret", 162 | wrap = "super::get_auth_middleware()" 163 | )] 164 | async fn update_user_secret( 165 | id: Identity, 166 | data: AppData, 167 | db: crate::DB, 168 | ) -> ServiceResult { 169 | let username = id.identity().unwrap(); 170 | let _secret = data.update_user_secret(&(**db), &username).await?; 171 | 172 | Ok(HttpResponse::Ok()) 173 | } 174 | -------------------------------------------------------------------------------- /src/api/v1/auth.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2021 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | use crate::data::api::v1::auth::{Login, Register}; 19 | use actix_identity::Identity; 20 | use actix_web::http::header; 21 | use actix_web::{web, HttpResponse, Responder}; 22 | 23 | use super::RedirectQuery; 24 | use crate::errors::*; 25 | use crate::AppData; 26 | 27 | pub fn services(cfg: &mut web::ServiceConfig) { 28 | cfg.service(register); 29 | cfg.service(login); 30 | cfg.service(signout); 31 | } 32 | #[my_codegen::post(path = "crate::V1_API_ROUTES.auth.register")] 33 | async fn register( 34 | payload: web::Json, 35 | data: AppData, 36 | db: crate::DB, 37 | ) -> ServiceResult { 38 | data.register(&(**db), &payload).await?; 39 | Ok(HttpResponse::Ok()) 40 | } 41 | 42 | #[my_codegen::post(path = "crate::V1_API_ROUTES.auth.login")] 43 | async fn login( 44 | id: Identity, 45 | payload: web::Json, 46 | query: web::Query, 47 | data: AppData, 48 | db: crate::DB, 49 | ) -> ServiceResult { 50 | let payload = payload.into_inner(); 51 | let username = data.login(&(**db), &payload).await?; 52 | id.remember(username); 53 | let query = query.into_inner(); 54 | if let Some(redirect_to) = query.redirect_to { 55 | Ok(HttpResponse::Found() 56 | .insert_header((header::LOCATION, redirect_to)) 57 | .finish()) 58 | } else { 59 | Ok(HttpResponse::Ok().into()) 60 | } 61 | } 62 | 63 | #[my_codegen::get( 64 | path = "crate::V1_API_ROUTES.auth.logout", 65 | wrap = "super::get_auth_middleware()" 66 | )] 67 | async fn signout(id: Identity) -> impl Responder { 68 | use actix_auth_middleware::GetLoginRoute; 69 | 70 | if id.identity().is_some() { 71 | id.forget(); 72 | } 73 | HttpResponse::Found() 74 | .append_header((header::LOCATION, crate::V1_API_ROUTES.get_login_route(None))) 75 | .finish() 76 | } 77 | -------------------------------------------------------------------------------- /src/api/v1/meta.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | use actix_web::{web, HttpResponse, Responder}; 18 | use serde::{Deserialize, Serialize}; 19 | 20 | use crate::{DB, GIT_COMMIT_HASH, VERSION}; 21 | 22 | #[derive(Clone, Debug, Deserialize, Serialize)] 23 | pub struct BuildDetails { 24 | pub version: &'static str, 25 | pub git_commit_hash: &'static str, 26 | } 27 | 28 | pub mod routes { 29 | pub struct Meta { 30 | pub build_details: &'static str, 31 | pub health: &'static str, 32 | } 33 | 34 | impl Meta { 35 | pub const fn new() -> Self { 36 | Self { 37 | build_details: "/api/v1/meta/build", 38 | health: "/api/v1/meta/health", 39 | } 40 | } 41 | } 42 | } 43 | 44 | /// emmits build details of the bninary 45 | #[my_codegen::get(path = "crate::V1_API_ROUTES.meta.build_details")] 46 | async fn build_details() -> impl Responder { 47 | let build = BuildDetails { 48 | version: VERSION, 49 | git_commit_hash: GIT_COMMIT_HASH, 50 | }; 51 | HttpResponse::Ok().json(build) 52 | } 53 | 54 | #[derive(Clone, Debug, Deserialize, Serialize)] 55 | /// Health check return datatype 56 | pub struct Health { 57 | db: bool, 58 | } 59 | 60 | /// emmits build details of the bninary 61 | #[my_codegen::get(path = "crate::V1_API_ROUTES.meta.health")] 62 | async fn health(db: DB) -> impl Responder { 63 | let health = Health { 64 | db: db.ping().await, 65 | }; 66 | HttpResponse::Ok().json(health) 67 | } 68 | 69 | pub fn services(cfg: &mut web::ServiceConfig) { 70 | cfg.service(health); 71 | cfg.service(build_details); 72 | } 73 | 74 | #[cfg(test)] 75 | mod tests { 76 | use actix_web::{http::StatusCode, test, App}; 77 | 78 | use crate::api::v1::meta::Health; 79 | use crate::routes::services; 80 | use crate::tests::*; 81 | use crate::*; 82 | 83 | #[actix_rt::test] 84 | async fn build_details_works() { 85 | let app = test::init_service(App::new().configure(services)).await; 86 | 87 | let resp = test::call_service( 88 | &app, 89 | test::TestRequest::get() 90 | .uri(V1_API_ROUTES.meta.build_details) 91 | .to_request(), 92 | ) 93 | .await; 94 | assert_eq!(resp.status(), StatusCode::OK); 95 | } 96 | 97 | #[actix_rt::test] 98 | async fn health_works() { 99 | let config = [ 100 | sqlx_postgres::get_data().await, 101 | sqlx_sqlite::get_data().await, 102 | ]; 103 | 104 | for (db, data) in config.iter() { 105 | let app = get_app!(data, db).await; 106 | let resp = get_request!(&app, V1_API_ROUTES.meta.health); 107 | assert_eq!(resp.status(), StatusCode::OK); 108 | 109 | let health: Health = test::read_body_json(resp).await; 110 | assert!(health.db); 111 | } 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/api/v1/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | use actix_auth_middleware::Authentication; 18 | use actix_web::web::ServiceConfig; 19 | use serde::Deserialize; 20 | 21 | pub mod account; 22 | pub mod auth; 23 | pub mod gists; 24 | pub mod meta; 25 | pub mod routes; 26 | 27 | pub use routes::ROUTES; 28 | 29 | pub fn services(cfg: &mut ServiceConfig) { 30 | auth::services(cfg); 31 | account::services(cfg); 32 | meta::services(cfg); 33 | gists::services(cfg); 34 | } 35 | 36 | #[derive(Deserialize)] 37 | pub struct RedirectQuery { 38 | pub redirect_to: Option, 39 | } 40 | 41 | pub fn get_auth_middleware() -> Authentication { 42 | Authentication::with_identity(ROUTES) 43 | } 44 | 45 | #[cfg(test)] 46 | mod tests; 47 | -------------------------------------------------------------------------------- /src/api/v1/tests/auth.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2021 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | use std::sync::Arc; 18 | 19 | use actix_auth_middleware::GetLoginRoute; 20 | use actix_web::http::{header, StatusCode}; 21 | use actix_web::test; 22 | 23 | use crate::api::v1::ROUTES; 24 | use crate::data::api::v1::auth::{Login, Register}; 25 | use crate::data::Data; 26 | use crate::db::BoxDB; 27 | use crate::errors::*; 28 | use crate::*; 29 | 30 | use crate::tests::*; 31 | 32 | #[actix_rt::test] 33 | async fn postgrest_auth_works() { 34 | let (db, data) = sqlx_postgres::get_data().await; 35 | auth_works(data.clone(), db.clone()).await; 36 | serverside_password_validation_works(data, db).await; 37 | } 38 | 39 | #[actix_rt::test] 40 | async fn sqlite_auth_works() { 41 | let (db, data) = sqlx_sqlite::get_data().await; 42 | auth_works(data.clone(), db.clone()).await; 43 | serverside_password_validation_works(data, db).await; 44 | } 45 | 46 | async fn auth_works(data: Arc, db: BoxDB) { 47 | const NAME: &str = "testuserfoo"; 48 | const PASSWORD: &str = "longpassword"; 49 | const EMAIL: &str = "testuser1foo@a.com"; 50 | 51 | let _ = data.delete_user(&db, NAME, PASSWORD).await; 52 | let app = get_app!(data, db).await; 53 | 54 | // 1. Register with email == None 55 | let msg = Register { 56 | username: NAME.into(), 57 | password: PASSWORD.into(), 58 | confirm_password: PASSWORD.into(), 59 | email: None, 60 | }; 61 | let resp = 62 | test::call_service(&app, post_request!(&msg, ROUTES.auth.register).to_request()).await; 63 | assert_eq!(resp.status(), StatusCode::OK); 64 | // delete user TODO 65 | let _ = data.delete_user(&db, NAME, PASSWORD).await; 66 | 67 | // 1. Register and signin 68 | let (_, signin_resp) = data.register_and_signin(&db, NAME, EMAIL, PASSWORD).await; 69 | let cookies = get_cookie!(signin_resp); 70 | 71 | // Sign in with email 72 | data.signin_test(&db, EMAIL, PASSWORD).await; 73 | 74 | // 2. check if duplicate username is allowed 75 | let mut msg = Register { 76 | username: NAME.into(), 77 | password: PASSWORD.into(), 78 | confirm_password: PASSWORD.into(), 79 | email: Some(EMAIL.into()), 80 | }; 81 | 82 | msg.username = format!("asdfasd{}", msg.username); 83 | data.bad_post_req_test( 84 | &db, 85 | NAME, 86 | PASSWORD, 87 | ROUTES.auth.register, 88 | &msg, 89 | ServiceError::EmailTaken, 90 | ) 91 | .await; 92 | 93 | msg.email = Some(format!("asdfasd{}", msg.email.unwrap())); 94 | msg.username = NAME.into(); 95 | data.bad_post_req_test( 96 | &db, 97 | NAME, 98 | PASSWORD, 99 | ROUTES.auth.register, 100 | &msg, 101 | ServiceError::UsernameTaken, 102 | ) 103 | .await; 104 | 105 | // 3. sigining in with non-existent user 106 | let mut creds = Login { 107 | login: "nonexistantuser".into(), 108 | password: msg.password.clone(), 109 | }; 110 | data.bad_post_req_test( 111 | &db, 112 | NAME, 113 | PASSWORD, 114 | ROUTES.auth.login, 115 | &creds, 116 | ServiceError::AccountNotFound, 117 | ) 118 | .await; 119 | 120 | creds.login = "nonexistantuser@example.com".into(); 121 | data.bad_post_req_test( 122 | &db, 123 | NAME, 124 | PASSWORD, 125 | ROUTES.auth.login, 126 | &creds, 127 | ServiceError::AccountNotFound, 128 | ) 129 | .await; 130 | 131 | // 4. trying to signin with wrong password 132 | creds.login = NAME.into(); 133 | creds.password = NAME.into(); 134 | 135 | data.bad_post_req_test( 136 | &db, 137 | NAME, 138 | PASSWORD, 139 | ROUTES.auth.login, 140 | &creds, 141 | ServiceError::WrongPassword, 142 | ) 143 | .await; 144 | 145 | // 5. signout 146 | let signout_resp = test::call_service( 147 | &app, 148 | test::TestRequest::get() 149 | .uri(ROUTES.auth.logout) 150 | .cookie(cookies) 151 | .to_request(), 152 | ) 153 | .await; 154 | assert_eq!(signout_resp.status(), StatusCode::FOUND); 155 | let headers = signout_resp.headers(); 156 | assert_eq!( 157 | headers.get(header::LOCATION).unwrap(), 158 | &crate::V1_API_ROUTES.get_login_route(None) 159 | ); 160 | 161 | let creds = Login { 162 | login: NAME.into(), 163 | password: PASSWORD.into(), 164 | }; 165 | 166 | //6. sigin with redirect URL set 167 | let redirect_to = ROUTES.auth.logout; 168 | let resp = test::call_service( 169 | &app, 170 | post_request!(&creds, &ROUTES.get_login_route(Some(redirect_to))).to_request(), 171 | ) 172 | .await; 173 | assert_eq!(resp.status(), StatusCode::FOUND); 174 | let headers = resp.headers(); 175 | assert_eq!(headers.get(header::LOCATION).unwrap(), &redirect_to); 176 | } 177 | 178 | async fn serverside_password_validation_works(data: Arc, db: BoxDB) { 179 | const NAME: &str = "testuser542"; 180 | const PASSWORD: &str = "longpassword2"; 181 | 182 | let db = &db; 183 | let _ = data.delete_user(db, NAME, PASSWORD).await; 184 | 185 | let app = get_app!(data, db).await; 186 | 187 | // checking to see if server-side password validation (password == password_config) 188 | // works 189 | let register_msg = Register { 190 | username: NAME.into(), 191 | password: PASSWORD.into(), 192 | confirm_password: NAME.into(), 193 | email: None, 194 | }; 195 | let resp = test::call_service( 196 | &app, 197 | post_request!(®ister_msg, ROUTES.auth.register).to_request(), 198 | ) 199 | .await; 200 | assert_eq!(resp.status(), StatusCode::BAD_REQUEST); 201 | let txt: ErrorToResponse = test::read_body_json(resp).await; 202 | assert_eq!(txt.error, format!("{}", ServiceError::PasswordsDontMatch)); 203 | } 204 | -------------------------------------------------------------------------------- /src/api/v1/tests/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2021 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | mod auth; 18 | mod protected; 19 | -------------------------------------------------------------------------------- /src/api/v1/tests/protected.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2021 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | use actix_web::http::StatusCode; 19 | use actix_web::test; 20 | 21 | use crate::data::Data; 22 | use crate::pages::PAGES; 23 | use crate::*; 24 | 25 | use crate::tests::*; 26 | 27 | #[actix_rt::test] 28 | async fn postgrest_protected_routes_work() { 29 | let (db, data) = sqlx_postgres::get_data().await; 30 | protected_routes_work(data.clone(), db.clone()).await; 31 | } 32 | 33 | #[actix_rt::test] 34 | async fn sqlite_protected_routes_work() { 35 | let (db, data) = sqlx_sqlite::get_data().await; 36 | protected_routes_work(data.clone(), db.clone()).await; 37 | } 38 | 39 | async fn protected_routes_work(data: Arc, db: BoxDB) { 40 | const NAME: &str = "testuser619"; 41 | const PASSWORD: &str = "longpassword2"; 42 | const EMAIL: &str = "testuser119@a.com2"; 43 | let db = &db; 44 | 45 | let _post_protected_urls = [ 46 | "/api/v1/account/secret/", 47 | "/api/v1/account/email/", 48 | "/api/v1/account/delete", 49 | ]; 50 | 51 | let get_protected_urls = [ 52 | V1_API_ROUTES.auth.logout, 53 | PAGES.auth.logout, 54 | PAGES.home, 55 | PAGES.gist.new, 56 | ]; 57 | 58 | let _ = data.delete_user(db, NAME, PASSWORD).await; 59 | 60 | let (_, signin_resp) = data.register_and_signin(db, NAME, EMAIL, PASSWORD).await; 61 | let cookies = get_cookie!(signin_resp); 62 | let app = get_app!(data, db).await; 63 | 64 | for url in get_protected_urls.iter() { 65 | let resp = get_request!(&app, url); 66 | assert_eq!(resp.status(), StatusCode::FOUND); 67 | 68 | let authenticated_resp = get_request!(&app, url, cookies.clone()); 69 | 70 | println!("{url}"); 71 | if url == &V1_API_ROUTES.auth.logout || url == &PAGES.auth.logout { 72 | assert_eq!(authenticated_resp.status(), StatusCode::FOUND); 73 | } else { 74 | assert_eq!(authenticated_resp.status(), StatusCode::OK); 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/data/api/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | pub mod v1; 18 | -------------------------------------------------------------------------------- /src/data/api/v1/account.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | //! Account management utility datastructures and methods 18 | use db_core::prelude::*; 19 | use serde::{Deserialize, Serialize}; 20 | use tokio::fs; 21 | 22 | pub use super::auth; 23 | use super::get_random; 24 | use super::gists::GistID; 25 | use crate::db::BoxDB; 26 | use crate::errors::*; 27 | use crate::Data; 28 | 29 | #[derive(Clone, Debug, Deserialize, Serialize)] 30 | /// Data structure used in `*_exists` methods 31 | pub struct AccountCheckResp { 32 | /// set to true if the attribute in question exists 33 | pub exists: bool, 34 | } 35 | 36 | /// Data structure used to change password of a registered user 37 | #[derive(Clone, Debug, Deserialize, Serialize)] 38 | pub struct ChangePasswordReqest { 39 | /// current password 40 | pub password: String, 41 | /// new password 42 | pub new_password: String, 43 | /// new password confirmation 44 | pub confirm_new_password: String, 45 | } 46 | 47 | /// Data structure used to represent account secret 48 | #[derive(Clone, Debug, Deserialize, Serialize)] 49 | pub struct Secret { 50 | /// account secret 51 | pub secret: String, 52 | } 53 | 54 | impl Data { 55 | /// check if email exists on database 56 | pub async fn email_exists(&self, db: &BoxDB, email: &str) -> ServiceResult { 57 | let resp = AccountCheckResp { 58 | exists: db.email_exists(email).await?, 59 | }; 60 | 61 | Ok(resp) 62 | } 63 | 64 | /// update email 65 | pub async fn set_email(&self, db: &BoxDB, username: &str, email: &str) -> ServiceResult<()> { 66 | self.creds.email(email)?; 67 | 68 | let payload = UpdateEmailPayload { username, email }; 69 | db.update_email(&payload).await?; 70 | Ok(()) 71 | } 72 | 73 | /// check if email exists in database 74 | pub async fn username_exists( 75 | &self, 76 | db: &BoxDB, 77 | username: &str, 78 | ) -> ServiceResult { 79 | let resp = AccountCheckResp { 80 | exists: db.username_exists(username).await?, 81 | }; 82 | Ok(resp) 83 | } 84 | 85 | /// update username of a registered user 86 | pub async fn update_username( 87 | &self, 88 | db: &BoxDB, 89 | current_username: &str, 90 | new_username: &str, 91 | ) -> ServiceResult { 92 | let processed_uname = self.creds.username(new_username)?; 93 | 94 | let db_payload = UpdateUsernamePayload { 95 | old_username: current_username, 96 | new_username: &processed_uname, 97 | }; 98 | 99 | db.update_username(&db_payload).await?; 100 | Ok(processed_uname) 101 | } 102 | 103 | /// get account secret of a registered user 104 | pub async fn get_secret(&self, db: &BoxDB, username: &str) -> ServiceResult { 105 | let secret = Secret { 106 | secret: db.get_secret(username).await?, 107 | }; 108 | 109 | Ok(secret) 110 | } 111 | 112 | /// update account secret of a registered user 113 | pub async fn update_user_secret(&self, db: &BoxDB, username: &str) -> ServiceResult { 114 | let mut secret; 115 | loop { 116 | secret = get_random(32); 117 | 118 | match db.update_secret(username, &secret).await { 119 | Ok(_) => break, 120 | Err(DBError::DuplicateSecret) => continue, 121 | Err(e) => return Err(e.into()), 122 | } 123 | } 124 | 125 | Ok(secret) 126 | } 127 | 128 | // returns Ok(()) upon successful authentication 129 | async fn authenticate(&self, db: &BoxDB, username: &str, password: &str) -> ServiceResult<()> { 130 | use argon2_creds::Config; 131 | let resp = db.username_login(username).await?; 132 | if Config::verify(&resp.password, password)? { 133 | Ok(()) 134 | } else { 135 | Err(ServiceError::WrongPassword) 136 | } 137 | } 138 | 139 | /// delete user 140 | pub async fn delete_user( 141 | &self, 142 | db: &BoxDB, 143 | username: &str, 144 | password: &str, 145 | ) -> ServiceResult<()> { 146 | self.authenticate(db, username, password).await?; 147 | let gists = db.get_user_gists(username).await?; 148 | for gist in gists.iter() { 149 | let path = self.get_gist_id_from_repo_path(&GistID::ID(&gist.public_id)); 150 | fs::remove_dir_all(&path).await?; 151 | } 152 | db.delete_account(username).await?; 153 | Ok(()) 154 | } 155 | 156 | /// change password 157 | pub async fn change_password( 158 | &self, 159 | db: &BoxDB, 160 | username: &str, 161 | payload: &ChangePasswordReqest, 162 | ) -> ServiceResult<()> { 163 | if payload.new_password != payload.confirm_new_password { 164 | return Err(ServiceError::PasswordsDontMatch); 165 | } 166 | 167 | self.authenticate(db, username, &payload.password).await?; 168 | 169 | let new_hash = self.creds.password(&payload.new_password)?; 170 | 171 | let db_payload = Creds { 172 | username: username.into(), 173 | password: new_hash, 174 | }; 175 | 176 | db.update_password(&db_payload).await?; 177 | 178 | Ok(()) 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/data/api/v1/auth.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | //! Authentication helper methods and data structures 18 | use db_core::prelude::*; 19 | use serde::{Deserialize, Serialize}; 20 | 21 | use super::get_random; 22 | use crate::errors::*; 23 | use crate::Data; 24 | 25 | /// Register payload 26 | #[derive(Clone, Debug, Deserialize, Serialize)] 27 | pub struct Register { 28 | /// username 29 | pub username: String, 30 | /// password 31 | pub password: String, 32 | /// password confirmation: `password` and `confirm_password` must match 33 | pub confirm_password: String, 34 | /// optional email 35 | pub email: Option, 36 | } 37 | 38 | /// Login payload 39 | #[derive(Clone, Debug, Deserialize, Serialize)] 40 | pub struct Login { 41 | // login accepts both username and email under "username field" 42 | // TODO update all instances where login is used 43 | /// user identifier: either username or email 44 | /// an email is detected by checkinf for the existence of `@` character 45 | pub login: String, 46 | /// password 47 | pub password: String, 48 | } 49 | 50 | #[derive(Clone, Debug, Deserialize, Serialize)] 51 | /// struct used to represent password 52 | pub struct Password { 53 | /// password 54 | pub password: String, 55 | } 56 | 57 | impl Data { 58 | /// Log in method. Returns `Ok(())` when user is authenticated and errors when authentication 59 | /// fails 60 | pub async fn login(&self, db: &T, payload: &Login) -> ServiceResult { 61 | use argon2_creds::Config; 62 | 63 | let verify = |stored: &str, received: &str| { 64 | if Config::verify(stored, received)? { 65 | Ok(()) 66 | } else { 67 | Err(ServiceError::WrongPassword) 68 | } 69 | }; 70 | 71 | if payload.login.contains('@') { 72 | let creds = db.email_login(&payload.login).await?; 73 | verify(&creds.password, &payload.password)?; 74 | Ok(creds.username) 75 | } else { 76 | let password = db.username_login(&payload.login).await?; 77 | verify(&password.password, &payload.password)?; 78 | Ok(payload.login.clone()) 79 | } 80 | } 81 | 82 | /// register new user 83 | pub async fn register(&self, db: &T, payload: &Register) -> ServiceResult<()> { 84 | if !self.settings.allow_registration { 85 | return Err(ServiceError::ClosedForRegistration); 86 | } 87 | 88 | if payload.password != payload.confirm_password { 89 | return Err(ServiceError::PasswordsDontMatch); 90 | } 91 | let username = self.creds.username(&payload.username)?; 92 | let hash = self.creds.password(&payload.password)?; 93 | 94 | if let Some(email) = &payload.email { 95 | self.creds.email(email)?; 96 | } 97 | 98 | let mut secret; 99 | 100 | if let Some(email) = &payload.email { 101 | loop { 102 | secret = get_random(32); 103 | 104 | let db_payload = EmailRegisterPayload { 105 | secret: &secret, 106 | username: &username, 107 | password: &hash, 108 | email, 109 | }; 110 | 111 | match db.email_register(&db_payload).await { 112 | Ok(_) => break, 113 | Err(DBError::DuplicateSecret) => continue, 114 | Err(e) => return Err(e.into()), 115 | } 116 | } 117 | } else { 118 | loop { 119 | secret = get_random(32); 120 | 121 | let db_payload = UsernameRegisterPayload { 122 | secret: &secret, 123 | username: &username, 124 | password: &hash, 125 | }; 126 | 127 | match db.username_register(&db_payload).await { 128 | Ok(_) => break, 129 | Err(DBError::DuplicateSecret) => continue, 130 | Err(e) => return Err(e.into()), 131 | } 132 | } 133 | } 134 | Ok(()) 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /src/data/api/v1/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | pub mod account; 18 | pub mod auth; 19 | pub mod gists; 20 | pub mod render_html; 21 | 22 | pub(crate) use crate::utils::get_random; 23 | -------------------------------------------------------------------------------- /src/data/api/v1/render_html.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | use std::path::Path; 18 | 19 | use pulldown_cmark::{html, Options, Parser}; 20 | use syntect::highlighting::{Color, ThemeSet}; 21 | use syntect::html::highlighted_html_for_string; 22 | use syntect::parsing::{SyntaxReference, SyntaxSet}; 23 | 24 | use crate::errors::*; 25 | 26 | pub trait GenerateHTML { 27 | fn generate(&mut self); 28 | } 29 | 30 | #[allow(dead_code)] 31 | pub const STYLE: &str = " 32 | "; 33 | 34 | thread_local! { 35 | pub(crate) static SYNTAX_SET: SyntaxSet = SyntaxSet::load_defaults_newlines(); 36 | } 37 | 38 | pub struct SourcegraphQuery<'a> { 39 | pub filepath: &'a str, 40 | pub code: &'a str, 41 | } 42 | 43 | pub fn render_markdown(content: &str) -> String { 44 | // Set up options and parser. Strikethroughs are not part of the CommonMark standard 45 | // and we therefore must enable it explicitly. 46 | let options = Options::all(); 47 | // options.insert(Options::ENABLE_STRIKETHROUGH); 48 | let parser = Parser::new_ext(content, options); 49 | 50 | // Write to String buffer. 51 | let mut html_output = String::with_capacity(content.len()); 52 | html::push_html(&mut html_output, parser); 53 | html_output 54 | } 55 | 56 | impl<'a> SourcegraphQuery<'a> { 57 | pub fn render_markdown(&self) -> String { 58 | render_markdown(self.code) 59 | } 60 | 61 | pub fn syntax_highlight(&self) -> String { 62 | // let ss = SYNTAX_SET; 63 | let ts = ThemeSet::load_defaults(); 64 | 65 | let theme = &ts.themes["InspiredGitHub"]; 66 | let c = theme.settings.background.unwrap_or(Color::WHITE); 67 | let mut num = 1; 68 | let mut output = format!( 69 | "", 73 | c.r, c.g, c.b 74 | ); 75 | 76 | // highlighted_html_for_string(&q.code, syntax_set, syntax_def, theme), 77 | let html = SYNTAX_SET.with(|ss| { 78 | let language = self.determine_language(ss).unwrap(); 79 | highlighted_html_for_string(self.code, ss, language, theme) 80 | }); 81 | let total_lines = html.lines().count(); 82 | for (line_num, line) in html.lines().enumerate() { 83 | if !line.trim().is_empty() { 84 | if line_num == 0 || line_num == total_lines - 1 { 85 | output.push_str(line); 86 | } else { 87 | output.push_str(&format!("" 88 | )); 89 | num += 1; 90 | } 91 | } 92 | } 93 | output 94 | } 95 | 96 | // adopted from 97 | // https://github.com/sourcegraph/sourcegraph/blob/9fe138ae75fd64dce06b621572b252a9c9c8da70/docker-images/syntax-highlighter/crates/sg-syntax/src/lib.rs#L81 98 | // with minimum modifications. Crate was MIT licensed at the time(2022-03-12 11:11) 99 | fn determine_language<'b>( 100 | &self, 101 | syntax_set: &'b SyntaxSet, 102 | ) -> ServiceResult<&'b SyntaxReference> { 103 | if self.filepath.is_empty() { 104 | // Legacy codepath, kept for backwards-compatability with old clients. 105 | match syntax_set.find_syntax_by_first_line(self.code) { 106 | Some(v) => { 107 | return Ok(v); 108 | } 109 | None => unimplemented!(), //Err(json!({"error": "invalid extension"})), 110 | }; 111 | } 112 | 113 | // Split the input path ("foo/myfile.go") into file name 114 | // ("myfile.go") and extension ("go"). 115 | let path = Path::new(&self.filepath); 116 | let file_name = path.file_name().and_then(|n| n.to_str()).unwrap_or(""); 117 | let extension = path.extension().and_then(|x| x.to_str()).unwrap_or(""); 118 | 119 | // Override syntect's language detection for conflicting file extensions because 120 | // it's impossible to express this logic in a syntax definition. 121 | struct Override { 122 | extension: &'static str, 123 | prefix_langs: Vec<(&'static str, &'static str)>, 124 | default: &'static str, 125 | } 126 | let overrides = vec![Override { 127 | extension: "cls", 128 | prefix_langs: vec![("%", "TeX"), ("\\", "TeX")], 129 | default: "Apex", 130 | }]; 131 | 132 | if let Some(Override { 133 | prefix_langs, 134 | default, 135 | .. 136 | }) = overrides.iter().find(|o| o.extension == extension) 137 | { 138 | let name = match prefix_langs 139 | .iter() 140 | .find(|(prefix, _)| self.code.starts_with(prefix)) 141 | { 142 | Some((_, lang)) => lang, 143 | None => default, 144 | }; 145 | return Ok(syntax_set 146 | .find_syntax_by_name(name) 147 | .unwrap_or_else(|| syntax_set.find_syntax_plain_text())); 148 | } 149 | 150 | Ok(syntax_set 151 | // First try to find a syntax whose "extension" matches our file 152 | // name. This is done due to some syntaxes matching an "extension" 153 | // that is actually a whole file name (e.g. "Dockerfile" or "CMakeLists.txt") 154 | // see https://github.com/trishume/syntect/pull/170 155 | .find_syntax_by_extension(file_name) 156 | .or_else(|| syntax_set.find_syntax_by_extension(extension)) 157 | .or_else(|| syntax_set.find_syntax_by_first_line(self.code)) 158 | .unwrap_or_else(|| syntax_set.find_syntax_plain_text())) 159 | } 160 | } 161 | 162 | #[cfg(test)] 163 | mod tests { 164 | use super::SourcegraphQuery; 165 | 166 | use syntect::parsing::SyntaxSet; 167 | 168 | #[test] 169 | fn cls_tex() { 170 | let syntax_set = SyntaxSet::load_defaults_newlines(); 171 | let query = SourcegraphQuery { 172 | filepath: "foo.cls", 173 | code: "%", 174 | }; 175 | let result = query.determine_language(&syntax_set); 176 | assert_eq!(result.unwrap().name, "TeX"); 177 | let _result = query.syntax_highlight(); 178 | } 179 | 180 | #[test] 181 | // renders markdown file into HTML 182 | fn markdown_render_works() { 183 | const README: &str = include_str!("./../../../../README.md"); 184 | let query = SourcegraphQuery { 185 | filepath: "README.md", 186 | code: README, 187 | }; 188 | let result = query.render_markdown(); 189 | // NOTE: this test will fail if README.md doesn't contain a H1 and an input field 190 | // of type checkbox that isn't checked/marked completed. 191 | assert!(result.contains("

")); 192 | } 193 | 194 | //#[test] 195 | //fn cls_apex() { 196 | // let syntax_set = SyntaxSet::load_defaults_newlines(); 197 | // let query = SourcegraphQuery { 198 | // filepath: "foo.cls".to_string(), 199 | // code: "/**".to_string(), 200 | // extension: String::new(), 201 | // }; 202 | // let result = determine_language(&query, &syntax_set); 203 | // assert_eq!(result.unwrap().name, "Apex"); 204 | //} 205 | } 206 | -------------------------------------------------------------------------------- /src/data/api/v1/tests/accounts.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | use std::sync::Arc; 18 | 19 | use db_core::prelude::*; 20 | 21 | use crate::api::v1::account::ChangePasswordReqest; 22 | use crate::api::v1::auth::Register; 23 | use crate::errors::*; 24 | use crate::tests::*; 25 | use crate::*; 26 | 27 | #[actix_rt::test] 28 | async fn postgrest_account_works() { 29 | let (db, data) = sqlx_postgres::get_data().await; 30 | uname_email_exists_works(data, db).await; 31 | email_udpate_password_validation_del_userworks(data,db).await; 32 | username_update_works(data, db).await; 33 | } 34 | 35 | #[actix_rt::test] 36 | async fn sqlite_account_works() { 37 | let (db, data) = sqlx_sqlite::get_data().await; 38 | uname_email_exists_works(data, db).await; 39 | email_udpate_password_validation_del_userworks(data,db).await; 40 | username_update_works(data, db).await; 41 | } 42 | 43 | async fn uname_email_exists_works(data: Arc, db: DB) { 44 | const NAME: &str = "testuserexists"; 45 | const NAME2: &str = "testuserexists2"; 46 | const NAME3: &str = "testuserexists3"; 47 | const PASSWORD: &str = "longpassword2"; 48 | const EMAIL: &str = "accotestsuser@a.com"; 49 | const EMAIL2: &str = "accotestsuser2@a.com"; 50 | const EMAIL3: &str = "accotestsuser3@a.com"; 51 | let db = &db; 52 | 53 | let _ = data.delete_user(db, NAME, PASSWORD).await; 54 | let _ = data.delete_user(db, NAME2, PASSWORD).await; 55 | let _ = data.delete_user(db, NAME3, PASSWORD).await; 56 | 57 | //// update username of nonexistent user 58 | //data.update_username(NAME, PASSWORD).await.err(); 59 | assert_eq!( 60 | data.update_username(db, NAME, PASSWORD).await.err(), 61 | Some(ServiceError::AccountNotFound) 62 | ); 63 | 64 | // update secret of nonexistent user 65 | assert_eq!( 66 | data.get_secret(db, NAME).await.err(), 67 | Some(ServiceError::AccountNotFound) 68 | ); 69 | 70 | // get secret of non-existent account 71 | assert_eq!( 72 | data.update_user_secret(db, NAME).await.err(), 73 | Some(ServiceError::AccountNotFound) 74 | ); 75 | 76 | //update email of nonexistent user 77 | assert_eq!( 78 | data.set_email(db, NAME, EMAIL).await.err(), 79 | Some(ServiceError::AccountNotFound) 80 | ); 81 | 82 | // check username exists for non existent account 83 | assert!(!data.username_exists(db, NAME).await.unwrap().exists); 84 | // check username email for non existent account 85 | assert!(!data.email_exists(db, EMAIL).await.unwrap().exists); 86 | 87 | let mut register_payload = Register { 88 | username: NAME.into(), 89 | password: PASSWORD.into(), 90 | confirm_password: PASSWORD.into(), 91 | email: Some(EMAIL.into()), 92 | }; 93 | data.register(db, ®ister_payload).await.unwrap(); 94 | register_payload.username = NAME2.into(); 95 | register_payload.email = Some(EMAIL2.into()); 96 | data.register(db, ®ister_payload).await.unwrap(); 97 | 98 | // check username exists 99 | assert!(data.username_exists(db, NAME).await.unwrap().exists); 100 | assert!(data.username_exists(db, NAME2).await.unwrap().exists); 101 | // check email exists 102 | assert!(data.email_exists(db, EMAIL).await.unwrap().exists); 103 | 104 | // check if get user secret works 105 | let secret = data.get_secret(db, NAME).await.unwrap(); 106 | 107 | data.update_user_secret(db, NAME).await.unwrap(); 108 | let new_secret = data.get_secret(db, NAME).await.unwrap(); 109 | assert_ne!(secret.secret, new_secret.secret); 110 | 111 | // update username 112 | data.update_username(db, NAME2, NAME3).await.unwrap(); 113 | assert!(!data.username_exists(db, NAME2).await.unwrap().exists); 114 | assert!(data.username_exists(db, NAME3).await.unwrap().exists); 115 | 116 | assert!(matches!( 117 | data.update_username(db, NAME3, NAME).await.err(), 118 | Some(ServiceError::UsernameTaken) 119 | )); 120 | 121 | // update email 122 | assert_eq!( 123 | data.set_email(db, NAME, EMAIL2).await.err(), 124 | Some(ServiceError::EmailTaken) 125 | ); 126 | data.set_email(db, NAME, EMAIL3).await.unwrap(); 127 | 128 | // change password 129 | let mut change_password_req = ChangePasswordReqest { 130 | password: PASSWORD.into(), 131 | new_password: NAME.into(), 132 | confirm_new_password: PASSWORD.into(), 133 | }; 134 | assert_eq!( 135 | data.change_password(db, NAME, &change_password_req) 136 | .await 137 | .err(), 138 | Some(ServiceError::PasswordsDontMatch) 139 | ); 140 | 141 | change_password_req.confirm_new_password = NAME.into(); 142 | data.change_password(db, NAME, &change_password_req) 143 | .await 144 | .unwrap(); 145 | } 146 | 147 | #[actix_rt::test] 148 | async fn email_udpate_password_validation_del_userworks(data: Arc, db: DB) { 149 | const NAME: &str = "testuser2"; 150 | const PASSWORD: &str = "longpassword2"; 151 | const EMAIL: &str = "testuser1@a.com2"; 152 | const NAME2: &str = "eupdauser"; 153 | const EMAIL2: &str = "eupdauser@a.com"; 154 | let db = &db; 155 | 156 | data.delete_user(db, NAME, PASSWORD).await; 157 | data.delete_user(db, NAME2,PASSWORD).await; 158 | 159 | let _ = data.register_and_signin(db, NAME2, EMAIL2, PASSWORD).await; 160 | let (data, _creds, signin_resp) = data.register_and_signin(db, NAME, EMAIL, PASSWORD).await; 161 | let cookies = get_cookie!(signin_resp); 162 | let app = get_app!(data).await; 163 | 164 | // update email 165 | let mut email_payload = Email { 166 | email: EMAIL.into(), 167 | }; 168 | let email_update_resp = test::call_service( 169 | &app, 170 | post_request!(&email_payload, ROUTES.account.update_email) 171 | //post_request!(&email_payload, EMAIL_UPDATE) 172 | .cookie(cookies.clone()) 173 | .to_request(), 174 | ) 175 | .await; 176 | assert_eq!(email_update_resp.status(), StatusCode::OK); 177 | 178 | // check duplicate email while duplicate email 179 | email_payload.email = EMAIL2.into(); 180 | data.bad_post_req_test( 181 | db, 182 | NAME, 183 | PASSWORD, 184 | ROUTES.account.update_email, 185 | &email_payload, 186 | ServiceError::EmailTaken, 187 | ) 188 | .await; 189 | 190 | // wrong password while deleting account 191 | let mut payload = Password { 192 | password: NAME.into(), 193 | }; 194 | data.bad_post_req_test( 195 | db, 196 | NAME, 197 | PASSWORD, 198 | ROUTES.account.delete, 199 | &payload, 200 | ServiceError::WrongPassword, 201 | ) 202 | .await; 203 | 204 | // delete account 205 | payload.password = PASSWORD.into(); 206 | let delete_user_resp = test::call_service( 207 | &app, 208 | post_request!(&payload, ROUTES.account.delete) 209 | .cookie(cookies.clone()) 210 | .to_request(), 211 | ) 212 | .await; 213 | 214 | assert_eq!(delete_user_resp.status(), StatusCode::OK); 215 | 216 | // try to delete an account that doesn't exist 217 | let account_not_found_resp = test::call_service( 218 | &app, 219 | post_request!(&payload, ROUTES.account.delete) 220 | .cookie(cookies) 221 | .to_request(), 222 | ) 223 | .await; 224 | assert_eq!(account_not_found_resp.status(), StatusCode::NOT_FOUND); 225 | let txt: ErrorToResponse = test::read_body_json(account_not_found_resp).await; 226 | assert_eq!(txt.error, format!("{}", ServiceError::AccountNotFound)); 227 | } 228 | 229 | async fn username_update_works(data: Arc, db: DB) { 230 | const NAME: &str = "testuserupda"; 231 | const EMAIL: &str = "testuserupda@sss.com"; 232 | const EMAIL2: &str = "testuserupda2@sss.com"; 233 | const PASSWORD: &str = "longpassword2"; 234 | const NAME2: &str = "terstusrtds"; 235 | const NAME_CHANGE: &str = "terstusrtdsxx"; 236 | 237 | let db = &db; 238 | 239 | futures::join!( 240 | data.delete_user(db, NAME, PASSWORD), 241 | data.delete_user(db, NAME2, PASSWORD), 242 | data.delete_user(db, NAME_CHANGE, PASSWORD) 243 | ); 244 | 245 | let _ = data.register_and_signin(db, NAME2, EMAIL2, PASSWORD).await; 246 | let (_creds, signin_resp) = data.register_and_signin(db, NAME, EMAIL, PASSWORD).await; 247 | let cookies = get_cookie!(signin_resp); 248 | let app = get_app!(data).await; 249 | 250 | // update username 251 | let mut username_udpate = Username { 252 | username: NAME_CHANGE.into(), 253 | }; 254 | let username_update_resp = test::call_service( 255 | &app, 256 | post_request!(&username_udpate, ROUTES.account.update_username) 257 | .cookie(cookies) 258 | .to_request(), 259 | ) 260 | .await; 261 | assert_eq!(username_update_resp.status(), StatusCode::OK); 262 | 263 | // check duplicate username with duplicate username 264 | username_udpate.username = NAME2.into(); 265 | data.bad_post_req_test( 266 | db, 267 | NAME_CHANGE, 268 | PASSWORD, 269 | ROUTES.account.update_username, 270 | &username_udpate, 271 | ServiceError::UsernameTaken, 272 | ) 273 | .await; 274 | } 275 | -------------------------------------------------------------------------------- /src/data/api/v1/tests/auth.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | use std::sync::Arc; 18 | 19 | use db_core::GPDatabse; 20 | 21 | use crate::api::v1::auth::{Login, Register}; 22 | use crate::errors::*; 23 | use crate::tests::*; 24 | use crate::Data; 25 | 26 | #[actix_rt::test] 27 | async fn postgrest_auth_works() { 28 | let (db, data) = sqlx_postgres::get_data().await; 29 | auth_works(data, &db).await; 30 | } 31 | 32 | #[actix_rt::test] 33 | async fn sqlite_auth_works() { 34 | let (db, data) = sqlx_sqlite::get_data().await; 35 | auth_works(data, &db).await; 36 | } 37 | 38 | async fn auth_works(data: Arc, db: &Box) { 39 | const NAME: &str = "testuser"; 40 | const PASSWORD: &str = "longpassword"; 41 | const EMAIL: &str = "testuser1@a.com"; 42 | 43 | let _ = data.delete_user(db, NAME, PASSWORD).await; 44 | 45 | // 1. Register with email == None 46 | let mut register_payload = Register { 47 | username: NAME.into(), 48 | password: PASSWORD.into(), 49 | confirm_password: PASSWORD.into(), 50 | email: None, 51 | }; 52 | 53 | data.register(db, ®ister_payload).await.unwrap(); 54 | // check if duplicate username is allowed 55 | assert!(matches!( 56 | data.register(db, ®ister_payload).await.err(), 57 | Some(ServiceError::UsernameTaken) 58 | )); 59 | 60 | // delete user 61 | data.delete_user(db, NAME, PASSWORD).await.unwrap(); 62 | 63 | // registration: passwords don't match 64 | register_payload.confirm_password = NAME.into(); 65 | assert!(matches!( 66 | data.register(db, ®ister_payload).await.err(), 67 | Some(ServiceError::PasswordsDontMatch) 68 | )); 69 | 70 | // Register with email 71 | register_payload.email = Some(EMAIL.into()); 72 | register_payload.confirm_password = PASSWORD.into(); 73 | data.register(db, ®ister_payload).await.unwrap(); 74 | 75 | // check if duplicate username is allowed 76 | let name = format!("{}dupemail", NAME); 77 | register_payload.username = name; 78 | assert!(matches!( 79 | data.register(db, ®ister_payload).await.err(), 80 | Some(ServiceError::EmailTaken) 81 | )); 82 | 83 | // Sign in with email 84 | let mut creds = Login { 85 | login: EMAIL.into(), 86 | password: PASSWORD.into(), 87 | }; 88 | data.login(db, &creds).await.unwrap(); 89 | 90 | // signin with username 91 | creds.login = NAME.into(); 92 | data.login(db, &creds).await.unwrap(); 93 | 94 | // sigining in with non-existent username 95 | creds.login = "nonexistantuser".into(); 96 | assert!(matches!( 97 | data.login(db, &creds).await.err(), 98 | Some(ServiceError::AccountNotFound) 99 | )); 100 | 101 | // sigining in with non-existent email 102 | creds.login = "nonexistantuser@example.com".into(); 103 | assert!(matches!( 104 | data.login(db, &creds).await.err(), 105 | Some(ServiceError::AccountNotFound) 106 | )); 107 | 108 | // sign in with incorrect password 109 | creds.login = NAME.into(); 110 | creds.password = NAME.into(); 111 | assert!(matches!( 112 | data.login(db, &creds).await.err(), 113 | Some(ServiceError::WrongPassword) 114 | )); 115 | } 116 | -------------------------------------------------------------------------------- /src/data/api/v1/tests/mod.rs: -------------------------------------------------------------------------------- 1 | mod accounts; 2 | mod auth; 3 | -------------------------------------------------------------------------------- /src/data/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | use std::sync::Arc; 18 | use std::thread; 19 | 20 | use argon2_creds::{Config as ArgonConfig, ConfigBuilder as ArgonConfigBuilder, PasswordPolicy}; 21 | 22 | use crate::settings::Settings; 23 | 24 | pub mod api; 25 | 26 | /// App data 27 | #[derive(Clone)] 28 | pub struct Data { 29 | /// credential-procession policy 30 | pub creds: ArgonConfig, 31 | /// settings 32 | pub settings: Settings, 33 | } 34 | 35 | impl Data { 36 | /// Get credential-processing policy 37 | pub fn get_creds() -> ArgonConfig { 38 | ArgonConfigBuilder::default() 39 | .username_case_mapped(true) 40 | .profanity(true) 41 | .blacklist(true) 42 | .password_policy(PasswordPolicy::default()) 43 | .build() 44 | .unwrap() 45 | } 46 | 47 | #[cfg(not(tarpaulin_include))] 48 | /// create new instance of app data 49 | pub fn new(settings: Option) -> Arc { 50 | let settings = settings.unwrap_or_else(|| Settings::new().unwrap()); 51 | let creds = Self::get_creds(); 52 | let c = creds.clone(); 53 | 54 | #[allow(unused_variables)] 55 | let init = thread::spawn(move || { 56 | log::info!("Initializing credential manager"); 57 | c.init(); 58 | log::info!("Initialized credential manager"); 59 | }); 60 | 61 | let data = Data { creds, settings }; 62 | 63 | #[cfg(not(debug_assertions))] 64 | init.join().unwrap(); 65 | 66 | Arc::new(data) 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/db.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | use crate::settings::Settings; 18 | use db_core::prelude::*; 19 | 20 | pub type BoxDB = Box; 21 | 22 | pub mod pg { 23 | 24 | use super::*; 25 | use db_sqlx_postgres::{ConnectionOptions, Fresh}; 26 | use sqlx::postgres::PgPoolOptions; 27 | 28 | pub async fn get_data(settings: Option) -> BoxDB { 29 | let settings = settings.unwrap_or_else(|| Settings::new().unwrap()); 30 | let pool = settings.database.pool; 31 | let pool_options = PgPoolOptions::new().max_connections(pool); 32 | let connection_options = ConnectionOptions::Fresh(Fresh { 33 | pool_options, 34 | url: settings.database.url, 35 | }); 36 | let db = connection_options.connect().await.unwrap(); 37 | db.migrate().await.unwrap(); 38 | Box::new(db) 39 | } 40 | } 41 | 42 | pub mod sqlite { 43 | use super::*; 44 | use db_sqlx_sqlite::{ConnectionOptions, Fresh}; 45 | use sqlx::sqlite::SqlitePoolOptions; 46 | 47 | pub async fn get_data(settings: Option) -> BoxDB { 48 | let settings = settings.unwrap_or_else(|| Settings::new().unwrap()); 49 | 50 | let pool = settings.database.pool; 51 | let pool_options = SqlitePoolOptions::new().max_connections(pool); 52 | let connection_options = ConnectionOptions::Fresh(Fresh { 53 | pool_options, 54 | url: settings.database.url, 55 | }); 56 | 57 | let db = connection_options.connect().await.unwrap(); 58 | db.migrate().await.unwrap(); 59 | Box::new(db) 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/demo.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | //! Demo user: Enable users to try out your application without signing up 18 | use std::time::Duration; 19 | 20 | use tokio::spawn; 21 | use tokio::time::sleep; 22 | 23 | use crate::data::api::v1::auth::Register; 24 | use crate::db::BoxDB; 25 | use crate::*; 26 | 27 | use errors::*; 28 | 29 | /// Demo username 30 | pub const DEMO_USER: &str = "aaronsw"; 31 | /// Demo password 32 | pub const DEMO_PASSWORD: &str = "password"; 33 | 34 | /// register demo user runner 35 | async fn register_demo_user(db: &BoxDB, data: &AppData) -> ServiceResult<()> { 36 | if !data.username_exists(db, DEMO_USER).await?.exists { 37 | let register_payload = Register { 38 | username: DEMO_USER.into(), 39 | password: DEMO_PASSWORD.into(), 40 | confirm_password: DEMO_PASSWORD.into(), 41 | email: None, 42 | }; 43 | 44 | log::info!("Registering demo user"); 45 | match data.register(db, ®ister_payload).await { 46 | Err(ServiceError::UsernameTaken) | Ok(_) => Ok(()), 47 | Err(e) => Err(e), 48 | } 49 | } else { 50 | Ok(()) 51 | } 52 | } 53 | 54 | async fn delete_demo_user(db: &BoxDB, data: &AppData) -> ServiceResult<()> { 55 | log::info!("Deleting demo user"); 56 | data.delete_user(db, DEMO_USER, DEMO_PASSWORD).await?; 57 | Ok(()) 58 | } 59 | 60 | /// creates and deletes demo user periodically 61 | pub async fn run(db: BoxDB, data: AppData, duration: Duration) -> ServiceResult<()> { 62 | register_demo_user(&db, &data).await?; 63 | 64 | let fut = async move { 65 | loop { 66 | sleep(duration).await; 67 | if let Err(e) = delete_demo_user(&db, &data).await { 68 | log::error!("Error while deleting demo user: {:?}", e); 69 | } 70 | if let Err(e) = register_demo_user(&db, &data).await { 71 | log::error!("Error while registering demo user: {:?}", e); 72 | } 73 | } 74 | }; 75 | spawn(fut); 76 | Ok(()) 77 | } 78 | 79 | #[cfg(test)] 80 | mod tests { 81 | use super::*; 82 | use crate::data::api::v1::auth::Login; 83 | use crate::tests::*; 84 | 85 | const DURATION: u64 = 5; 86 | 87 | #[actix_rt::test] 88 | async fn postgrest_demo_works() { 89 | let (db, data) = sqlx_postgres::get_data().await; 90 | let (db2, _) = sqlx_postgres::get_data().await; 91 | demo_account_works(data, &db, &db2).await; 92 | } 93 | 94 | #[actix_rt::test] 95 | async fn sqlite_demo_works() { 96 | let (db, data) = sqlx_sqlite::get_data().await; 97 | let (db2, _) = sqlx_sqlite::get_data().await; 98 | demo_account_works(data, &db, &db2).await; 99 | } 100 | 101 | async fn demo_account_works(data: Arc, db: &BoxDB, db2: &BoxDB) { 102 | let _ = data.delete_user(db, DEMO_USER, DEMO_PASSWORD).await; 103 | let data = AppData::new(data); 104 | let duration = Duration::from_secs(DURATION); 105 | 106 | // register works 107 | let _ = register_demo_user(db, &data).await.unwrap(); 108 | assert!(data.username_exists(db, DEMO_USER).await.unwrap().exists); 109 | let signin = Login { 110 | login: DEMO_USER.into(), 111 | password: DEMO_PASSWORD.into(), 112 | }; 113 | data.login(db, &signin).await.unwrap(); 114 | 115 | // deletion works 116 | assert!(super::delete_demo_user(db, &data).await.is_ok()); 117 | assert!(!data.username_exists(db, DEMO_USER).await.unwrap().exists); 118 | run(db2.clone(), data.clone(), duration).await.unwrap(); 119 | 120 | sleep(Duration::from_secs(DURATION)).await; 121 | assert!(data.username_exists(db, DEMO_USER).await.unwrap().exists); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | use std::env; 18 | use std::sync::Arc; 19 | 20 | use actix_identity::{CookieIdentityPolicy, IdentityService}; 21 | use actix_web::{ 22 | error::InternalError, http::StatusCode, middleware as actix_middleware, web::Data as WebData, 23 | web::JsonConfig, App, HttpServer, 24 | }; 25 | use lazy_static::lazy_static; 26 | use log::info; 27 | use static_assets::FileMap; 28 | 29 | mod api; 30 | pub mod data; 31 | mod db; 32 | pub mod demo; 33 | pub mod errors; 34 | mod pages; 35 | mod routes; 36 | mod settings; 37 | mod static_assets; 38 | #[cfg(test)] 39 | mod tests; 40 | mod utils; 41 | 42 | pub use api::v1::ROUTES as V1_API_ROUTES; 43 | pub use data::Data; 44 | pub use settings::Settings; 45 | 46 | pub const CACHE_AGE: u32 = 604800; 47 | 48 | pub const GIT_COMMIT_HASH: &str = env!("GIT_HASH"); 49 | pub const VERSION: &str = env!("CARGO_PKG_VERSION"); 50 | pub const PKG_NAME: &str = env!("CARGO_PKG_NAME"); 51 | pub const PKG_DESCRIPTION: &str = env!("CARGO_PKG_DESCRIPTION"); 52 | pub const PKG_HOMEPAGE: &str = env!("CARGO_PKG_HOMEPAGE"); 53 | 54 | pub type AppData = WebData>; 55 | pub type DB = WebData>; 56 | 57 | lazy_static! { 58 | pub static ref FILES: FileMap = FileMap::new(); 59 | } 60 | 61 | #[cfg(not(tarpaulin_include))] 62 | #[actix_web::main] 63 | async fn main() -> std::io::Result<()> { 64 | let settings = Settings::new().unwrap(); 65 | pretty_env_logger::init(); 66 | lazy_static::initialize(&pages::TEMPLATES); 67 | info!( 68 | "{}: {}.\nFor more information, see: {}\nBuild info:\nVersion: {} commit: {}", 69 | PKG_NAME, 70 | PKG_DESCRIPTION, 71 | PKG_HOMEPAGE, 72 | VERSION, 73 | &GIT_COMMIT_HASH[..10] 74 | ); 75 | 76 | println!("Starting server on: http://{}", settings.server.get_ip()); 77 | let workers = settings.server.workers.unwrap_or_else(num_cpus::get); 78 | let socket_addr = settings.server.get_ip(); 79 | 80 | log::info!("DB type: {}", settings.database.database_type); 81 | let db = match settings.database.database_type { 82 | settings::DBType::Sqlite => db::sqlite::get_data(Some(settings.clone())).await, 83 | settings::DBType::Postgres => db::pg::get_data(Some(settings.clone())).await, 84 | }; 85 | let db = WebData::new(db); 86 | 87 | let data = WebData::new(data::Data::new(Some(settings))); 88 | HttpServer::new(move || { 89 | App::new() 90 | .wrap(actix_middleware::Logger::default()) 91 | .wrap(actix_middleware::Compress::default()) 92 | .app_data(data.clone()) 93 | .app_data(db.clone()) 94 | .app_data(get_json_err()) 95 | .wrap(get_identity_service(&data.settings)) 96 | .wrap( 97 | actix_middleware::DefaultHeaders::new() 98 | .add(("Permissions-Policy", "interest-cohort=()")), 99 | ) 100 | .wrap(actix_middleware::NormalizePath::new( 101 | actix_middleware::TrailingSlash::Trim, 102 | )) 103 | .configure(routes::services) 104 | }) 105 | .workers(workers) 106 | .bind(socket_addr) 107 | .unwrap() 108 | .run() 109 | .await 110 | } 111 | 112 | #[cfg(not(tarpaulin_include))] 113 | pub fn get_json_err() -> JsonConfig { 114 | JsonConfig::default() 115 | .error_handler(|err, _| InternalError::new(err, StatusCode::BAD_REQUEST).into()) 116 | } 117 | 118 | #[cfg(not(tarpaulin_include))] 119 | pub fn get_identity_service(settings: &Settings) -> IdentityService { 120 | let cookie_secret = &settings.server.cookie_secret; 121 | IdentityService::new( 122 | CookieIdentityPolicy::new(cookie_secret.as_bytes()) 123 | .path("/") 124 | .name("Authorization") 125 | //TODO change cookie age 126 | .max_age_secs(216000) 127 | .domain(&settings.server.domain) 128 | .secure(false), 129 | ) 130 | } 131 | -------------------------------------------------------------------------------- /src/pages/auth/login.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | use std::cell::RefCell; 18 | 19 | use actix_identity::Identity; 20 | use actix_web::http::header::ContentType; 21 | use tera::Context; 22 | 23 | use crate::api::v1::RedirectQuery; 24 | use crate::data::api::v1::auth::Login as LoginPayload; 25 | use crate::pages::errors::*; 26 | use crate::settings::Settings; 27 | use crate::AppData; 28 | 29 | pub use super::*; 30 | 31 | pub struct Login { 32 | ctx: RefCell, 33 | } 34 | 35 | pub const LOGIN: TemplateFile = TemplateFile::new("login", "pages/auth/login.html"); 36 | 37 | impl CtxError for Login { 38 | fn with_error(&self, e: &ReadableError) -> String { 39 | self.ctx.borrow_mut().insert(ERROR_KEY, e); 40 | self.render() 41 | } 42 | } 43 | 44 | impl Login { 45 | pub fn new(settings: &Settings, payload: Option<&LoginPayload>) -> Self { 46 | let ctx = RefCell::new(context(settings)); 47 | if let Some(payload) = payload { 48 | ctx.borrow_mut().insert(PAYLOAD_KEY, payload); 49 | } 50 | Self { ctx } 51 | } 52 | 53 | pub fn render(&self) -> String { 54 | TEMPLATES.render(LOGIN.name, &self.ctx.borrow()).unwrap() 55 | } 56 | 57 | pub fn page(s: &Settings) -> String { 58 | let p = Self::new(s, None); 59 | p.render() 60 | } 61 | } 62 | 63 | #[my_codegen::get(path = "PAGES.auth.login")] 64 | pub async fn get_login(data: AppData) -> impl Responder { 65 | let login = Login::page(&data.settings); 66 | let html = ContentType::html(); 67 | HttpResponse::Ok().content_type(html).body(login) 68 | } 69 | 70 | pub fn services(cfg: &mut web::ServiceConfig) { 71 | cfg.service(get_login); 72 | cfg.service(login_submit); 73 | } 74 | 75 | #[my_codegen::post(path = "PAGES.auth.login")] 76 | pub async fn login_submit( 77 | id: Identity, 78 | payload: web::Form, 79 | query: web::Query, 80 | data: AppData, 81 | db: crate::DB, 82 | ) -> PageResult { 83 | let username = data 84 | .login(&(**db), &payload) 85 | .await 86 | .map_err(|e| PageError::new(Login::new(&data.settings, Some(&payload)), e))?; 87 | id.remember(username); 88 | let query = query.into_inner(); 89 | if let Some(redirect_to) = query.redirect_to { 90 | Ok(HttpResponse::Found() 91 | .insert_header((http::header::LOCATION, redirect_to)) 92 | .finish()) 93 | } else { 94 | Ok(HttpResponse::Found() 95 | .insert_header((http::header::LOCATION, PAGES.home)) 96 | .finish()) 97 | } 98 | } 99 | 100 | #[cfg(test)] 101 | mod tests { 102 | use super::Login; 103 | use super::LoginPayload; 104 | use crate::errors::*; 105 | use crate::pages::errors::*; 106 | use crate::settings::Settings; 107 | 108 | #[test] 109 | fn register_page_renders() { 110 | let settings = Settings::new().unwrap(); 111 | Login::page(&settings); 112 | let payload = LoginPayload { 113 | login: "foo".into(), 114 | password: "foo".into(), 115 | }; 116 | let page = Login::new(&settings, Some(&payload)); 117 | page.with_error(&ReadableError::new(&ServiceError::WrongPassword)); 118 | page.render(); 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /src/pages/auth/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | use actix_identity::Identity; 18 | use actix_web::*; 19 | 20 | pub use super::{context, Footer, TemplateFile, PAGES, PAYLOAD_KEY, TEMPLATES}; 21 | 22 | pub mod login; 23 | pub mod register; 24 | #[cfg(test)] 25 | mod test; 26 | 27 | pub const AUTH_BASE: TemplateFile = TemplateFile::new("authbase", "pages/auth/base.html"); 28 | pub const DEMO: TemplateFile = TemplateFile::new("demo_banner", "pages/auth/demo.html"); 29 | 30 | pub fn register_templates(t: &mut tera::Tera) { 31 | for template in [AUTH_BASE, login::LOGIN, register::REGISTER, DEMO].iter() { 32 | template.register(t).expect(template.name); 33 | } 34 | } 35 | 36 | pub fn services(cfg: &mut web::ServiceConfig) { 37 | cfg.service(signout); 38 | register::services(cfg); 39 | login::services(cfg); 40 | } 41 | 42 | #[my_codegen::get(path = "PAGES.auth.logout", wrap = "super::get_auth_middleware()")] 43 | async fn signout(id: Identity) -> impl Responder { 44 | use actix_auth_middleware::GetLoginRoute; 45 | 46 | if id.identity().is_some() { 47 | id.forget(); 48 | } 49 | HttpResponse::Found() 50 | .append_header((http::header::LOCATION, PAGES.get_login_route(None))) 51 | .finish() 52 | } 53 | 54 | //#[post(path = "PAGES.auth.login")] 55 | //pub async fn login_submit( 56 | // id: Identity, 57 | // payload: web::Form, 58 | // data: AppData, 59 | //) -> PageResult { 60 | // let payload = payload.into_inner(); 61 | // match runners::login_runner(&payload, &data).await { 62 | // Ok(username) => { 63 | // id.remember(username); 64 | // Ok(HttpResponse::Found() 65 | // .insert_header((header::LOCATION, PAGES.home)) 66 | // .finish()) 67 | // } 68 | // Err(e) => { 69 | // let status = e.status_code(); 70 | // let heading = status.canonical_reason().unwrap_or("Error"); 71 | // 72 | // Ok(HttpResponseBuilder::new(status) 73 | // .content_type("text/html; charset=utf-8") 74 | // .body( 75 | // IndexPage::new(heading, &format!("{}", e)) 76 | // .render_once() 77 | // .unwrap(), 78 | // )) 79 | // } 80 | // } 81 | //} 82 | // 83 | //#[cfg(test)] 84 | //mod tests { 85 | // use actix_web::test; 86 | // 87 | // use super::*; 88 | // 89 | // use crate::api::v1::auth::runners::{Login, Register}; 90 | // use crate::data::Data; 91 | // use crate::tests::*; 92 | // use crate::*; 93 | // use actix_web::http::StatusCode; 94 | // 95 | // #[actix_rt::test] 96 | // async fn auth_form_works() { 97 | // let data = Data::new().await; 98 | // const NAME: &str = "testuserform"; 99 | // const PASSWORD: &str = "longpassword"; 100 | // 101 | // let app = get_app!(data).await; 102 | // 103 | // delete_user(NAME, &data).await; 104 | // 105 | // // 1. Register with email == None 106 | // let msg = Register { 107 | // username: NAME.into(), 108 | // password: PASSWORD.into(), 109 | // confirm_password: PASSWORD.into(), 110 | // email: None, 111 | // }; 112 | // let resp = test::call_service( 113 | // &app, 114 | // post_request!(&msg, V1_API_ROUTES.auth.register).to_request(), 115 | // ) 116 | // .await; 117 | // assert_eq!(resp.status(), StatusCode::OK); 118 | // 119 | // // correct form login 120 | // let msg = Login { 121 | // login: NAME.into(), 122 | // password: PASSWORD.into(), 123 | // }; 124 | // 125 | // let resp = test::call_service( 126 | // &app, 127 | // post_request!(&msg, PAGES.auth.login, FORM).to_request(), 128 | // ) 129 | // .await; 130 | // assert_eq!(resp.status(), StatusCode::FOUND); 131 | // let headers = resp.headers(); 132 | // assert_eq!(headers.get(header::LOCATION).unwrap(), PAGES.home,); 133 | // 134 | // // incorrect form login 135 | // let msg = Login { 136 | // login: NAME.into(), 137 | // password: NAME.into(), 138 | // }; 139 | // let resp = test::call_service( 140 | // &app, 141 | // post_request!(&msg, PAGES.auth.login, FORM).to_request(), 142 | // ) 143 | // .await; 144 | // assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); 145 | // 146 | // // non-existent form login 147 | // let msg = Login { 148 | // login: PASSWORD.into(), 149 | // password: PASSWORD.into(), 150 | // }; 151 | // let resp = test::call_service( 152 | // &app, 153 | // post_request!(&msg, PAGES.auth.login, FORM).to_request(), 154 | // ) 155 | // .await; 156 | // assert_eq!(resp.status(), StatusCode::NOT_FOUND); 157 | // } 158 | //} 159 | // 160 | -------------------------------------------------------------------------------- /src/pages/auth/register.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | use actix_web::http::header::ContentType; 18 | use std::cell::RefCell; 19 | use tera::Context; 20 | 21 | use crate::data::api::v1::auth::Register as RegisterPayload; 22 | use crate::pages::errors::*; 23 | use crate::settings::Settings; 24 | use crate::AppData; 25 | 26 | pub use super::*; 27 | 28 | pub const REGISTER: TemplateFile = TemplateFile::new("register", "pages/auth/register.html"); 29 | 30 | pub struct Register { 31 | ctx: RefCell, 32 | } 33 | 34 | impl CtxError for Register { 35 | fn with_error(&self, e: &ReadableError) -> String { 36 | self.ctx.borrow_mut().insert(ERROR_KEY, e); 37 | self.render() 38 | } 39 | } 40 | 41 | impl Register { 42 | fn new(settings: &Settings, payload: Option<&RegisterPayload>) -> Self { 43 | let ctx = RefCell::new(context(settings)); 44 | if let Some(payload) = payload { 45 | ctx.borrow_mut().insert(PAYLOAD_KEY, payload); 46 | } 47 | Self { ctx } 48 | } 49 | 50 | pub fn render(&self) -> String { 51 | TEMPLATES.render(REGISTER.name, &self.ctx.borrow()).unwrap() 52 | } 53 | 54 | pub fn page(s: &Settings) -> String { 55 | let p = Self::new(s, None); 56 | p.render() 57 | } 58 | } 59 | 60 | #[my_codegen::get(path = "PAGES.auth.register")] 61 | pub async fn get_register(data: AppData) -> impl Responder { 62 | let login = Register::page(&data.settings); 63 | let html = ContentType::html(); 64 | HttpResponse::Ok().content_type(html).body(login) 65 | } 66 | 67 | pub fn services(cfg: &mut web::ServiceConfig) { 68 | cfg.service(get_register); 69 | cfg.service(register_submit); 70 | } 71 | 72 | #[my_codegen::post(path = "PAGES.auth.register")] 73 | pub async fn register_submit( 74 | mut payload: web::Form, 75 | data: AppData, 76 | db: crate::DB, 77 | ) -> PageResult { 78 | if let Some(email) = &payload.email { 79 | if email.is_empty() { 80 | payload.email = None; 81 | } 82 | } 83 | data.register(&(**db), &payload) 84 | .await 85 | .map_err(|e| PageError::new(Register::new(&data.settings, Some(&payload)), e))?; 86 | Ok(HttpResponse::Found() 87 | .insert_header((http::header::LOCATION, PAGES.auth.login)) 88 | .finish()) 89 | } 90 | 91 | #[cfg(test)] 92 | mod tests { 93 | use super::Register; 94 | use super::RegisterPayload; 95 | use crate::errors::*; 96 | use crate::pages::errors::*; 97 | use crate::settings::Settings; 98 | 99 | #[test] 100 | fn register_page_renders() { 101 | let settings = Settings::new().unwrap(); 102 | Register::page(&settings); 103 | let payload = RegisterPayload { 104 | username: "foo".into(), 105 | password: "foo".into(), 106 | confirm_password: "foo".into(), 107 | email: Some("foo".into()), 108 | }; 109 | let page = Register::new(&settings, Some(&payload)); 110 | page.with_error(&ReadableError::new(&ServiceError::WrongPassword)); 111 | page.render(); 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/pages/auth/test.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | use actix_auth_middleware::GetLoginRoute; 18 | 19 | use actix_web::http::header; 20 | use actix_web::http::StatusCode; 21 | use actix_web::test; 22 | 23 | use super::*; 24 | 25 | use crate::data::api::v1::auth::{Login, Register}; 26 | use crate::data::Data; 27 | use crate::errors::*; 28 | use crate::tests::*; 29 | use crate::*; 30 | 31 | #[actix_rt::test] 32 | async fn postgrest_pages_auth_works() { 33 | let (db, data) = sqlx_postgres::get_data().await; 34 | auth_works(data.clone(), db.clone()).await; 35 | serverside_password_validation_works(data.clone(), db.clone()).await; 36 | } 37 | 38 | #[actix_rt::test] 39 | async fn sqlite_pages_auth_works() { 40 | let (db, data) = sqlx_sqlite::get_data().await; 41 | auth_works(data.clone(), db.clone()).await; 42 | serverside_password_validation_works(data.clone(), db.clone()).await; 43 | } 44 | 45 | async fn auth_works(data: Arc, db: BoxDB) { 46 | const NAME: &str = "testuserform"; 47 | const EMAIL: &str = "testuserform@foo.com"; 48 | const PASSWORD: &str = "longpassword"; 49 | 50 | let _ = data.delete_user(&db, NAME, PASSWORD).await; 51 | let app = get_app!(data, db).await; 52 | 53 | // 1. Register with email == None 54 | let msg = Register { 55 | username: NAME.into(), 56 | password: PASSWORD.into(), 57 | confirm_password: PASSWORD.into(), 58 | email: None, 59 | }; 60 | let resp = test::call_service( 61 | &app, 62 | post_request!(&msg, PAGES.auth.register, FORM).to_request(), 63 | ) 64 | .await; 65 | assert_eq!(resp.status(), StatusCode::FOUND); 66 | let headers = resp.headers(); 67 | assert_eq!(headers.get(header::LOCATION).unwrap(), PAGES.auth.login); 68 | let _ = data.delete_user(&db, NAME, PASSWORD).await; 69 | 70 | // 1. Register with email == "" // Form request handler converts empty emails to None 71 | let msg = Register { 72 | username: NAME.into(), 73 | password: PASSWORD.into(), 74 | confirm_password: PASSWORD.into(), 75 | email: Some("".into()), 76 | }; 77 | let resp = test::call_service( 78 | &app, 79 | post_request!(&msg, PAGES.auth.register, FORM).to_request(), 80 | ) 81 | .await; 82 | assert_eq!(resp.status(), StatusCode::FOUND); 83 | let headers = resp.headers(); 84 | assert_eq!(headers.get(header::LOCATION).unwrap(), PAGES.auth.login); 85 | let _ = data.delete_user(&db, NAME, PASSWORD).await; 86 | 87 | // 1. Register with email 88 | let msg = Register { 89 | username: NAME.into(), 90 | password: PASSWORD.into(), 91 | confirm_password: PASSWORD.into(), 92 | email: Some(EMAIL.into()), 93 | }; 94 | let resp = test::call_service( 95 | &app, 96 | post_request!(&msg, PAGES.auth.register, FORM).to_request(), 97 | ) 98 | .await; 99 | assert_eq!(resp.status(), StatusCode::FOUND); 100 | let headers = resp.headers(); 101 | assert_eq!(headers.get(header::LOCATION).unwrap(), PAGES.auth.login); 102 | 103 | // sign in 104 | let msg = Login { 105 | login: NAME.into(), 106 | password: PASSWORD.into(), 107 | }; 108 | let resp = test::call_service( 109 | &app, 110 | post_request!(&msg, PAGES.auth.login, FORM).to_request(), 111 | ) 112 | .await; 113 | assert_eq!(resp.status(), StatusCode::FOUND); 114 | let headers = resp.headers(); 115 | assert_eq!(headers.get(header::LOCATION).unwrap(), PAGES.home); 116 | let cookies = get_cookie!(resp); 117 | 118 | // redirect after signin 119 | let redirect = "/foo/bar/nonexistantuser"; 120 | let url = PAGES.get_login_route(Some(redirect)); 121 | let resp = test::call_service(&app, post_request!(&msg, &url, FORM).to_request()).await; 122 | assert_eq!(resp.status(), StatusCode::FOUND); 123 | let headers = resp.headers(); 124 | assert_eq!(headers.get(header::LOCATION).unwrap(), &redirect); 125 | 126 | // wrong password signin 127 | let msg = Login { 128 | login: NAME.into(), 129 | password: NAME.into(), 130 | }; 131 | let resp = test::call_service( 132 | &app, 133 | post_request!(&msg, PAGES.auth.login, FORM).to_request(), 134 | ) 135 | .await; 136 | assert_eq!(resp.status(), ServiceError::WrongPassword.status_code()); 137 | 138 | // signout 139 | let signout_resp = test::call_service( 140 | &app, 141 | test::TestRequest::get() 142 | .uri(PAGES.auth.logout) 143 | .cookie(cookies) 144 | .to_request(), 145 | ) 146 | .await; 147 | assert_eq!(signout_resp.status(), StatusCode::FOUND); 148 | let headers = signout_resp.headers(); 149 | assert_eq!( 150 | headers.get(header::LOCATION).unwrap(), 151 | &PAGES.get_login_route(None) 152 | ); 153 | } 154 | 155 | async fn serverside_password_validation_works(data: Arc, db: BoxDB) { 156 | const NAME: &str = "pagetestuser542"; 157 | const PASSWORD: &str = "longpassword2"; 158 | 159 | let db = &db; 160 | let _ = data.delete_user(db, NAME, PASSWORD).await; 161 | 162 | let app = get_app!(data, db).await; 163 | 164 | // checking to see if server-side password validation (password == password_config) 165 | // works 166 | let register_msg = Register { 167 | username: NAME.into(), 168 | password: PASSWORD.into(), 169 | confirm_password: NAME.into(), 170 | email: None, 171 | }; 172 | let resp = test::call_service( 173 | &app, 174 | post_request!(®ister_msg, PAGES.auth.register, FORM).to_request(), 175 | ) 176 | .await; 177 | assert_eq!( 178 | resp.status(), 179 | ServiceError::PasswordsDontMatch.status_code() 180 | ); 181 | } 182 | -------------------------------------------------------------------------------- /src/pages/errors.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | use std::fmt; 18 | 19 | use actix_web::{ 20 | error::ResponseError, 21 | http::{header::ContentType, StatusCode}, 22 | HttpResponse, HttpResponseBuilder, 23 | }; 24 | use derive_more::Display; 25 | use derive_more::Error; 26 | use serde::*; 27 | 28 | use super::TemplateFile; 29 | use crate::errors::ServiceError; 30 | 31 | pub const ERROR_KEY: &str = "error"; 32 | 33 | pub const ERROR_TEMPLATE: TemplateFile = TemplateFile::new("error_comp", "components/error.html"); 34 | pub fn register_templates(t: &mut tera::Tera) { 35 | ERROR_TEMPLATE.register(t).expect(ERROR_TEMPLATE.name); 36 | } 37 | 38 | /// Render template with error context 39 | pub trait CtxError { 40 | fn with_error(&self, e: &ReadableError) -> String; 41 | } 42 | 43 | #[derive(Serialize, Debug, Display, Clone)] 44 | #[display(fmt = "title: {} reason: {}", title, reason)] 45 | pub struct ReadableError { 46 | pub reason: String, 47 | pub title: String, 48 | } 49 | 50 | impl ReadableError { 51 | pub fn new(e: &ServiceError) -> Self { 52 | let reason = format!("{}", e); 53 | let title = format!("{}", e.status_code()); 54 | 55 | Self { reason, title } 56 | } 57 | } 58 | 59 | #[derive(Error, Display)] 60 | #[display(fmt = "{}", readable)] 61 | pub struct PageError { 62 | #[error(not(source))] 63 | template: T, 64 | readable: ReadableError, 65 | #[error(not(source))] 66 | error: ServiceError, 67 | } 68 | 69 | impl fmt::Debug for PageError { 70 | #[cfg(not(tarpaulin_include))] 71 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 72 | f.debug_struct("PageError") 73 | .field("readable", &self.readable) 74 | .finish() 75 | } 76 | } 77 | 78 | impl PageError { 79 | /// create new instance of [PageError] from a template and an error 80 | pub fn new(template: T, error: ServiceError) -> Self { 81 | let readable = ReadableError::new(&error); 82 | Self { 83 | error, 84 | template, 85 | readable, 86 | } 87 | } 88 | } 89 | 90 | #[cfg(not(tarpaulin_include))] 91 | impl ResponseError for PageError { 92 | fn error_response(&self) -> HttpResponse { 93 | HttpResponseBuilder::new(self.status_code()) 94 | .content_type(ContentType::html()) 95 | .body(self.template.with_error(&self.readable)) 96 | } 97 | 98 | fn status_code(&self) -> StatusCode { 99 | self.error.status_code() 100 | } 101 | } 102 | 103 | /// Generic result data structure 104 | #[cfg(not(tarpaulin_include))] 105 | pub type PageResult = std::result::Result>; 106 | -------------------------------------------------------------------------------- /src/pages/gists/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | use actix_web::*; 18 | 19 | pub use super::{ 20 | auth_ctx, context, errors::*, get_auth_middleware, Footer, TemplateFile, PAGES, PAYLOAD_KEY, 21 | TEMPLATES, 22 | }; 23 | 24 | pub mod new; 25 | #[cfg(test)] 26 | mod tests; 27 | pub mod view; 28 | 29 | pub const GIST_BASE: TemplateFile = TemplateFile::new("gistbase", "pages/gists/base.html"); 30 | pub const GIST_EXPLORE: TemplateFile = 31 | TemplateFile::new("gist_explore", "pages/gists/explore.html"); 32 | 33 | pub fn register_templates(t: &mut tera::Tera) { 34 | for template in [GIST_BASE, GIST_EXPLORE].iter() { 35 | template.register(t).expect(template.name); 36 | } 37 | new::register_templates(t); 38 | view::register_templates(t); 39 | } 40 | 41 | pub fn services(cfg: &mut web::ServiceConfig) { 42 | new::services(cfg); 43 | view::services(cfg); 44 | } 45 | -------------------------------------------------------------------------------- /src/pages/gists/tests.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | use actix_http::header; 18 | use actix_web::http::StatusCode; 19 | use actix_web::test; 20 | use actix_web::ResponseError; 21 | 22 | use db_core::prelude::*; 23 | use pages::routes::PostCommentPath; 24 | 25 | use super::new::*; 26 | 27 | use crate::api::v1::gists::PostCommentRequest; 28 | use crate::data::Data; 29 | use crate::errors::*; 30 | use crate::tests::*; 31 | use crate::*; 32 | 33 | #[actix_rt::test] 34 | async fn postgres_pages_gists_work() { 35 | let (db, data) = sqlx_postgres::get_data().await; 36 | gists_new_route_works(data.clone(), db.clone()).await; 37 | } 38 | 39 | #[actix_rt::test] 40 | async fn sqlite_pages_gists_work() { 41 | let (db, data) = sqlx_sqlite::get_data().await; 42 | gists_new_route_works(data.clone(), db.clone()).await; 43 | } 44 | 45 | async fn gists_new_route_works(data: Arc, db: BoxDB) { 46 | const NAME: &str = "newgisttestuserexists"; 47 | const PASSWORD: &str = "longpassword2"; 48 | const EMAIL: &str = "newgisttestuserexists@a.com2"; 49 | const COMMENT: &str = "this string is never used anywhere but for commenting, so that I can get away with body inlcudes"; 50 | let db = &db; 51 | 52 | let _ = data.delete_user(db, NAME, PASSWORD).await; 53 | 54 | let (_, signin_resp) = data.register_and_signin(db, NAME, EMAIL, PASSWORD).await; 55 | let cookies = get_cookie!(signin_resp); 56 | let app = get_app!(data, db).await; 57 | let new_gist = get_request!(&app, PAGES.gist.new, cookies.clone()); 58 | assert_eq!(new_gist.status(), StatusCode::OK); 59 | let files = FieldNames::::new(1); 60 | 61 | // create gist 62 | let payload = serde_json::json!({ 63 | "description": "", 64 | "visibility": GistVisibility::Private.to_str(), 65 | files.filename.clone() : "foo.md", 66 | files.content.clone() : "foo.md", 67 | }); 68 | 69 | let resp = test::call_service( 70 | &app, 71 | post_request!(&payload, PAGES.gist.new, FORM) 72 | .cookie(cookies.clone()) 73 | .to_request(), 74 | ) 75 | .await; 76 | assert_eq!(resp.status(), StatusCode::FOUND); 77 | let gist_id = resp.headers().get(header::LOCATION).unwrap(); 78 | let resp = get_request!(&app, gist_id.to_str().unwrap(), cookies.clone()); 79 | assert_eq!(resp.status(), StatusCode::OK); 80 | 81 | // add new file during gist creation 82 | let payload = serde_json::json!({ 83 | "description": "", 84 | "visibility": GistVisibility::Private.to_str(), 85 | files.filename.clone() : "foo.md", 86 | files.content.clone() : "foo.md", 87 | "add_file": "", 88 | }); 89 | 90 | let resp = test::call_service( 91 | &app, 92 | post_request!(&payload, PAGES.gist.new, FORM) 93 | .cookie(cookies.clone()) 94 | .to_request(), 95 | ) 96 | .await; 97 | assert_eq!(resp.status(), StatusCode::OK); 98 | let empty_gist = test::call_service( 99 | &app, 100 | post_request!(&serde_json::Value::default(), PAGES.gist.new, FORM) 101 | .cookie(cookies.clone()) 102 | .to_request(), 103 | ) 104 | .await; 105 | assert_eq!(empty_gist.status(), ServiceError::GistEmpty.status_code()); 106 | 107 | // get gist 108 | 109 | let mut route_iter = gist_id.to_str().unwrap().split('/'); 110 | let name = route_iter.nth(1).unwrap(); 111 | let gist = route_iter.next().unwrap(); 112 | let gist_route_componenet = PostCommentPath { 113 | username: name.to_string(), 114 | gist: gist.to_string(), 115 | }; 116 | let gist_html_route = PAGES.gist.get_gist_route(&gist_route_componenet); 117 | let gist_html_page = get_request!(&app, &gist_html_route); 118 | assert_eq!(gist_html_page.status(), StatusCode::OK); 119 | 120 | // post comment 121 | let comment_url = PAGES.gist.get_post_comment_route(&gist_route_componenet); 122 | let comment = PostCommentRequest { 123 | comment: COMMENT.into(), 124 | }; 125 | let comment_resp = test::call_service( 126 | &app, 127 | post_request!(&comment, &comment_url, FORM) 128 | .cookie(cookies) 129 | .to_request(), 130 | ) 131 | .await; 132 | assert_eq!(comment_resp.status(), StatusCode::FOUND); 133 | } 134 | -------------------------------------------------------------------------------- /src/pages/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | use actix_web::*; 18 | use lazy_static::lazy_static; 19 | use rust_embed::RustEmbed; 20 | use serde::*; 21 | use tera::*; 22 | 23 | use crate::settings::Settings; 24 | use crate::static_assets::ASSETS; 25 | use crate::{GIT_COMMIT_HASH, VERSION}; 26 | 27 | pub mod auth; 28 | pub mod errors; 29 | pub mod gists; 30 | pub mod routes; 31 | 32 | pub use routes::get_auth_middleware; 33 | pub use routes::PAGES; 34 | 35 | pub struct TemplateFile { 36 | pub name: &'static str, 37 | pub path: &'static str, 38 | } 39 | 40 | impl TemplateFile { 41 | pub const fn new(name: &'static str, path: &'static str) -> Self { 42 | Self { name, path } 43 | } 44 | 45 | pub fn register(&self, t: &mut Tera) -> std::result::Result<(), tera::Error> { 46 | t.add_raw_template(self.name, &Templates::get_template(self).expect(self.name)) 47 | } 48 | 49 | #[cfg(test)] 50 | #[allow(dead_code)] 51 | pub fn register_from_file(&self, t: &mut Tera) -> std::result::Result<(), tera::Error> { 52 | use std::path::Path; 53 | t.add_template_file(Path::new("templates/").join(self.path), Some(self.name)) 54 | } 55 | } 56 | 57 | pub const PAYLOAD_KEY: &str = "payload"; 58 | 59 | pub const BASE: TemplateFile = TemplateFile::new("base", "components/base.html"); 60 | pub const FOOTER: TemplateFile = TemplateFile::new("footer", "components/footer.html"); 61 | pub const PUB_NAV: TemplateFile = TemplateFile::new("pub_nav", "components/nav/pub.html"); 62 | pub const AUTH_NAV: TemplateFile = TemplateFile::new("auth_nav", "components/nav/auth.html"); 63 | 64 | lazy_static! { 65 | pub static ref TEMPLATES: Tera = { 66 | let mut tera = Tera::default(); 67 | for t in [BASE, FOOTER, PUB_NAV, AUTH_NAV].iter() { 68 | t.register(&mut tera).unwrap(); 69 | } 70 | errors::register_templates(&mut tera); 71 | tera.autoescape_on(vec![".html", ".sql"]); 72 | auth::register_templates(&mut tera); 73 | gists::register_templates(&mut tera); 74 | tera 75 | }; 76 | } 77 | 78 | #[derive(RustEmbed)] 79 | #[folder = "templates/"] 80 | pub struct Templates; 81 | 82 | impl Templates { 83 | pub fn get_template(t: &TemplateFile) -> Option { 84 | match Self::get(t.path) { 85 | Some(file) => Some(String::from_utf8_lossy(&file.data).into_owned()), 86 | None => None, 87 | } 88 | } 89 | } 90 | 91 | pub fn context(s: &Settings) -> Context { 92 | let mut ctx = Context::new(); 93 | let footer = Footer::new(s); 94 | ctx.insert("footer", &footer); 95 | ctx.insert("page", &PAGES); 96 | ctx.insert("assets", &*ASSETS); 97 | ctx 98 | } 99 | 100 | pub fn auth_ctx(username: Option<&str>, s: &Settings) -> Context { 101 | use routes::GistProfilePathComponent; 102 | let mut profile_link = None; 103 | if let Some(name) = username { 104 | profile_link = Some( 105 | PAGES 106 | .gist 107 | .get_profile_route(GistProfilePathComponent { username: name }), 108 | ); 109 | } 110 | let mut ctx = Context::new(); 111 | let footer = Footer::new(s); 112 | ctx.insert("footer", &footer); 113 | ctx.insert("page", &PAGES); 114 | ctx.insert("assets", &*ASSETS); 115 | ctx.insert("loggedin_user", &profile_link); 116 | ctx 117 | } 118 | 119 | #[derive(Serialize)] 120 | pub struct Footer<'a> { 121 | version: &'a str, 122 | admin_email: &'a str, 123 | source_code: &'a str, 124 | git_hash: &'a str, 125 | settings: &'a Settings, 126 | demo_user: &'a str, 127 | demo_password: &'a str, 128 | } 129 | 130 | impl<'a> Footer<'a> { 131 | pub fn new(settings: &'a Settings) -> Self { 132 | Self { 133 | version: VERSION, 134 | source_code: &settings.source_code, 135 | admin_email: &settings.admin_email, 136 | git_hash: &GIT_COMMIT_HASH[..8], 137 | demo_user: crate::demo::DEMO_USER, 138 | demo_password: crate::demo::DEMO_PASSWORD, 139 | settings, 140 | } 141 | } 142 | } 143 | 144 | pub fn services(cfg: &mut web::ServiceConfig) { 145 | auth::services(cfg); 146 | gists::services(cfg); 147 | } 148 | 149 | #[cfg(test)] 150 | mod tests { 151 | 152 | #[test] 153 | fn templates_work_basic() { 154 | use super::*; 155 | use tera::Tera; 156 | 157 | let mut tera = Tera::default(); 158 | let mut tera2 = Tera::default(); 159 | for t in [ 160 | BASE, 161 | FOOTER, 162 | PUB_NAV, 163 | AUTH_NAV, 164 | auth::AUTH_BASE, 165 | auth::login::LOGIN, 166 | auth::register::REGISTER, 167 | errors::ERROR_TEMPLATE, 168 | gists::GIST_BASE, 169 | gists::GIST_EXPLORE, 170 | gists::new::NEW_GIST, 171 | ] 172 | .iter() 173 | { 174 | t.register_from_file(&mut tera2).unwrap(); 175 | t.register(&mut tera).unwrap(); 176 | } 177 | } 178 | } 179 | 180 | #[cfg(test)] 181 | mod http_page_tests { 182 | use actix_web::http::StatusCode; 183 | use actix_web::test; 184 | 185 | use crate::data::Data; 186 | use crate::db::BoxDB; 187 | use crate::tests::*; 188 | use crate::*; 189 | 190 | use super::PAGES; 191 | 192 | #[actix_rt::test] 193 | async fn postgrest_templates_work() { 194 | let (db, data) = sqlx_postgres::get_data().await; 195 | templates_work(data, db).await; 196 | } 197 | 198 | #[actix_rt::test] 199 | async fn sqlite_templates_work() { 200 | let (db, data) = sqlx_sqlite::get_data().await; 201 | templates_work(data, db).await; 202 | } 203 | 204 | async fn templates_work(data: Arc, db: BoxDB) { 205 | let app = get_app!(data, db).await; 206 | 207 | for file in [PAGES.auth.login, PAGES.auth.register].iter() { 208 | let resp = get_request!(&app, file); 209 | assert_eq!(resp.status(), StatusCode::OK); 210 | } 211 | } 212 | } 213 | -------------------------------------------------------------------------------- /src/pages/routes.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | use actix_auth_middleware::{Authentication, GetLoginRoute}; 18 | use serde::*; 19 | 20 | pub use crate::api::v1::routes::{GetFilePath, PostCommentPath}; 21 | 22 | /// constant [Pages](Pages) instance 23 | pub const PAGES: Pages = Pages::new(); 24 | 25 | #[derive(Serialize)] 26 | /// Top-level routes data structure for V1 AP1 27 | pub struct Pages { 28 | /// Authentication routes 29 | pub auth: Auth, 30 | /// Gist routes 31 | pub gist: Gists, 32 | /// home page 33 | pub home: &'static str, 34 | } 35 | 36 | impl Pages { 37 | /// create new instance of Routes 38 | const fn new() -> Pages { 39 | let gist = Gists::new(); 40 | let home = gist.new; 41 | Pages { 42 | auth: Auth::new(), 43 | gist, 44 | home, 45 | } 46 | } 47 | } 48 | 49 | #[derive(Serialize)] 50 | /// Authentication routes 51 | pub struct Auth { 52 | /// logout route 53 | pub logout: &'static str, 54 | /// login route 55 | pub login: &'static str, 56 | /// registration route 57 | pub register: &'static str, 58 | } 59 | 60 | impl Auth { 61 | /// create new instance of Authentication route 62 | pub const fn new() -> Auth { 63 | let login = "/login"; 64 | let logout = "/logout"; 65 | let register = "/join"; 66 | Auth { 67 | logout, 68 | login, 69 | register, 70 | } 71 | } 72 | } 73 | 74 | #[derive(Deserialize)] 75 | pub struct GistProfilePathComponent<'a> { 76 | pub username: &'a str, 77 | } 78 | 79 | #[derive(Serialize)] 80 | /// Gist routes 81 | pub struct Gists { 82 | /// profile route 83 | pub profile: &'static str, 84 | /// new gist route 85 | pub new: &'static str, 86 | /// view gist 87 | pub view_gist: &'static str, 88 | /// post comment on gist 89 | pub post_comment: &'static str, 90 | /// get file 91 | pub get_file: &'static str, 92 | } 93 | 94 | impl Gists { 95 | /// create new instance of Gists route 96 | pub const fn new() -> Self { 97 | let profile = "/~{username}"; 98 | let view_gist = "/~{username}/{gist}"; 99 | let post_comment = "/~{username}/{gist}/comment"; 100 | let get_file = "/~{username}/{gist}/contents/{file}"; 101 | let new = "/"; 102 | Self { 103 | profile, 104 | new, 105 | view_gist, 106 | post_comment, 107 | get_file, 108 | } 109 | } 110 | 111 | /// get profile route with placeholders replaced with values provided. 112 | pub fn get_profile_route(&self, components: GistProfilePathComponent) -> String { 113 | self.profile.replace("{username}", components.username) 114 | } 115 | 116 | /// get gist route route with placeholders replaced with values provided. 117 | pub fn get_gist_route(&self, components: &PostCommentPath) -> String { 118 | self.view_gist 119 | .replace("{username}", &components.username) 120 | .replace("{gist}", &components.gist) 121 | } 122 | 123 | /// get post_comment route with placeholders replaced with values provided. 124 | pub fn get_post_comment_route(&self, components: &PostCommentPath) -> String { 125 | self.post_comment 126 | .replace("{username}", &components.username) 127 | .replace("{gist}", &components.gist) 128 | } 129 | 130 | /// get file routes with placeholders replaced with values provided. 131 | /// filename is auto-escaped using [urlencoding::encode] 132 | pub fn get_file_route(&self, components: &GetFilePath) -> String { 133 | self.get_file 134 | .replace("{username}", &components.username) 135 | .replace("{gist}", &components.gist) 136 | .replace("{file}", &urlencoding::encode(&components.file)) 137 | } 138 | } 139 | 140 | pub fn get_auth_middleware() -> Authentication { 141 | Authentication::with_identity(PAGES) 142 | } 143 | 144 | impl GetLoginRoute for Pages { 145 | fn get_login_route(&self, src: Option<&str>) -> String { 146 | if let Some(redirect_to) = src { 147 | format!( 148 | "{}?redirect_to={}", 149 | self.auth.login, 150 | urlencoding::encode(redirect_to) 151 | ) 152 | } else { 153 | self.auth.login.to_string() 154 | } 155 | } 156 | } 157 | 158 | #[cfg(test)] 159 | mod tests { 160 | use super::*; 161 | #[test] 162 | fn gist_route_substitution_works() { 163 | const NAME: &str = "bob"; 164 | const GIST: &str = "foo"; 165 | const FILE: &str = "README.md"; 166 | let get_profile = format!("/~{NAME}"); 167 | let view_gist = format!("/~{NAME}/{GIST}"); 168 | let post_comment = format!("/~{NAME}/{GIST}/comment"); 169 | let get_file = format!("/~{NAME}/{GIST}/contents/{FILE}"); 170 | 171 | let profile_component = GistProfilePathComponent { username: NAME }; 172 | 173 | assert_eq!(get_profile, PAGES.gist.get_profile_route(profile_component)); 174 | 175 | let profile_component = PostCommentPath { 176 | username: NAME.into(), 177 | gist: GIST.into(), 178 | }; 179 | 180 | assert_eq!(view_gist, PAGES.gist.get_gist_route(&profile_component)); 181 | 182 | let post_comment_path = PostCommentPath { 183 | gist: GIST.into(), 184 | username: NAME.into(), 185 | }; 186 | 187 | assert_eq!( 188 | post_comment, 189 | PAGES.gist.get_post_comment_route(&post_comment_path) 190 | ); 191 | 192 | let file_component = GetFilePath { 193 | username: NAME.into(), 194 | gist: GIST.into(), 195 | file: FILE.into(), 196 | }; 197 | assert_eq!(get_file, PAGES.gist.get_file_route(&file_component)); 198 | } 199 | } 200 | -------------------------------------------------------------------------------- /src/routes.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | use actix_web::web; 18 | 19 | use crate::api::v1; 20 | 21 | pub fn services(cfg: &mut web::ServiceConfig) { 22 | v1::services(cfg); 23 | crate::pages::services(cfg); 24 | crate::static_assets::services(cfg); 25 | } 26 | -------------------------------------------------------------------------------- /src/static_assets/filemap.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | use cache_buster::Files; 18 | 19 | pub struct FileMap { 20 | pub files: Files, 21 | } 22 | 23 | impl FileMap { 24 | #[allow(clippy::new_without_default)] 25 | pub fn new() -> Self { 26 | let map = include_str!("../cache_buster_data.json"); 27 | let files = Files::new(map); 28 | Self { files } 29 | } 30 | pub fn get(&self, path: impl AsRef) -> Option<&str> { 31 | let file_path = self.files.get_full_path(path); 32 | file_path.map(|file_path| &file_path[1..]) 33 | } 34 | } 35 | 36 | #[cfg(test)] 37 | mod tests { 38 | 39 | #[test] 40 | fn filemap_works() { 41 | let files = super::FileMap::new(); 42 | let css = files.get("./static/cache/css/main.css").unwrap(); 43 | println!("{}", css); 44 | assert!(css.contains("/assets/css/main")); 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/static_assets/mod.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | use actix_web::*; 18 | 19 | pub mod filemap; 20 | pub mod static_files; 21 | 22 | pub use filemap::FileMap; 23 | pub use routes::{Assets, ASSETS}; 24 | 25 | pub fn services(cfg: &mut web::ServiceConfig) { 26 | cfg.service(static_files::static_files); 27 | } 28 | 29 | pub mod routes { 30 | use lazy_static::lazy_static; 31 | use serde::*; 32 | 33 | use super::*; 34 | 35 | lazy_static! { 36 | pub static ref ASSETS: Assets = Assets::new(); 37 | } 38 | 39 | #[derive(Serialize)] 40 | /// Top-level routes data structure for V1 AP1 41 | pub struct Assets { 42 | /// Authentication routes 43 | pub css: &'static str, 44 | } 45 | 46 | impl Assets { 47 | /// create new instance of Routes 48 | pub fn new() -> Assets { 49 | Assets { 50 | css: &static_files::assets::CSS, 51 | } 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /src/static_assets/static_files.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | use std::borrow::Cow; 18 | 19 | use actix_web::body::BoxBody; 20 | use actix_web::{get, http::header, web, HttpResponse, Responder}; 21 | use mime_guess::from_path; 22 | use rust_embed::RustEmbed; 23 | 24 | use crate::CACHE_AGE; 25 | 26 | pub mod assets { 27 | use crate::FILES; 28 | use lazy_static::lazy_static; 29 | 30 | lazy_static! { 31 | pub static ref CSS: &'static str = FILES.get("./static/cache/css/main.css").unwrap(); 32 | } 33 | } 34 | 35 | #[derive(RustEmbed)] 36 | #[folder = "assets/"] 37 | struct Asset; 38 | 39 | fn handle_assets(path: &str) -> HttpResponse { 40 | match Asset::get(path) { 41 | Some(content) => { 42 | let body: BoxBody = match content.data { 43 | Cow::Borrowed(bytes) => BoxBody::new(bytes), 44 | Cow::Owned(bytes) => BoxBody::new(bytes), 45 | }; 46 | 47 | HttpResponse::Ok() 48 | .insert_header(header::CacheControl(vec![ 49 | header::CacheDirective::Public, 50 | header::CacheDirective::Extension("immutable".into(), None), 51 | header::CacheDirective::MaxAge(CACHE_AGE), 52 | ])) 53 | .content_type(from_path(path).first_or_octet_stream().as_ref()) 54 | .body(body) 55 | } 56 | None => HttpResponse::NotFound().body("404 Not Found"), 57 | } 58 | } 59 | 60 | #[get("/assets/{_:.*}")] 61 | pub async fn static_files(path: web::Path) -> impl Responder { 62 | handle_assets(&path) 63 | } 64 | 65 | #[cfg(test)] 66 | mod tests { 67 | use actix_web::http::StatusCode; 68 | use actix_web::test; 69 | 70 | use crate::data::Data; 71 | use crate::db::BoxDB; 72 | use crate::tests::*; 73 | use crate::*; 74 | 75 | use super::assets::CSS; 76 | 77 | #[actix_rt::test] 78 | async fn postgrest_static_files_works() { 79 | let (db, data) = sqlx_postgres::get_data().await; 80 | static_assets_work(data, db).await; 81 | } 82 | 83 | #[actix_rt::test] 84 | async fn sqlite_static_files_works() { 85 | let (db, data) = sqlx_sqlite::get_data().await; 86 | static_assets_work(data, db).await; 87 | } 88 | 89 | async fn static_assets_work(data: Arc, db: BoxDB) { 90 | let app = get_app!(data, db).await; 91 | 92 | let file = *CSS; 93 | let resp = get_request!(&app, file); 94 | assert_eq!(resp.status(), StatusCode::OK); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /src/tests.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | 18 | use std::env; 19 | pub use std::sync::Arc; 20 | 21 | //use actix_web::cookie::Cookie; 22 | use actix_web::test; 23 | use actix_web::{ 24 | body::{BoxBody, EitherBody}, 25 | dev::ServiceResponse, 26 | error::ResponseError, 27 | http::StatusCode, 28 | }; 29 | use serde::Serialize; 30 | 31 | use crate::api::v1::ROUTES; 32 | use crate::data::api::v1::auth::{Login, Register}; 33 | use crate::data::Data; 34 | pub use crate::db::BoxDB; 35 | use crate::db::{pg, sqlite}; 36 | use crate::errors::*; 37 | use crate::settings::DBType; 38 | use crate::*; 39 | 40 | pub mod sqlx_postgres { 41 | use super::*; 42 | 43 | pub async fn get_data() -> (BoxDB, Arc) { 44 | let url = env::var("POSTGRES_DATABASE_URL").unwrap(); 45 | let mut settings = Settings::new().unwrap(); 46 | settings.database.url = url.clone(); 47 | settings.database.database_type = DBType::Postgres; 48 | let db = pg::get_data(Some(settings.clone())).await; 49 | (db, Data::new(Some(settings))) 50 | } 51 | } 52 | 53 | pub mod sqlx_sqlite { 54 | use super::*; 55 | 56 | pub async fn get_data() -> (BoxDB, Arc) { 57 | let url = env::var("SQLITE_DATABASE_URL").unwrap(); 58 | let mut settings = Settings::new().unwrap(); 59 | settings.database.url = url.clone(); 60 | settings.database.database_type = DBType::Sqlite; 61 | let db = sqlite::get_data(Some(settings.clone())).await; 62 | (db, Data::new(Some(settings))) 63 | } 64 | } 65 | 66 | #[macro_export] 67 | macro_rules! get_cookie { 68 | ($resp:expr) => { 69 | $resp.response().cookies().next().unwrap().to_owned() 70 | }; 71 | } 72 | 73 | #[allow(dead_code, clippy::upper_case_acronyms)] 74 | pub struct FORM; 75 | 76 | #[macro_export] 77 | macro_rules! post_request { 78 | ($uri:expr) => { 79 | test::TestRequest::post().uri($uri) 80 | }; 81 | 82 | ($serializable:expr, $uri:expr) => { 83 | test::TestRequest::post() 84 | .uri($uri) 85 | .insert_header((actix_web::http::header::CONTENT_TYPE, "application/json")) 86 | .set_payload(serde_json::to_string($serializable).unwrap()) 87 | }; 88 | 89 | ($serializable:expr, $uri:expr, FORM) => { 90 | test::TestRequest::post().uri($uri).set_form($serializable) 91 | }; 92 | } 93 | 94 | #[macro_export] 95 | macro_rules! get_request { 96 | ($app:expr,$route:expr ) => { 97 | test::call_service(&$app, test::TestRequest::get().uri($route).to_request()).await 98 | }; 99 | 100 | ($app:expr, $route:expr, $cookies:expr) => { 101 | test::call_service( 102 | &$app, 103 | test::TestRequest::get() 104 | .uri($route) 105 | .cookie($cookies) 106 | .to_request(), 107 | ) 108 | .await 109 | }; 110 | } 111 | 112 | #[macro_export] 113 | macro_rules! delete_request { 114 | ($app:expr,$route:expr ) => { 115 | test::call_service(&$app, test::TestRequest::delete().uri($route).to_request()).await 116 | }; 117 | 118 | ($app:expr, $route:expr, $cookies:expr) => { 119 | test::call_service( 120 | &$app, 121 | test::TestRequest::delete() 122 | .uri($route) 123 | .cookie($cookies) 124 | .to_request(), 125 | ) 126 | .await 127 | }; 128 | } 129 | 130 | #[macro_export] 131 | macro_rules! get_app { 132 | ("APP", $settings:expr) => { 133 | actix_web::App::new() 134 | .app_data(crate::get_json_err()) 135 | .wrap(crate::get_identity_service($settings)) 136 | .wrap(actix_web::middleware::NormalizePath::new( 137 | actix_web::middleware::TrailingSlash::Trim, 138 | )) 139 | .configure(crate::routes::services) 140 | }; 141 | 142 | ($settings:ident) => { 143 | test::init_service(get_app!("APP", $settings)) 144 | }; 145 | ($data:expr, $db:expr) => { 146 | test::init_service( 147 | get_app!("APP", &$data.settings) 148 | .app_data(crate::DB::new($db.clone())) 149 | .app_data(crate::AppData::new($data.clone())), 150 | ) 151 | }; 152 | } 153 | 154 | impl Data { 155 | /// register and signin utility 156 | pub async fn register_and_signin( 157 | &self, 158 | db: &BoxDB, 159 | name: &str, 160 | email: &str, 161 | password: &str, 162 | ) -> (Login, ServiceResponse>) { 163 | self.register_test(db, name, email, password).await; 164 | self.signin_test(db, name, password).await 165 | } 166 | 167 | pub fn to_arc(&self) -> Arc { 168 | Arc::new(self.clone()) 169 | } 170 | 171 | /// register utility 172 | pub async fn register_test(&self, db: &BoxDB, name: &str, email: &str, password: &str) { 173 | let app = get_app!(self.to_arc(), db.clone()).await; 174 | 175 | // 1. Register 176 | let msg = Register { 177 | username: name.into(), 178 | password: password.into(), 179 | confirm_password: password.into(), 180 | email: Some(email.into()), 181 | }; 182 | let resp = 183 | test::call_service(&app, post_request!(&msg, ROUTES.auth.register).to_request()).await; 184 | // let resp_err: ErrorToResponse = test::read_body_json(resp).await; 185 | // panic!("{}", resp_err.error); 186 | assert_eq!(resp.status(), StatusCode::OK); 187 | } 188 | 189 | /// signin util 190 | pub async fn signin_test( 191 | &self, 192 | db: &BoxDB, 193 | name: &str, 194 | password: &str, 195 | ) -> (Login, ServiceResponse>) { 196 | let app = get_app!(self.to_arc(), db.clone()).await; 197 | 198 | // 2. signin 199 | let creds = Login { 200 | login: name.into(), 201 | password: password.into(), 202 | }; 203 | let signin_resp = 204 | test::call_service(&app, post_request!(&creds, ROUTES.auth.login).to_request()).await; 205 | assert_eq!(signin_resp.status(), StatusCode::OK); 206 | (creds, signin_resp) 207 | } 208 | 209 | /// pub duplicate test 210 | pub async fn bad_post_req_test( 211 | &self, 212 | db: &BoxDB, 213 | name: &str, 214 | password: &str, 215 | url: &str, 216 | payload: &T, 217 | err: ServiceError, 218 | ) { 219 | let (_, signin_resp) = self.signin_test(db, name, password).await; 220 | let cookies = get_cookie!(signin_resp); 221 | let app = get_app!(self.to_arc(), db.clone()).await; 222 | 223 | let resp = test::call_service( 224 | &app, 225 | post_request!(&payload, url) 226 | .cookie(cookies.clone()) 227 | .to_request(), 228 | ) 229 | .await; 230 | assert_eq!(resp.status(), err.status_code()); 231 | let resp_err: ErrorToResponse = test::read_body_json(resp).await; 232 | //println!("{}", txt.error); 233 | assert_eq!(resp_err.error, format!("{}", err)); 234 | } 235 | 236 | /// bad post req test without payload 237 | pub async fn bad_post_req_test_witout_payload( 238 | &self, 239 | db: &BoxDB, 240 | name: &str, 241 | password: &str, 242 | url: &str, 243 | err: ServiceError, 244 | ) { 245 | let (_, signin_resp) = self.signin_test(db, name, password).await; 246 | let app = get_app!(self.to_arc(), db.clone()).await; 247 | let cookies = get_cookie!(signin_resp); 248 | 249 | let resp = test::call_service( 250 | &app, 251 | post_request!(url).cookie(cookies.clone()).to_request(), 252 | ) 253 | .await; 254 | assert_eq!(resp.status(), err.status_code()); 255 | let resp_err: ErrorToResponse = test::read_body_json(resp).await; 256 | //println!("{}", resp_err.error); 257 | assert_eq!(resp_err.error, format!("{}", err)); 258 | } 259 | } 260 | -------------------------------------------------------------------------------- /src/utils.rs: -------------------------------------------------------------------------------- 1 | /* 2 | * Copyright (C) 2022 Aravinth Manivannan 3 | * 4 | * This program is free software: you can redistribute it and/or modify 5 | * it under the terms of the GNU Affero General Public License as 6 | * published by the Free Software Foundation, either version 3 of the 7 | * License, or (at your option) any later version. 8 | * 9 | * This program is distributed in the hope that it will be useful, 10 | * but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | * GNU Affero General Public License for more details. 13 | * 14 | * You should have received a copy of the GNU Affero General Public License 15 | * along with this program. If not, see . 16 | */ 17 | /// Get random string of specific length 18 | pub(crate) fn get_random(len: usize) -> String { 19 | use rand::{distributions::Alphanumeric, rngs::ThreadRng, thread_rng, Rng}; 20 | use std::iter; 21 | 22 | let mut rng: ThreadRng = thread_rng(); 23 | 24 | iter::repeat(()) 25 | .map(|()| rng.sample(Alphanumeric)) 26 | .map(char::from) 27 | .take(len) 28 | .collect::() 29 | } 30 | 31 | pub fn escape_spaces(name: &str) -> String { 32 | if name.contains(' ') { 33 | name.replace(' ', "\\ ") 34 | } else { 35 | name.to_string() 36 | } 37 | } 38 | 39 | #[cfg(test)] 40 | mod tests { 41 | use super::*; 42 | 43 | #[test] 44 | fn space_escape() { 45 | let space = "do re mi"; 46 | assert_eq!(&escape_spaces(space), ("do\\ re\\ mi")); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /static/cache/css/main.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | html { 7 | font-family: Georgia, "Times New Roman", Times, serif; 8 | color: #333; 9 | } 10 | 11 | h1, 12 | h2, 13 | h3, 14 | h4, 15 | h5, 16 | h6 { 17 | font-family: Arial, Helvetica, sans-serif; 18 | font-weight: 500; 19 | } 20 | 21 | h1 { 22 | font-size: 2rem; 23 | } 24 | 25 | h2 { 26 | font-size: 1.5rem; 27 | } 28 | 29 | body { 30 | width: 100%; 31 | min-height: 100vh; 32 | display: flex; 33 | flex-direction: column; 34 | justify-content: space-between; 35 | } 36 | 37 | .nav__container { 38 | margin-top: 5px; 39 | display: flex; 40 | flex-direction: row; 41 | 42 | box-sizing: border-box; 43 | width: 100%; 44 | align-items: center; 45 | height: 20px; 46 | } 47 | 48 | .nav__home-btn { 49 | font-family: monospace, monospace; 50 | font-weight: 500; 51 | margin: auto; 52 | margin-left: 5px; 53 | letter-spacing: 0.1rem; 54 | } 55 | 56 | a:hover { 57 | color: rgb(0, 86, 179); 58 | text-decoration: underline; 59 | } 60 | 61 | .nav__hamburger-menu { 62 | display: none; 63 | } 64 | 65 | .nav__spacer { 66 | flex: 3; 67 | margin: auto; 68 | } 69 | 70 | .nav__logo-container { 71 | display: inline-flex; 72 | text-decoration: none; 73 | } 74 | 75 | .nav__toggle { 76 | display: none; 77 | } 78 | 79 | .nav__logo { 80 | display: inline-flex; 81 | margin: auto; 82 | padding: 5px; 83 | width: 40px; 84 | } 85 | 86 | .nav__link-group { 87 | list-style: none; 88 | display: flex; 89 | flex-direction: row; 90 | align-items: center; 91 | align-self: center; 92 | margin: auto; 93 | text-align: center; 94 | } 95 | 96 | .nav__link-container { 97 | display: flex; 98 | padding: 0 10px; 99 | height: 100%; 100 | } 101 | 102 | .nav__link { 103 | text-decoration: none; 104 | } 105 | 106 | a { 107 | text-decoration: none; 108 | } 109 | 110 | a, 111 | a:visited { 112 | color: rgb(0, 86, 179); 113 | } 114 | 115 | main { 116 | flex: 4; 117 | width: 100%; 118 | margin: auto; 119 | 120 | display: flex; 121 | flex-direction: column; 122 | align-items: center; 123 | justify-content: space-evenly; 124 | } 125 | 126 | .auth__main { 127 | flex-direction: row; 128 | } 129 | 130 | .main { 131 | min-height: 80vh; 132 | align-items: center; 133 | display: flex; 134 | flex-direction: column; 135 | justify-content: center; 136 | flex: 2; 137 | align-items: center; 138 | } 139 | 140 | .login { 141 | flex: 1; 142 | display: flex; 143 | flex-direction: column; 144 | align-items: center; 145 | } 146 | 147 | .form { 148 | display: flex; 149 | flex-direction: column; 150 | width: 80%; 151 | margin: auto; 152 | padding: 0 10px; 153 | } 154 | 155 | .form__input { 156 | display: block; 157 | width: 100%; 158 | margin: 10px 0; 159 | padding: 5px 0; 160 | } 161 | 162 | .form__submit { 163 | width: 100%; 164 | display: block; 165 | margin: 10px 0; 166 | background-color: #b4345b; 167 | color: #fff; 168 | border: none; 169 | padding: 5px 0; 170 | cursor: pointer; 171 | } 172 | 173 | .form__submit:hover { 174 | background-color: #bb486b; 175 | } 176 | 177 | footer { 178 | display: block; 179 | font-size: 0.7rem; 180 | margin-bottom: 5px; 181 | } 182 | 183 | .footer__container { 184 | width: 90%; 185 | justify-content: space-between; 186 | margin: auto; 187 | display: flex; 188 | flex-direction: row; 189 | } 190 | 191 | .footer__column { 192 | list-style: none; 193 | display: flex; 194 | margin: auto 50px; 195 | } 196 | 197 | .footer__link-container { 198 | margin: 5px; 199 | } 200 | .license__conatiner { 201 | display: flex; 202 | } 203 | 204 | .footer__link { 205 | text-decoration: none; 206 | padding: 0 10px; 207 | } 208 | 209 | .footer__column-divider, 210 | .footer__column-divider--mobile-visible { 211 | font-weight: 500; 212 | opacity: 0.7; 213 | margin: 0 5px; 214 | } 215 | 216 | .footer__icon { 217 | margin: auto 5px; 218 | height: 20px; 219 | } 220 | 221 | .error_container { 222 | text-align: center; 223 | color: #c00; 224 | width: 100%; 225 | margin: 10px 0; 226 | } 227 | 228 | .gist__new { 229 | width: 80%; 230 | } 231 | 232 | .gist__file-content { 233 | display: block; 234 | width: 100%; 235 | margin: 10px 0; 236 | padding: 5px 0; 237 | height: 320px; 238 | } 239 | 240 | .gist__button-group { 241 | display: flex; 242 | width: 100%; 243 | align-items: center; 244 | justify-content: space-between; 245 | } 246 | 247 | .gist__button-container { 248 | flex: 1; 249 | max-width: 200px; 250 | } 251 | 252 | .form__submit--secondary { 253 | width: 100%; 254 | display: block; 255 | margin: 10px 0; 256 | border: none; 257 | padding: 5px 0; 258 | cursor: pointer; 259 | background-color: #e9e9ed; 260 | } 261 | 262 | .auth__demo-user__banner { 263 | margin: auto; 264 | margin-top: 5px; 265 | font-size: 0.8rem; 266 | text-align: center; 267 | } 268 | 269 | .auth__demo-user__cred { 270 | font-family: monospace, monospace; 271 | } 272 | 273 | .gist__container { 274 | display: flex; 275 | width: 100%; 276 | flex-direction: column; 277 | height: auto; 278 | } 279 | 280 | pre { 281 | font-size: 13px; 282 | font-family: Consolas, \"Liberation Mono\", Menlo, Courier, monospace; 283 | box-sizing: border-box; 284 | } 285 | 286 | .line { 287 | display: block; 288 | padding: 0px; 289 | cursor: pointer; 290 | } 291 | 292 | .line-number { 293 | color: #24292f; 294 | margin: 0; 295 | margin-right: 20px; 296 | min-width: 35px; 297 | padding: 0; 298 | width: 1%; 299 | padding-right: 10px; 300 | padding-left: 10px; 301 | display: inline-block; 302 | text-align: right; 303 | } 304 | 305 | details, 306 | summary { 307 | display: "inline"; 308 | margin: 0; 309 | padding: 0; 310 | } 311 | 312 | summary::marker { 313 | content: ""; 314 | margin: 0; 315 | padding: 0 5px; 316 | } 317 | 318 | .line-link { 319 | display: block; 320 | } 321 | 322 | pre { 323 | box-sizing: border-box; 324 | } 325 | 326 | .line { 327 | /* margin: -5px 5px; */ 328 | } 329 | 330 | .line-number { 331 | /* min-width: 50px; */ 332 | min-width: 35px; 333 | } 334 | 335 | .gist_file { 336 | border-radius: 6px; 337 | padding-bottom: 0px; 338 | margin: 10px 0; 339 | border: 1px solid #ddd; 340 | border-radius: 6px; 341 | box-sizing: border-box; 342 | overflow-x: scroll; 343 | } 344 | 345 | .gist__filename-container { 346 | padding: 8px; 347 | background: #eeee; 348 | margin-bottom: 10px; 349 | display: flex; 350 | justify-content: space-between; 351 | } 352 | 353 | .gist__filename-name { 354 | font-weight: 600; 355 | color: #333; 356 | } 357 | 358 | .gist__file-anchor { 359 | color: rgb(0, 86, 179); 360 | margin-right: 5px; 361 | } 362 | 363 | .gist__filename-raw { 364 | margin-left: 10px; 365 | } 366 | 367 | .gist__meta-container { 368 | flex: 1; 369 | max-height: 70px; 370 | min-height: 70px; 371 | display: flex; 372 | align-items: left; 373 | flex-direction: column; 374 | width: 80%; 375 | margin-top: 20px; 376 | } 377 | 378 | .gist__data-container { 379 | width: 70%; 380 | flex: 2; 381 | margin: auto; 382 | } 383 | 384 | .gist__name { 385 | margin-left: 60px; 386 | display: flex; 387 | } 388 | 389 | .gist__description { 390 | margin-left: 60px; 391 | } 392 | 393 | .gist__visibility { 394 | display: inline-block; 395 | padding: 0 7px; 396 | font-size: 12px; 397 | font-weight: 500; 398 | line-height: 18px; 399 | white-space: nowrap; 400 | border: 1px solid transparent; 401 | border-top-color: transparent; 402 | border-right-color: transparent; 403 | border-bottom-color: transparent; 404 | border-left-color: transparent; 405 | border-radius: 2em; 406 | background: #ffa500; 407 | margin: auto 0px; 408 | } 409 | 410 | .gist__name-text { 411 | display: inline; 412 | width: auto; 413 | } 414 | 415 | .gist__comment-container { 416 | width: 70%; 417 | margin: auto; 418 | } 419 | 420 | .gist__comment-form { 421 | width: 100%; 422 | } 423 | 424 | .form__label { 425 | display: inline-block; 426 | width: 100%; 427 | } 428 | 429 | .comment { 430 | width: 100%; 431 | min-height: 100px; 432 | } 433 | 434 | .gist__comment-content { 435 | width: 100%; 436 | border: 1px solid #ddd; 437 | padding: 5px; 438 | } 439 | 440 | .comment__meta { 441 | width: 100%; 442 | display: flex; 443 | justify-content: space-between; 444 | } 445 | 446 | .comment__container { 447 | width: 95%; 448 | margin: 20px auto; 449 | } 450 | 451 | .comment__created { 452 | font-size: 0.8rem; 453 | } 454 | 455 | .comment__owner { 456 | font-size: 0.8rem; 457 | } 458 | -------------------------------------------------------------------------------- /templates/components/base.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Gists 8 | {% block title %} {% endblock %} 9 | 10 | 11 |
{% block nav %} {% endblock %}
12 |
{% block main %} {% endblock %}
13 | {% include "footer" %} 14 | 15 | 16 | -------------------------------------------------------------------------------- /templates/components/comments/index.html: -------------------------------------------------------------------------------- 1 |
2 | {% if "comments" in payload %} 3 | {% for comment in payload.comments %} 4 | {% set comment_id = "comment" ~ comment.id %} 5 |
6 | 10 |

{{ comment.comment }}

11 |
12 | {% endfor %} 13 | {% endif %} 14 | {% include "gist_comment_input" %} 15 |
16 | -------------------------------------------------------------------------------- /templates/components/comments/new.html: -------------------------------------------------------------------------------- 1 |
2 | 15 |
16 |
17 | 23 |
24 |
25 | 26 |
27 |
28 |
29 | -------------------------------------------------------------------------------- /templates/components/error.html: -------------------------------------------------------------------------------- 1 | {% if error %} 2 |
3 |

ERROR: {{ error.title }}

4 |

{{ error.reason }}

5 |
6 | {% endif %} 7 | -------------------------------------------------------------------------------- /templates/components/footer.html: -------------------------------------------------------------------------------- 1 | 47 | -------------------------------------------------------------------------------- /templates/components/nav/auth.html: -------------------------------------------------------------------------------- 1 | 29 | -------------------------------------------------------------------------------- /templates/components/nav/base.html: -------------------------------------------------------------------------------- 1 | 19 | -------------------------------------------------------------------------------- /templates/components/nav/pub.html: -------------------------------------------------------------------------------- 1 | 27 | -------------------------------------------------------------------------------- /templates/pages/auth/base.html: -------------------------------------------------------------------------------- 1 | {% extends 'base' %} 2 | {% block title %}{% block title_name %} {% endblock %} | GitPad{% endblock %} 3 | 4 | {% block nav %} {% include "pub_nav" %} {% endblock %} 5 | {% block main %} 6 |
7 |
8 |
9 |

GitPad

10 |

11 | Welcome to GitPad - A libre pastebin service! Currently implemented 12 | features include: 13 |

    14 |
  • Git-powered version history
  • 15 |
  • Comments on gists
  • 16 |
17 |

18 |
19 |
20 | 23 |
24 | {% endblock %} 25 | -------------------------------------------------------------------------------- /templates/pages/auth/demo.html: -------------------------------------------------------------------------------- 1 | {% if footer.settings.allow_demo %} 2 |

3 | Try Kaizen without joining
4 | 5 | user: {{ footer.demo_user }} 6 | 7 | 8 | password: {{ footer.demo_password }} 9 | 10 |

11 | {% endif %} 12 | -------------------------------------------------------------------------------- /templates/pages/auth/login.html: -------------------------------------------------------------------------------- 1 | {% extends 'authbase' %} 2 | {% block login %} 3 |

Sign In

4 |
5 | {% include "error_comp" %} 6 | 20 | 21 | 34 |
35 | Forgot password? 36 | 37 |
38 |
39 | 40 |

41 | New to GitPad? 42 | Create an account 43 |

44 | {% include "demo_banner" %} 45 | {% endblock %} 46 | -------------------------------------------------------------------------------- /templates/pages/auth/register.html: -------------------------------------------------------------------------------- 1 | {% extends 'authbase' %} 2 | {% block title_name %}Sign Up {% endblock %} 3 | {% block login %} 4 |

Sign Up

5 |
6 | {% include "error_comp" %} 7 | 21 | 22 | 35 | 36 | 49 | 50 | 63 |
64 | Forgot password? 65 | 66 |
67 |
68 | 69 |

70 | Already have an account? 71 | Login 72 |

73 | {% include "demo_banner" %} 74 | {% endblock %} 75 | -------------------------------------------------------------------------------- /templates/pages/gists/base.html: -------------------------------------------------------------------------------- 1 | {% extends 'base' %} 2 | {% block title %}{% block title_name %} {% endblock %} | GitPad{% endblock %} 3 | 4 | {% block nav %} {% include "auth_nav" %} {% endblock %} 5 | {% block main %} 6 | {% block gist_main %} {% endblock %} 7 | {% endblock %} 8 | -------------------------------------------------------------------------------- /templates/pages/gists/explore.html: -------------------------------------------------------------------------------- 1 | {% extends 'gistbase' %} 2 | {% block gist_main %} 3 |

Place holder

4 | {% endblock %} 5 | -------------------------------------------------------------------------------- /templates/pages/gists/new/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'gistbase' %} 2 | {% block gist_main %} 3 |
4 | {% include "error_comp" %} 5 | 18 | {% for fieldname in fieldnames%} 19 | {% set content = payload | nth(n=(loop.index - 1)) %} 20 | 21 | 33 | 42 | 43 | {% endfor %} 44 |
45 | 56 | 57 | 67 | 68 | 78 |
79 |
80 |
81 | 87 |
88 |
89 | 90 |
91 |
92 |
93 | {% endblock %} 94 | -------------------------------------------------------------------------------- /templates/pages/gists/view/_filename.html: -------------------------------------------------------------------------------- 1 |
2 | #{{ payload_file.f.filename }} 3 | 4 | Link 5 | Raw 6 | 7 |
8 | -------------------------------------------------------------------------------- /templates/pages/gists/view/_meta.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | {% if "description" in payload.gist %} 6 |

{{ payload.gist.description}}

7 | {% endif %} 8 |
9 | -------------------------------------------------------------------------------- /templates/pages/gists/view/_text.html: -------------------------------------------------------------------------------- 1 |
2 | {% include "gist_filename" %} 3 | {% if "text" in payload_file.f.content.file %} 4 | {{ payload_file.f.content.file.text }} 5 | {% endif %} 6 |
7 | 8 | -------------------------------------------------------------------------------- /templates/pages/gists/view/index.html: -------------------------------------------------------------------------------- 1 | {% extends 'gistbase' %} 2 | {% block gist_main %} 3 | {% include "error_comp" %} 4 |
5 | {% if payload %} 6 | {% if "gist" in payload %} 7 | {% include "gist_meta" %} 8 |
9 | {% for payload_file in payload.gist.files %} 10 | {% if "file" in payload_file.f.content %} 11 | {% include "gist_textfile" %} 12 | {% elif "dir" in payload_file.f.content %} 13 | {% for payload_file in payload_file.f.content.dir %} 14 | {% if "file" in payload_file.f.content %} 15 | {% include "gist_textfile" %} 16 | {% endif %} 17 | {% endfor %} 18 | {% endif %} 19 | {% endfor %} 20 |
21 | {% endif %} 22 | {% endif %} 23 |
24 | {% include "gist_comments" %} 25 | {% endblock %} 26 | -------------------------------------------------------------------------------- /website/.gitignore: -------------------------------------------------------------------------------- 1 | public 2 | static/doc 3 | -------------------------------------------------------------------------------- /website/bin/zola: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/realaravinth/gitpad/4127f923aa174ecebed93898dd39c1b21b6b2605/website/bin/zola -------------------------------------------------------------------------------- /website/config.toml: -------------------------------------------------------------------------------- 1 | # The URL the site will be built for 2 | base_url = "https://gitpad.org" 3 | 4 | # Whether to automatically compile all Sass files in the sass directory 5 | compile_sass = true 6 | 7 | # Whether to build a search index to be used later on by a JavaScript library 8 | build_search_index = true 9 | 10 | [markdown] 11 | # Whether to do syntax highlighting 12 | # Theme can be customised by setting the `highlight_theme` variable to a theme supported by Zola 13 | highlight_code = true 14 | 15 | [extra] 16 | # Put all your custom variables here 17 | -------------------------------------------------------------------------------- /website/static/css/main.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | } 5 | 6 | html { 7 | font-family: Georgia, "Times New Roman", Times, serif; 8 | color: #333; 9 | } 10 | 11 | h1, 12 | h2, 13 | h3, 14 | h4, 15 | h5, 16 | h6 { 17 | font-family: Arial, Helvetica, sans-serif; 18 | font-weight: 500; 19 | } 20 | 21 | h1 { 22 | font-size: 2rem; 23 | } 24 | 25 | h2 { 26 | font-size: 1.5rem; 27 | } 28 | 29 | body { 30 | width: 100%; 31 | min-height: 100vh; 32 | display: flex; 33 | flex-direction: column; 34 | justify-content: space-between; 35 | } 36 | 37 | .nav__container { 38 | margin-top: 5px; 39 | display: flex; 40 | flex-direction: row; 41 | 42 | box-sizing: border-box; 43 | width: 100%; 44 | align-items: center; 45 | height: 20px; 46 | } 47 | 48 | .nav__home-btn { 49 | font-family: monospace, monospace; 50 | font-weight: 500; 51 | margin: auto; 52 | margin-left: 5px; 53 | letter-spacing: 0.1rem; 54 | } 55 | 56 | a:hover { 57 | color: rgb(0, 86, 179); 58 | text-decoration: underline; 59 | } 60 | 61 | .nav__hamburger-menu { 62 | display: none; 63 | } 64 | 65 | .nav__spacer { 66 | flex: 3; 67 | margin: auto; 68 | } 69 | 70 | .nav__logo-container { 71 | display: inline-flex; 72 | text-decoration: none; 73 | } 74 | 75 | .nav__toggle { 76 | display: none; 77 | } 78 | 79 | .nav__logo { 80 | display: inline-flex; 81 | margin: auto; 82 | padding: 5px; 83 | width: 40px; 84 | } 85 | 86 | .nav__link-group { 87 | list-style: none; 88 | display: flex; 89 | flex-direction: row; 90 | align-items: center; 91 | align-self: center; 92 | margin: auto; 93 | text-align: center; 94 | } 95 | 96 | .nav__link-container { 97 | display: flex; 98 | padding: 0 10px; 99 | height: 100%; 100 | } 101 | 102 | .nav__link { 103 | text-decoration: none; 104 | } 105 | 106 | a { 107 | text-decoration: none; 108 | } 109 | 110 | a, 111 | a:visited { 112 | color: rgb(0, 86, 179); 113 | } 114 | 115 | main { 116 | flex: 4; 117 | width: 100%; 118 | margin: auto; 119 | display: flex; 120 | align-items: center; 121 | justify-content: space-evenly; 122 | } 123 | 124 | .main { 125 | min-height: 80vh; 126 | align-items: center; 127 | display: flex; 128 | flex-direction: column; 129 | justify-content: center; 130 | flex: 2; 131 | align-items: center; 132 | } 133 | 134 | .login { 135 | flex: 1; 136 | display: flex; 137 | flex-direction: column; 138 | align-items: center; 139 | } 140 | 141 | .form { 142 | display: flex; 143 | flex-direction: column; 144 | width: 80%; 145 | margin: auto; 146 | padding: 0 10px; 147 | } 148 | 149 | .form__input { 150 | display: block; 151 | width: 100%; 152 | margin: 10px 0; 153 | padding: 5px 0; 154 | } 155 | 156 | .form__submit { 157 | width: 100%; 158 | display: block; 159 | margin: 10px 0; 160 | background-color: #b4345b; 161 | color: #fff; 162 | border: none; 163 | padding: 5px 0; 164 | cursor: pointer; 165 | } 166 | 167 | .form__submit:hover { 168 | background-color: #bb486b; 169 | } 170 | 171 | footer { 172 | display: block; 173 | font-size: 0.7rem; 174 | margin-bottom: 5px; 175 | } 176 | 177 | .footer__container { 178 | width: 90%; 179 | justify-content: space-between; 180 | margin: auto; 181 | display: flex; 182 | flex-direction: row; 183 | } 184 | 185 | .footer__column { 186 | list-style: none; 187 | display: flex; 188 | margin: auto 50px; 189 | } 190 | 191 | .footer__link-container { 192 | margin: 5px; 193 | } 194 | .license__conatiner { 195 | display: flex; 196 | } 197 | 198 | .footer__link { 199 | text-decoration: none; 200 | padding: 0 10px; 201 | } 202 | 203 | .footer__column-divider, 204 | .footer__column-divider--mobile-visible { 205 | font-weight: 500; 206 | opacity: 0.7; 207 | margin: 0 5px; 208 | } 209 | 210 | .footer__icon { 211 | margin: auto 5px; 212 | height: 20px; 213 | } 214 | 215 | .error_container { 216 | text-align: center; 217 | color: #c00; 218 | width: 100%; 219 | margin: 10px 0; 220 | } 221 | 222 | .gist__new { 223 | width: 80%; 224 | } 225 | 226 | .gist__file-content { 227 | display: block; 228 | width: 100%; 229 | margin: 10px 0; 230 | padding: 5px 0; 231 | height: 320px; 232 | } 233 | 234 | .gist__button-group { 235 | display: flex; 236 | width: 100%; 237 | align-items: center; 238 | justify-content: space-between; 239 | } 240 | 241 | .gist__button-container { 242 | flex: 1; 243 | max-width: 200px; 244 | } 245 | 246 | .form__submit--secondary { 247 | width: 100%; 248 | display: block; 249 | margin: 10px 0; 250 | border: none; 251 | padding: 5px 0; 252 | cursor: pointer; 253 | background-color: #e9e9ed; 254 | } 255 | 256 | .auth__demo-user__banner { 257 | margin: auto; 258 | margin-top: 5px; 259 | font-size: 0.8rem; 260 | text-align: center; 261 | } 262 | 263 | .auth__demo-user__cred { 264 | font-family: monospace, monospace; 265 | } 266 | 267 | .btn { 268 | padding: 10px; 269 | border: none; 270 | background-color: #b4345b; 271 | margin-top: 20px; 272 | margin-right: 10px; 273 | color: #fff; 274 | width: 150px; 275 | height: 40px; 276 | } 277 | -------------------------------------------------------------------------------- /website/templates/footer.html: -------------------------------------------------------------------------------- 1 | 36 | -------------------------------------------------------------------------------- /website/templates/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 8 | Home | GitPad 9 | 10 | 11 |
12 | 20 |
21 | {% include "footer.html" %} 22 | 23 | 24 | --------------------------------------------------------------------------------