Compare commits
41 Commits
hide_stuff
...
v0.1.3
Author | SHA1 | Date | |
---|---|---|---|
13fea4748e | |||
84f51bd2f3
|
|||
9eec2a2671
|
|||
8e0d076e67 | |||
31d3cb5f27
|
|||
a1e748c0a9 | |||
4f2c810a2f
|
|||
3ce6daea15 | |||
16fd963c8d
|
|||
6d3cd0566e
|
|||
42fd6f186f | |||
98699fcdeb | |||
e2c8035b99
|
|||
7bc9725f63 | |||
eb12a979b6
|
|||
1e33c89223 | |||
784fe9a438 | |||
3e2857dcb1 | |||
0f6cbfa52a
|
|||
eae61ea52e | |||
dc3bc810bb
|
|||
f57e542126
|
|||
0752725119
|
|||
80015040f1
|
|||
19045da969 | |||
49ad457b95 | |||
6847216f86 | |||
8b5ca214d3 | |||
634d056823 | |||
dd4290c51c | |||
587ce73f2d | |||
b4b76576da | |||
3af9ba8f5c | |||
0a34423f5a | |||
53a423def3 | |||
bd4a897637 | |||
2e0e116934 | |||
985d375459 | |||
74975936cb | |||
8943f49bfe | |||
a8b38b74ff |
16
.drone.yml
16
.drone.yml
@@ -14,14 +14,14 @@ steps:
|
||||
- cargo test --workspace -- --nocapture
|
||||
|
||||
# Test the nightly toolchain
|
||||
- name: test_nightly
|
||||
image: "artixlinux/artixweb-packages-ci:latest"
|
||||
environment:
|
||||
RUSTUP_TOOLCHAIN: nightly
|
||||
commands:
|
||||
- export CARGO_HOME="/drone/src/cargo"
|
||||
- rustup --version && rustc --version && cargo --version
|
||||
- cargo test --workspace -- --nocapture
|
||||
#- name: test_nightly
|
||||
# image: "artixlinux/artixweb-packages-ci:latest"
|
||||
# environment:
|
||||
# RUSTUP_TOOLCHAIN: nightly
|
||||
# commands:
|
||||
# - export CARGO_HOME="/drone/src/cargo"
|
||||
# - rustup --version && rustc --version && cargo --version
|
||||
# - cargo test --workspace -- --nocapture
|
||||
|
||||
# Check rust code format and pass exhaustive lint
|
||||
- name: format_and_lint
|
||||
|
117
.gitea/workflows/build-docker.yml
Normal file
117
.gitea/workflows/build-docker.yml
Normal file
@@ -0,0 +1,117 @@
|
||||
name: Docker Image CI
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
branches: [ master ]
|
||||
push:
|
||||
branches: [ master ]
|
||||
tags:
|
||||
- 'v*'
|
||||
pull_request:
|
||||
branches: [ master ]
|
||||
|
||||
|
||||
jobs:
|
||||
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
timeout-minutes: 90
|
||||
strategy:
|
||||
fail-fast: true
|
||||
env:
|
||||
REGISTRY: gitea.artixlinux.org
|
||||
DH_REGISTRY: docker.io
|
||||
REPO_ORG: ${{ gitea.repository_owner }}
|
||||
DH_ORG: artixlinux
|
||||
IMAGE_NAME: artixweb_packages
|
||||
DH_IMAGE_NAME: artixweb-packages
|
||||
ABSOLUTE_IMAGE: ${{ env.REGISTRY }}/${{ env.REPO_ORG }}/${{ env.IMAGE_NAME }}
|
||||
ABSOLUTE_DH_IMAGE: ${{ env.DH_REGISTRY }}/${{ env.DH_ORG }}/${{ env.DH_IMAGE_NAME }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: https://github.com/actions/checkout@v4
|
||||
- name: Set up docker
|
||||
run: curl -fsSL https://get.docker.com | sh
|
||||
- name: Set up QEMU
|
||||
uses: https://github.com/docker/setup-qemu-action@v2
|
||||
- name: Set up Docker Buildx
|
||||
id: buildx
|
||||
uses: https://github.com/docker/setup-buildx-action@v2
|
||||
with:
|
||||
install: true
|
||||
|
||||
- name: Log in to the Container registry
|
||||
uses: https://github.com/docker/login-action@v2
|
||||
if: startsWith(gitea.ref, 'refs/tags/v')
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: corysanin
|
||||
password: ${{ secrets.PAT }}
|
||||
|
||||
- name: Log in to the Docker Hub
|
||||
uses: https://github.com/docker/login-action@v2
|
||||
if: startsWith(gitea.ref, 'refs/tags/v')
|
||||
with:
|
||||
registry: ${{ env.DH_REGISTRY }}
|
||||
username: ${{ secrets.DOCKERHUB_USER }}
|
||||
password: ${{ secrets.DOCKERHUB }}
|
||||
|
||||
- name: Define metadata variables
|
||||
if: startsWith(gitea.ref, 'refs/tags/v')
|
||||
run: |
|
||||
sed -i "s/LABEL Version=.*/ARG version=${{ gitea.ref_name }}/" Dockerfile
|
||||
cat Dockerfile
|
||||
|
||||
- name: Extract metadata for release Docker image
|
||||
if: startsWith(gitea.ref, 'refs/tags/v')
|
||||
id: meta
|
||||
uses: https://github.com/docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.ABSOLUTE_DH_IMAGE }}
|
||||
# ${{ env.ABSOLUTE_IMAGE }}
|
||||
##unexpected status from PUT request: 413 Request Entity Too Large
|
||||
tags: |
|
||||
type=raw,value=latest
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
|
||||
- name: Extract metadata for develop Docker image
|
||||
if: "!startsWith(gitea.ref, 'refs/tags/v')"
|
||||
id: meta-develop
|
||||
uses: https://github.com/docker/metadata-action@v5
|
||||
with:
|
||||
images: |
|
||||
${{ env.ABSOLUTE_IMAGE }}
|
||||
tags: |
|
||||
type=ref,enable=true,priority=600,prefix=,suffix=,event=branch
|
||||
type=ref,enable=true,priority=600,prefix=pr-,suffix=,event=pr
|
||||
|
||||
- name: Build and push release Docker image
|
||||
if: startsWith(gitea.ref, 'refs/tags/v')
|
||||
uses: https://github.com/docker/build-push-action@v4
|
||||
with:
|
||||
file: Dockerfile.ci
|
||||
target: deploy
|
||||
pull: true
|
||||
push: true
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
platforms: linux/amd64
|
||||
|
||||
- name: Build develop Docker image
|
||||
if: "!startsWith(gitea.ref, 'refs/tags/v')"
|
||||
uses: https://github.com/docker/build-push-action@v4
|
||||
with:
|
||||
file: Dockerfile.ci
|
||||
target: deploy
|
||||
pull: true
|
||||
push: false
|
||||
tags: ${{ steps.meta-develop.outputs.tags }}
|
||||
labels: ${{ steps.meta-develop.outputs.labels }}
|
||||
platforms: linux/amd64
|
5
.gitignore
vendored
5
.gitignore
vendored
@@ -1,6 +1,9 @@
|
||||
# Cargo stuff
|
||||
target/
|
||||
*Cargo.lock
|
||||
*/Cargo.lock
|
||||
|
||||
# editor stuff
|
||||
.vscode/
|
||||
|
||||
# diesel
|
||||
.env
|
||||
|
2627
Cargo.lock
generated
Normal file
2627
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
58
Dockerfile.ci
Normal file
58
Dockerfile.ci
Normal file
@@ -0,0 +1,58 @@
|
||||
# This file is part of Artix Web Packages. See LICENSE file for details
|
||||
# Copyright 2022 - Artix Linux
|
||||
FROM artixlinux/artixlinux:base as build
|
||||
|
||||
# update pacman
|
||||
# install clang and pkg-config
|
||||
# install rustup and put $HOME/.cargo/bin in the $PATH
|
||||
# install stable and nightly toolchains
|
||||
# install rustfmt-preview and clippy-preview in the nightly toolchain
|
||||
RUN set -x \
|
||||
&& pacman -Syu --noconfirm \
|
||||
&& pacman -S --noconfirm clang pkgconf \
|
||||
&& pacman -Scc --noconfirm \
|
||||
&& curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \
|
||||
&& export PATH="$HOME/.cargo/bin:$PATH" \
|
||||
&& rustup toolchain add stable nightly \
|
||||
&& rustup component add --toolchain nightly rustfmt-preview clippy-preview
|
||||
|
||||
# Update PATH in the container
|
||||
ENV PATH="/root/.cargo/bin:${PATH}"
|
||||
ENV RUSTUP_TOOLCHAIN=stable
|
||||
|
||||
RUN pacman -Sy --noconfirm postgresql-libs
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
COPY . ./
|
||||
|
||||
RUN cargo build --release
|
||||
RUN set -x \
|
||||
&& install -vd /usr/share/artixweb_packages/assets \
|
||||
&& cp -rv crates/artixweb-packages/assets/* /usr/share/artixweb_packages/assets/ \
|
||||
&& install -vd /usr/share/artixweb_packages/templates \
|
||||
&& cp -v crates/artixweb-packages/templates/* /usr/share/artixweb_packages/templates/ \
|
||||
&& install -vDm 755 target/release/artixweb_packages -t "/usr/share/artixweb_packages"
|
||||
|
||||
FROM artixlinux/artixlinux:base AS deploy
|
||||
|
||||
LABEL Maintainer="damnwidget@artixlinux.org"
|
||||
LABEL Name="Artix Web Packages Container"
|
||||
LABEL Version="Develop"
|
||||
|
||||
EXPOSE 1936/tcp
|
||||
|
||||
RUN pacman -Sy --noconfirm postgresql-libs \
|
||||
&& set -x \
|
||||
&& pacman -Syu --noconfirm \
|
||||
&& pacman -Scc --noconfirm \
|
||||
&& printf "[lib32]\nInclude = /etc/pacman.d/mirrorlist\n" >> /etc/pacman.conf \
|
||||
&& printf "[system-gremlins]\nInclude = /etc/pacman.d/mirrorlist\n" >> /etc/pacman.conf \
|
||||
&& printf "[world-gremlins]\nInclude = /etc/pacman.d/mirrorlist\n" >> /etc/pacman.conf \
|
||||
&& printf "[galaxy-gremlins]\nInclude = /etc/pacman.d/mirrorlist\n" >> /etc/pacman.conf \
|
||||
&& printf "[lib32-gremlins]\nInclude = /etc/pacman.d/mirrorlist\n" >> /etc/pacman.conf \
|
||||
&& pacman -Sy --noconfirm
|
||||
|
||||
WORKDIR /usr/share/artixweb_packages
|
||||
COPY --from=0 /usr/share/artixweb_packages ./
|
||||
CMD ["./artixweb_packages"]
|
38
Dockerfile.install
Normal file
38
Dockerfile.install
Normal file
@@ -0,0 +1,38 @@
|
||||
# This file is part of Artix Web Packages. See LICENSE file for details
|
||||
# Copyright 2022 - Artix Linux
|
||||
FROM artixlinux/artixweb-packages-ci:latest AS build
|
||||
|
||||
LABEL Maintainer="damnwidget@artixlinux.org"
|
||||
LABEL Name="Artix Web Packages Container"
|
||||
LABEL Version="0.1.0"
|
||||
|
||||
RUN pacman -Sy --noconfirm postgresql-libs
|
||||
|
||||
WORKDIR /build
|
||||
|
||||
COPY . ./
|
||||
|
||||
ENV RUSTUP_TOOLCHAIN=stable
|
||||
|
||||
RUN cargo build --release
|
||||
RUN set -x \
|
||||
&& install -vd /usr/share/artixweb_packages/assets \
|
||||
&& cp -rv crates/artixweb-packages/assets/* /usr/share/artixweb_packages/assets/ \
|
||||
&& install -vd /usr/share/artixweb_packages/templates \
|
||||
&& cp -v crates/artixweb-packages/templates/* /usr/share/artixweb_packages/templates/ \
|
||||
&& install -vDm 755 target/release/artixweb_packages -t "/usr/share/artixweb_packages"
|
||||
|
||||
FROM artixlinux/base:latest AS artix
|
||||
|
||||
EXPOSE 1936/tcp
|
||||
|
||||
RUN pacman -Sy --noconfirm postgresql-libs
|
||||
RUN pacman -Syu --noconfirm
|
||||
RUN pacman -Scc --noconfirm
|
||||
RUN set -x \
|
||||
&& echo -e "[lib32]\nInclude = /etc/pacman.d/mirrorlist" >> /etc/pacman.conf \
|
||||
&& echo -e "[universe]\nServer = https://universe.artixlinux.org/\$arch" >> /etc/pacman.conf
|
||||
|
||||
WORKDIR /usr/share/artixweb_packages
|
||||
COPY --from=0 /usr/share/artixweb_packages ./
|
||||
CMD ["./artixweb_packages"]
|
187
README.md
Normal file
187
README.md
Normal file
@@ -0,0 +1,187 @@
|
||||
# ArtixWeb Packages
|
||||
|
||||
ArtixWeb Packages contains the official Artix repo packages web application crates and service used to power [artix packages][1] website.
|
||||
|
||||
## Installation
|
||||
|
||||
The easiest way of installing the solution is by using the provided [Installation Dockerfile][2] to build a docker image that you can deploy into a Docker Swarm, Docker Composer or any other container orchestrator of your choice.
|
||||
|
||||
> There are composer and swarm configuration templates in the [example_files][3] directory that you can use to base your configuration on, those examples are fully documented on their own
|
||||
|
||||
Below you can find a detailed step by step guide on how to do so
|
||||
|
||||
### Cloning the repository
|
||||
|
||||
First we need to clone this repository locally, for that we will need the [git package][git] from artix's world repository if we do not have it yet
|
||||
|
||||
> During this document `doas` will be used instead of `sudo`, obviously, you can use whatever fits you
|
||||
|
||||
```
|
||||
cd ~
|
||||
mkdir -p projects/artix
|
||||
git clone https://gitea.artixlinux.org/artix/artixweb_packages.git projects/artix/artixweb_packages
|
||||
```
|
||||
|
||||
The command above will make a local clone of this repository in `$HOME/projects/artix/artixweb_packages`, if you prefer your copy to reside in some other location in your har drive just clone it wherever you like instead
|
||||
|
||||
### Building the Docker Image
|
||||
|
||||
For building the Docker image we need the [docker package][docker] from the galaxy repository to be installed, proceed to install it if you don't have it yet
|
||||
|
||||
```
|
||||
cd ~/projects/artix/artixweb_packages
|
||||
docker build -f Dockerfile.install -t artixweb-packages:latest .
|
||||
```
|
||||
|
||||
The command above will generate a new Artix based container with the latest version of the application ready to be deployed on any Docker server
|
||||
|
||||
### Deploying the Service
|
||||
|
||||
Now that we have a container we will be able to deploy it to any Docker capable service. The service has a few requirements that must be spinned up along side, how do you choose to architecture the whole solution is up to you, follow the example `docker compose` file in the [example_files][3] directory to learn more about the service dependencies.
|
||||
|
||||
To start the provided composer example, just cd into `example_files` and run:
|
||||
|
||||
```
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
> Refer to the `exmple_files/docker-compose.yml` file for further explanations, it is fully documented
|
||||
|
||||
#### Environment Variables and Options
|
||||
|
||||
The service is configured just passing options to its command line, with environment variables or with a mix of both
|
||||
|
||||
> **note**: some configuration options are only available as environment variables
|
||||
|
||||
```
|
||||
Artix Linux Packages Information Website
|
||||
|
||||
USAGE:
|
||||
artixweb_packages [OPTIONS] --key <SESSION_KEY> --smtp_user <SMTP_USER> --smtp_pwd <SMTP_PWD> --smtp_relay <SMTP_RELAY>
|
||||
|
||||
OPTIONS:
|
||||
-b, --bind <BIND_ADDRESS> [env: BIND_ADDRESS=] [default: 127.0.0.1]
|
||||
-d, --domain <DOMAIN> [default: localhost]
|
||||
-h, --help Print help information
|
||||
-k, --key <SESSION_KEY> [env: SESSION_KEY=]
|
||||
-p, --port <PORT> [default: 1936]
|
||||
--smtp_pwd <SMTP_PWD> [env: SMTP_PWD=]
|
||||
--smtp_relay <SMTP_RELAY> [env: SMTP_RELAY=]
|
||||
--smtp_user <SMTP_USER> [env: SMTP_USER=]
|
||||
-u, --databaseurl <DATABASE_URL> [env: DATABASE_URL=] [default: localhost]
|
||||
-v, --verbose
|
||||
-V, --version Print version information
|
||||
```
|
||||
|
||||
By default, the service binds to the loopback interface, this means that it will not allow connections from outside the Docker container, so we have to bind it to `0.0.0.0`
|
||||
|
||||
> This binds by default on `loopback` in case users prefer to run the service in real hardware behind a `nginx` proxy or similar
|
||||
|
||||
The `-k` and `--key` options or `SEESION_KEY` environment variable is used to provide of a 256 bits key (as an hex string) that will be used by the application to cipher and sign session cookies
|
||||
|
||||
> Recommended to use Docker secrets for this or any other vaulting solution
|
||||
|
||||
The `-u` and `--databaseurl` or `DATABASE_URL` environment variable is used to provide a valid [PostgreSQL][postgres] database URL for the service to connect to, this database is used to store package metadata and package flags information as well as the local service users. An example of `DATABASE_URL` could be `postgres://user:password@db.url.com/artix_packages`
|
||||
|
||||
For email sending, smtp options must be provided, the `-smtp_relay` is the address of a valid SMTP server that is listening in secure TLS port. If the SMTP connection fails for any reason, the service tries to use an unencrypted localhost connection to a SMTP server running in port 25, this will most likely fail in a Docker container solution (unless you install an SMTP server in the container that is unlikely to happen).
|
||||
|
||||
The rest of the options in there are self explanatory and does not require of further documentation
|
||||
|
||||
#### Environment Variables Only
|
||||
|
||||
Users **should** also provide an `API_TOKEN` that will be used to authenticate API requests that have administrative or special purpose. The token can be any string of any length, we recommend a sha256, for example, a sha256sum of 4MB of random data from `/dev/urandom` like
|
||||
|
||||
```
|
||||
dd if=/dev/urandom bs=8b count=1024 iflag=fullblock 2>/dev/null | sha256sum | awk '{print $1}'
|
||||
```
|
||||
|
||||
This token **must** be provided to any administrative like endpoint (usually using [curl][curl] or similar)
|
||||
|
||||
#### Optional Environment Variables Only
|
||||
|
||||
There are some optional environment only variables that can be adjusted as well to configure the service behavior, they get listed below:
|
||||
|
||||
* `APP_NAME`: used to set up the web site title, defaults to `ArtixWeb Packages`
|
||||
* `GITEA_URL`: used to set up the URL to the Artix's gitea service, defaults to `https://gitea.artixlinux.org`
|
||||
* `GITEA_API_URL`: used to set up the URL to the Artix's gitea API service, defaults to `https://gitea.artixlinux.org/api`
|
||||
* `GITEA_TOKEN`: used to set up the gitea token, defaults to empty string
|
||||
* `DATABASES_PATH`: used to set up the path of the pacman databases the service uses, defaults to `/var/lib/pacman`
|
||||
|
||||
### Creating DB schema
|
||||
|
||||
For easy DB schema creation an `adminer` container is included in the composer file.
|
||||
|
||||
> The user can also just copy the `schema.sql` file in the `example_files` directory to the running [postgres][postgres] container and use `psql` command from the [postgres-libs package][postgres] instead
|
||||
|
||||
Point your browser to http://localhost:8080 and login into the database, if you did not made any changes to the `docker-composer.yml` file it will looks like the screenshot below
|
||||
|
||||
![adminer_login][adminer_login]
|
||||
|
||||
After login in, click on the `Import` link on the left side menu and select the `example_files/schema.sql` file from the project
|
||||
|
||||
![adminer_import][adminer_import]
|
||||
|
||||
Just click the `Execute` button to import the database schema
|
||||
|
||||
![adminer_schema][adminer_schema]
|
||||
|
||||
As a last step and to make sure everything is fine, restart the stack using the following command inside the `example_files` directory
|
||||
|
||||
```
|
||||
docker compose restart
|
||||
```
|
||||
|
||||
You should now be able to access the ArtixWeb packages web interface in http://localhost:8000
|
||||
|
||||
## How it works?
|
||||
|
||||
ArtixWeb Packages works using a combination of `libalpm` and gitea API queries, the first time that a package detail is check by anyone, the metadata extracted from gitea API gets pushed into a local `postgres` database that is used both to cache/store relational packages metadata/information and users storage.
|
||||
|
||||
### How to flag a package?
|
||||
|
||||
Only trusted users that have an account can flag packages, any non authenticated user will be unable to access the flagging packages UI unless they are properly authenticated into the service. Packages can be flag just by entering its details and clicking in the `Flag package out-of-date` link in the right side `Actions Panel`.
|
||||
|
||||
> **note**: the only user data stored in the service database is the user email, that is used to authenticate within the service, the email is necessary because it is used to send admin created invitations via email, users use the link on the invitation email to register into the service, the email is not used for any other purpose and users or activity is not being track in any way
|
||||
|
||||
### How to un-flag a package?
|
||||
|
||||
Packages will be automatically un-flagged as soon as a new version is pushed into the Artix stable package repositories and the service syncs it.
|
||||
|
||||
### Manually un-flag
|
||||
|
||||
Users being in possession of the `API_TOKEN` that the service is using can manually un-flag any package using a curl request
|
||||
|
||||
```
|
||||
curl -X DELETE \
|
||||
-H "X-Admin-Token: <token>" \
|
||||
https://packages.artixlinux.org/akonadi/21.12.3-2
|
||||
```
|
||||
|
||||
### How to create an invitation?
|
||||
|
||||
Being in possession of the `API_TOKEN` that the service is currently using, one can use [curl][curl] to create a new invitation and send an invitation email
|
||||
|
||||
```
|
||||
curl -X POST \
|
||||
-H "X-Admin-Token: <token>" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"email":"user@email.com"}' \
|
||||
https://packages.artixlinux.org/api/invitation
|
||||
```
|
||||
|
||||
The service will use the configured SMTP access in order to send the email to the user, if the email can not be send, the invitation ID will be printed into the service logs output.
|
||||
|
||||
---
|
||||
|
||||
[1]: https://packages.artixlinux.org
|
||||
[2]: https://gitea.artixlinux.org/artix/artixweb_packages/src/branch/master/Dockerfile.install
|
||||
[3]: https://gitea.artixlinux.org/artix/artixweb_packages/src/branch/master/example_files
|
||||
|
||||
[git]: https://packages.artixlinux.org/details/git
|
||||
[docker]: https://packages.artixlinux.org/details/docker
|
||||
[postgres]: https://packages.artixlinux.org/details/posgresql-libs
|
||||
[curl]: https://packages.artixlinux.org/details/curl
|
||||
|
||||
[adminer_login]: docs/images/adminer_login.png
|
||||
[adminer_import]: docs/images/adminer_import.png
|
||||
[adminer_schema]: docs/images/adminer_schema_imported.png
|
@@ -8,7 +8,7 @@ homepage = "https://packages.artixlinux.org"
|
||||
keywords = ["artix", "packages", "gitea"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
edition = "2021"
|
||||
version = "0.1.0"
|
||||
version = "0.1.3"
|
||||
|
||||
[lib]
|
||||
name = "artix_gitea"
|
||||
@@ -17,5 +17,5 @@ path = "src/lib.rs"
|
||||
[dependencies]
|
||||
actix-web = "4.0.1"
|
||||
serde = { version = "1.0.136", features = ["derive"] }
|
||||
awc = { version = "3.0.0-beta.21", features = ["rustls"] }
|
||||
awc = { version = "3.0.0", features = ["rustls"] }
|
||||
chrono = { version = "0.4.19", features = ["serde"] }
|
||||
|
@@ -8,7 +8,7 @@ homepage = "https://packages.artixlinux.org"
|
||||
keywords = ["artix", "packages"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
edition = "2021"
|
||||
version = "0.1.0"
|
||||
version = "0.1.3"
|
||||
|
||||
[lib]
|
||||
name = "artix_pkglib"
|
||||
@@ -16,7 +16,7 @@ path = "src/lib.rs"
|
||||
|
||||
|
||||
[dependencies]
|
||||
alpm = "2.2.1"
|
||||
alpm-utils = "2.0.0"
|
||||
pacmanconf = "2.0.0"
|
||||
thiserror = "1.0.1"
|
||||
alpm = "3.0.4"
|
||||
alpm-utils = "3.0.2"
|
||||
pacmanconf = "2.1.0"
|
||||
thiserror = "1.0.58"
|
||||
|
@@ -11,14 +11,14 @@ use alpm::{Alpm, Db, Package};
|
||||
|
||||
/// Data structure used to returns information about an specific packages search
|
||||
pub struct PackagesResultData<'a> {
|
||||
packages: Vec<Package<'a>>,
|
||||
packages: Vec<&'a Package>,
|
||||
total: usize,
|
||||
}
|
||||
|
||||
impl<'a> PackagesResultData<'a> {
|
||||
/// Retrieve the packages of this result if any
|
||||
#[must_use]
|
||||
pub fn packages(&self) -> &Vec<Package<'a>> {
|
||||
pub fn packages(&self) -> &Vec<&Package> {
|
||||
&self.packages
|
||||
}
|
||||
|
||||
@@ -32,22 +32,19 @@ impl<'a> PackagesResultData<'a> {
|
||||
/// Returns back all the packages contained in the given database
|
||||
/// If the database is invalid it returns None
|
||||
#[must_use]
|
||||
pub fn get_packages(db: Db<'_>, limit: usize, offset: usize) -> Vec<Package<'_>> {
|
||||
pub fn get_packages<'a>(db: &'a Db, limit: usize, offset: usize) -> Vec<&'a Package> {
|
||||
let mut ret = Vec::new();
|
||||
if db.is_valid().is_ok() {
|
||||
let pkgs = db.pkgs();
|
||||
if pkgs.is_empty() {
|
||||
return ret;
|
||||
}
|
||||
ret.extend(pkgs);
|
||||
|
||||
// if offset is set, split the vector at the given offset
|
||||
if offset != 0 {
|
||||
ret = ret.split_at(offset).1.to_vec();
|
||||
// if offset is set, truncate the vector at the given offset
|
||||
if offset != 0 && offset < ret.len() {
|
||||
ret = ret.split_off(offset);
|
||||
}
|
||||
|
||||
// if limit is set, truncate the vector at it
|
||||
if limit != 0 {
|
||||
if limit != 0 && limit < ret.len() {
|
||||
ret.truncate(limit);
|
||||
}
|
||||
}
|
||||
@@ -85,7 +82,7 @@ pub fn get_all_packages<'a>(
|
||||
|
||||
if ret.is_empty() {
|
||||
return PackagesResultData {
|
||||
packages: ret,
|
||||
packages: Vec::new(),
|
||||
total: 0,
|
||||
};
|
||||
}
|
||||
@@ -126,14 +123,14 @@ mod test {
|
||||
let alpm = Alpm::new("/", &db_location).unwrap();
|
||||
let db = alpm.register_syncdb("world", SigLevel::NONE).unwrap();
|
||||
|
||||
let pkgs = get_packages(db, 0, 0);
|
||||
let pkgs = get_packages(*db, 0, 0);
|
||||
assert!(!pkgs.is_empty());
|
||||
|
||||
let pkgs = get_packages(db, 2, 0);
|
||||
let pkgs = get_packages(*db, 2, 0);
|
||||
assert_eq!(pkgs.len(), 2);
|
||||
|
||||
let pkgs = get_packages(db, 0, 0);
|
||||
let pkgs2 = get_packages(db, 0, 10);
|
||||
let pkgs = get_packages(*db, 0, 0);
|
||||
let pkgs2 = get_packages(*db, 0, 10);
|
||||
assert_eq!(pkgs2[0].name(), pkgs[10].name());
|
||||
}
|
||||
|
||||
|
@@ -5,7 +5,7 @@
|
||||
Copyright (c) 2022 - Oscar Campos <damnwidget@artixlinux.org>
|
||||
*/
|
||||
|
||||
use alpm::{Alpm, AlpmList, Dep, Package, Ver};
|
||||
use alpm::{Alpm, AlpmList, Dep, Package, Pkg, Ver};
|
||||
|
||||
use crate::error::Error;
|
||||
|
||||
@@ -17,7 +17,7 @@ use crate::error::Error;
|
||||
pub fn get_package_dependencies<'a>(
|
||||
alpm: &'a Alpm,
|
||||
package_name: &'a str,
|
||||
) -> Result<AlpmList<'a, Dep<'a>>, Error<'a>> {
|
||||
) -> Result<AlpmList<'a, &'a Dep>, Error<'a>> {
|
||||
for db in alpm.syncdbs() {
|
||||
let db_pkgs = db.search([package_name].iter());
|
||||
if let Ok(pkgs) = db_pkgs {
|
||||
@@ -40,7 +40,7 @@ pub fn get_package_dependencies<'a>(
|
||||
pub fn get_package_optional_dependencies<'a>(
|
||||
alpm: &'a Alpm,
|
||||
package_name: &'a str,
|
||||
) -> Result<AlpmList<'a, Dep<'a>>, Error<'a>> {
|
||||
) -> Result<AlpmList<'a, &'a Dep>, Error<'a>> {
|
||||
for db in alpm.syncdbs() {
|
||||
let db_pkgs = db.search([package_name].iter());
|
||||
if let Ok(pkgs) = db_pkgs {
|
||||
@@ -84,7 +84,7 @@ pub fn get_package_version<'a>(
|
||||
///
|
||||
/// If the package can not be found a [`Error::PackageNotFound`] error is returned
|
||||
#[allow(clippy::module_name_repetitions)]
|
||||
pub fn get_package<'a>(alpm: &'a Alpm, package_name: &'a str) -> Result<Package<'a>, Error<'a>> {
|
||||
pub fn get_package<'a>(alpm: &'a Alpm, package_name: &'a str) -> Result<&'a Pkg, Error<'a>> {
|
||||
for db in alpm.syncdbs() {
|
||||
let db_pkgs = db.search([package_name].iter());
|
||||
if let Ok(pkgs) = db_pkgs {
|
||||
@@ -110,15 +110,15 @@ pub fn retrieve_providers<'a>(
|
||||
arch: &str,
|
||||
alpm: &'a Alpm,
|
||||
requirement: &'a str,
|
||||
) -> Vec<Package<'a>> {
|
||||
let mut pkgs = Vec::new();
|
||||
) -> Vec<&'a Package> {
|
||||
let mut pkgs: Vec<&'a Package> = Vec::new();
|
||||
for db in alpm.syncdbs() {
|
||||
if arch.to_lowercase() != "any" && db.name() == "lib32" {
|
||||
continue;
|
||||
}
|
||||
|
||||
for pkg in db.pkgs() {
|
||||
let mut a: Vec<Dep<'_>> = Vec::new();
|
||||
let mut a: Vec<&'a Dep> = Vec::new();
|
||||
a.extend(pkg.provides().iter().filter(|d| d.name() == requirement));
|
||||
if !a.is_empty() {
|
||||
pkgs.push(pkg);
|
||||
|
@@ -7,7 +7,7 @@ repository = "gitea.artixlinux.org/artix/artixweb_packages"
|
||||
keywords = ["artix", "packages"]
|
||||
license = "MIT OR Apache-2.0"
|
||||
edition = "2021"
|
||||
version = "0.1.0"
|
||||
version = "0.1.3"
|
||||
|
||||
[[bin]]
|
||||
name = "artixweb_packages"
|
||||
@@ -15,18 +15,32 @@ test = false
|
||||
bench = false
|
||||
|
||||
[dependencies]
|
||||
artix-gitea = { path = "../artix-gitea", version = "=0.1.0" }
|
||||
artix-pkglib = { path = "../artix-pkglib", version = "=0.1.0" }
|
||||
artix-gitea = { path = "../artix-gitea", version = "=0.1.3" }
|
||||
artix-pkglib = { path = "../artix-pkglib", version = "=0.1.3" }
|
||||
|
||||
actix-files = "0.6.0"
|
||||
actix-session = "0.5.0"
|
||||
actix-identity = "0.4"
|
||||
argon2 = "0.4.0"
|
||||
askama = "0.11.1"
|
||||
derive_more = "0.99.17"
|
||||
env_logger = "0.9.0"
|
||||
lazy_static = "1.4.0"
|
||||
lettre = "0.10.0-rc.4"
|
||||
log = "0.4.14"
|
||||
r2d2 = "0.8"
|
||||
time = "0.3"
|
||||
|
||||
actix-session = { version = "0.6.0", features = ["cookie-session"] }
|
||||
actix-web = { version = "4.0.1", features = ["rustls"] }
|
||||
clap = { version = "3.1.5", features = ["cargo", "suggestions"] }
|
||||
clap = { version = "3.1.5", features = ["cargo", "suggestions", "env"] }
|
||||
chrono = { version = "0.4.19", features = ["serde"] }
|
||||
diesel = { version = "1.4.8", features = [
|
||||
"postgres",
|
||||
"uuidv07",
|
||||
"r2d2",
|
||||
"chrono",
|
||||
] }
|
||||
rand_core = { version = "0.6", features = ["std"] }
|
||||
serde = { version = "^1.0", features = ["derive"] }
|
||||
serde_json = { version = "1.0.79" }
|
||||
uuid = { version = "0.8", features = ["serde", "v4"] }
|
||||
|
@@ -22,12 +22,29 @@ header {
|
||||
align-items: inherit;
|
||||
display: flex;
|
||||
flex-direction: inherit;
|
||||
justify-content: space-between;
|
||||
margin: auto;
|
||||
min-height: 52px;
|
||||
padding: 0 .5rem;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
div.login_logout {
|
||||
font-size: 0.85em;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
div.login_logout>span>form {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
div.login_logout>span>form>input {
|
||||
background-color: none;
|
||||
border: none;
|
||||
color: #53bffc;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a {
|
||||
text-decoration: none;
|
||||
color: #53bffc;
|
||||
@@ -49,7 +66,7 @@ nav {
|
||||
display: inline;
|
||||
font-weight: normal;
|
||||
margin-top: 8px;
|
||||
padding-left: 3em;
|
||||
padding-left: .5em;
|
||||
}
|
||||
|
||||
nav a {
|
||||
@@ -63,11 +80,17 @@ nav a:focus {
|
||||
}
|
||||
|
||||
h2,
|
||||
h3 {
|
||||
h3,
|
||||
h4 {
|
||||
border-bottom: 1px solid #858585;
|
||||
margin: .5em 0 .5em 0;
|
||||
}
|
||||
|
||||
details summary {
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
fieldset {
|
||||
border: 0;
|
||||
margin: 0;
|
||||
@@ -138,6 +161,8 @@ td,
|
||||
th {
|
||||
border: 1px solid #858585;
|
||||
text-align: left;
|
||||
overflow-x: auto;
|
||||
overflow-wrap: anywhere;
|
||||
padding: 8px;
|
||||
}
|
||||
|
||||
@@ -149,6 +174,7 @@ th {
|
||||
color: #fff;
|
||||
background-color: #0f3147;
|
||||
border: 1px solid #0A6682;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
th a {
|
||||
@@ -156,6 +182,14 @@ th a {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.flagged {
|
||||
color: rgb(221, 87, 87);
|
||||
}
|
||||
|
||||
.unflagged {
|
||||
color: rgb(36, 136, 36);
|
||||
}
|
||||
|
||||
footer {
|
||||
border: 1px solid #858585;
|
||||
font-size: 0.85em;
|
||||
@@ -211,7 +245,7 @@ details[open]>summary {
|
||||
}
|
||||
|
||||
details[open] summary~* {
|
||||
animation: sweep 1s ease-in-out;
|
||||
animation: sweep .5s ease-in-out;
|
||||
}
|
||||
|
||||
@keyframes sweep {
|
||||
@@ -222,4 +256,40 @@ details[open] summary~* {
|
||||
100% {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
[role="registration"] {
|
||||
margin-top: 80px;
|
||||
}
|
||||
|
||||
span.code {
|
||||
font-weight: bold;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
input.try_again {
|
||||
margin-top: 16px;
|
||||
margin-bottom: 30px;
|
||||
}
|
||||
|
||||
[role="action_panel"] {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
border: solid 1px #858585;
|
||||
float: right;
|
||||
padding: 10px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
[role="action_panel"]>section>span {
|
||||
font-size: 0.95em;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 588px) {
|
||||
[role="action_panel"] {
|
||||
float: none;
|
||||
}
|
||||
|
||||
.grid-50-50 {
|
||||
display: initial;
|
||||
}
|
||||
}
|
||||
|
2
crates/artixweb-packages/diesel.toml
Normal file
2
crates/artixweb-packages/diesel.toml
Normal file
@@ -0,0 +1,2 @@
|
||||
[print_schema]
|
||||
file = "src/schema.rs"
|
0
crates/artixweb-packages/migrations/.gitkeep
Normal file
0
crates/artixweb-packages/migrations/.gitkeep
Normal file
@@ -0,0 +1,6 @@
|
||||
-- This file was automatically created by Diesel to setup helper functions
|
||||
-- and other internal bookkeeping. This file is safe to edit, any future
|
||||
-- changes will be added to existing projects as new migrations.
|
||||
|
||||
DROP FUNCTION IF EXISTS diesel_manage_updated_at(_tbl regclass);
|
||||
DROP FUNCTION IF EXISTS diesel_set_updated_at();
|
@@ -0,0 +1,36 @@
|
||||
-- This file was automatically created by Diesel to setup helper functions
|
||||
-- and other internal bookkeeping. This file is safe to edit, any future
|
||||
-- changes will be added to existing projects as new migrations.
|
||||
|
||||
|
||||
|
||||
|
||||
-- Sets up a trigger for the given table to automatically set a column called
|
||||
-- `updated_at` whenever the row is modified (unless `updated_at` was included
|
||||
-- in the modified columns)
|
||||
--
|
||||
-- # Example
|
||||
--
|
||||
-- ```sql
|
||||
-- CREATE TABLE users (id SERIAL PRIMARY KEY, updated_at TIMESTAMP NOT NULL DEFAULT NOW());
|
||||
--
|
||||
-- SELECT diesel_manage_updated_at('users');
|
||||
-- ```
|
||||
CREATE OR REPLACE FUNCTION diesel_manage_updated_at(_tbl regclass) RETURNS VOID AS $$
|
||||
BEGIN
|
||||
EXECUTE format('CREATE TRIGGER set_updated_at BEFORE UPDATE ON %s
|
||||
FOR EACH ROW EXECUTE PROCEDURE diesel_set_updated_at()', _tbl);
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
||||
|
||||
CREATE OR REPLACE FUNCTION diesel_set_updated_at() RETURNS trigger AS $$
|
||||
BEGIN
|
||||
IF (
|
||||
NEW IS DISTINCT FROM OLD AND
|
||||
NEW.updated_at IS NOT DISTINCT FROM OLD.updated_at
|
||||
) THEN
|
||||
NEW.updated_at := current_timestamp;
|
||||
END IF;
|
||||
RETURN NEW;
|
||||
END;
|
||||
$$ LANGUAGE plpgsql;
|
@@ -0,0 +1,2 @@
|
||||
-- This file should undo anything in `up.sql`
|
||||
DROP TABLE packages
|
@@ -0,0 +1,6 @@
|
||||
-- Table 'packages' is used to store local information about a given package
|
||||
CREATE TABLE packages (
|
||||
package_name VARCHAR NOT NULL UNIQUE PRIMARY KEY, -- This actually also includes the package version
|
||||
gitea_url VARCHAR NOT NULL, -- Gitea repository URL
|
||||
last_update TIMESTAMP NOT NULL, -- Last time this package was updated in gitea
|
||||
)
|
@@ -0,0 +1,2 @@
|
||||
-- This file should undo anything in `up.sql`
|
||||
DROP TABLE users
|
@@ -0,0 +1,8 @@
|
||||
-- Table 'users' is used to store information about artixweb users
|
||||
CREATE TABLE users (
|
||||
email VARCHAR(100) NOT NULL UNIQUE PRIMARY KEY,
|
||||
hash VARCHAR(122) NOT NULL, -- argon hash
|
||||
created_at TIMESTAMP NOT NULL,
|
||||
banned BOOLEAN NOT NULL DEFAULT 'f'
|
||||
admin BOOLEAN NOT NULL DEFAULT 'f'
|
||||
)
|
@@ -0,0 +1,2 @@
|
||||
-- This file should undo anything in `up.sql`
|
||||
DROP TABLE invitations
|
@@ -0,0 +1,6 @@
|
||||
-- Table 'invitations' is used to store new user creation invitations
|
||||
CREATE TABLE invitations (
|
||||
id UUID NOT NULL UNIQUE PRIMARY KEY,
|
||||
email VARCHAR(100) NOT NULL,
|
||||
expires_at TIMESTAMP NOT NULL
|
||||
)
|
@@ -0,0 +1,2 @@
|
||||
-- This file should undo anything in `up.sql`
|
||||
DROP TABLE package_flags
|
@@ -0,0 +1,15 @@
|
||||
-- Table 'package_flags' is a relation table used by users to flag outdated packages
|
||||
CREATE TABLE package_flags (
|
||||
user_email VARCHAR(100) NOT NULL,
|
||||
package_name VARCHAR NOT NULL,
|
||||
flag_on TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
PRIMARY KEY (user_email, package_name),
|
||||
|
||||
-- trolling flags?, no issue, just delete the user
|
||||
CONSTRAINT fk_user_email
|
||||
FOREIGN KEY (user_email) REFERENCES users (email) ON DELETE CASCADE,
|
||||
|
||||
-- package unmaintained?, no issue, just delete the package
|
||||
CONSTRAINT fk_package_id
|
||||
FOREIGN KEY (package_name) REFERENCES packages (package_name) ON DELETE CASCADE
|
||||
)
|
@@ -5,9 +5,13 @@
|
||||
Copyright (c) 2022 - Oscar Campos <damnwidget@artixlinux.org>
|
||||
*/
|
||||
|
||||
use std::env;
|
||||
use std::{env, fs::File, io::Read, path::Path};
|
||||
|
||||
use argon2::{password_hash::SaltString, Argon2, PasswordHash, PasswordHasher, PasswordVerifier};
|
||||
use artix_pkglib::prelude::{Alpm, SigLevel};
|
||||
use rand_core::OsRng;
|
||||
|
||||
use super::lib::errors::ServiceError;
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
/// Global constant application settings
|
||||
@@ -17,9 +21,14 @@ lazy_static::lazy_static! {
|
||||
gitea_api_url: env::var("GITEA_API_URL").unwrap_or_else(|_| String::from(artix_gitea::prelude::GITEA_API_URL)),
|
||||
gitea_token: env::var("GITEA_TOKEN").unwrap_or_else(|_| String::new()),
|
||||
databases_path: env::var("DATABASES_PATH").unwrap_or_else(|_| String::from("/var/lib/pacman")),
|
||||
api_token: env::var("API_TOKEN").unwrap_or_else(|_| generate_random_token()),
|
||||
};
|
||||
}
|
||||
|
||||
lazy_static::lazy_static! {
|
||||
pub static ref SECRET_KEY: String = std::env::var("SECRET_KEY").unwrap_or_else(|_| generate_random_token());
|
||||
}
|
||||
|
||||
/// Data structure containing the site configuration
|
||||
pub struct AppSettings {
|
||||
pub app_name: String,
|
||||
@@ -27,6 +36,7 @@ pub struct AppSettings {
|
||||
pub gitea_api_url: String,
|
||||
pub gitea_token: String,
|
||||
pub databases_path: String,
|
||||
pub api_token: String,
|
||||
}
|
||||
|
||||
impl Default for AppSettings {
|
||||
@@ -37,11 +47,43 @@ impl Default for AppSettings {
|
||||
gitea_api_url: String::from(artix_gitea::prelude::GITEA_API_URL),
|
||||
gitea_token: String::new(),
|
||||
databases_path: String::from("/var/lib/pacman"),
|
||||
api_token: generate_random_token(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const DATABASESS: [&str; 5] = ["system", "world", "galaxy", "universe", "lib32"];
|
||||
/// Data structure containing SMTP configuration
|
||||
#[derive(Clone)]
|
||||
pub struct SmtpOptions {
|
||||
pub user: String,
|
||||
pub password: String,
|
||||
pub relay: String,
|
||||
}
|
||||
|
||||
fn generate_random_token() -> String {
|
||||
use std::fmt::Write;
|
||||
let path = Path::new("/dev/urandom");
|
||||
let mut file = File::open(&path).unwrap();
|
||||
let mut buffer = [0; 32];
|
||||
file.read_exact(&mut buffer[..]).unwrap();
|
||||
let mut token = String::new();
|
||||
for byte in buffer {
|
||||
write!(&mut token, "{:x}", byte).expect("Unable to write");
|
||||
}
|
||||
token
|
||||
}
|
||||
|
||||
const DATABASESS: [&str; 8] = [
|
||||
"system",
|
||||
"world",
|
||||
"galaxy",
|
||||
"lib32",
|
||||
"system-gremlins",
|
||||
"world-gremlins",
|
||||
"galaxy-gremlins",
|
||||
"lib32-gremlins"
|
||||
];
|
||||
const MIN_PASSWORD_LENGTH: usize = 8;
|
||||
|
||||
/// Syncs the configured databases
|
||||
pub fn sync_databases(alpm: &Alpm) {
|
||||
@@ -49,3 +91,66 @@ pub fn sync_databases(alpm: &Alpm) {
|
||||
alpm.register_syncdb(db, SigLevel::USE_DEFAULT).unwrap();
|
||||
}
|
||||
}
|
||||
|
||||
/// hash the given password with argon2
|
||||
pub fn hash_password(password: &str) -> Result<String, ServiceError> {
|
||||
let pwd = password.as_bytes();
|
||||
let salt = SaltString::generate(&mut OsRng);
|
||||
|
||||
let argon2 = Argon2::default();
|
||||
if let Ok(password_hash) = argon2.hash_password(pwd, &salt) {
|
||||
Ok(password_hash.to_string())
|
||||
} else {
|
||||
Err(ServiceError::InternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
// verify a password
|
||||
pub fn verify(hash: &str, password: &str) -> Result<bool, ServiceError> {
|
||||
let argon2 = Argon2::default();
|
||||
|
||||
if let Ok(parsed_hash) = PasswordHash::new(hash) {
|
||||
if argon2
|
||||
.verify_password(password.as_bytes(), &parsed_hash)
|
||||
.is_ok()
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
Err(ServiceError::Unauthorized)
|
||||
}
|
||||
|
||||
// check password strength
|
||||
pub fn check_password_strength(password: &str) -> bool {
|
||||
if password.len() < MIN_PASSWORD_LENGTH {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mut punctuation_found = false;
|
||||
let mut caps_found = false;
|
||||
for c in password.chars() {
|
||||
match c {
|
||||
'.' | ',' | '$' | '%' | ';' | ':' | '@' | '_' | '-' | '!' | '?' => {
|
||||
punctuation_found = true;
|
||||
if caps_found {
|
||||
break;
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
if c.is_uppercase() {
|
||||
caps_found = true;
|
||||
if punctuation_found {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !punctuation_found || !caps_found {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
112
crates/artixweb-packages/src/handlers/auth.rs
Normal file
112
crates/artixweb-packages/src/handlers/auth.rs
Normal file
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
This file is part of Artix Web Packages. See LICENSE file for details
|
||||
|
||||
Copyright (c) 2022 - Artix Linux
|
||||
Copyright (c) 2022 - Oscar Campos <damnwidget@artixlinux.org>
|
||||
*/
|
||||
|
||||
use std::future::{ready, Ready};
|
||||
|
||||
use actix_identity::Identity;
|
||||
use actix_session::Session;
|
||||
use actix_web::{dev::Payload, http, web, Error, FromRequest, HttpRequest, HttpResponse};
|
||||
use askama::Template;
|
||||
use diesel::prelude::*;
|
||||
use diesel::PgConnection;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::config::verify;
|
||||
use crate::lib::errors::ServiceError;
|
||||
use crate::models::{Pool, SlimUser, User};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct Data {
|
||||
pub email: String,
|
||||
pub password: String,
|
||||
}
|
||||
|
||||
pub type LoggedUser = SlimUser;
|
||||
|
||||
impl FromRequest for LoggedUser {
|
||||
type Error = Error;
|
||||
type Future = Ready<Result<LoggedUser, Error>>;
|
||||
|
||||
fn from_request(req: &HttpRequest, pl: &mut Payload) -> Self::Future {
|
||||
if let Ok(identity) = Identity::from_request(req, pl).into_inner() {
|
||||
if let Some(user_data) = identity.identity() {
|
||||
if let Ok(user) = serde_json::from_str(&user_data) {
|
||||
return ready(Ok(user));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ready(Err(ServiceError::Unauthorized.into()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "login_ui.html")]
|
||||
struct LoginUITemplate {
|
||||
user_email: String,
|
||||
generation_time: u128,
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_async)]
|
||||
pub async fn ui(session: Session) -> HttpResponse {
|
||||
let start_time = std::time::Instant::now();
|
||||
if session.get::<String>("user_email").unwrap().is_some() {
|
||||
return HttpResponse::SeeOther()
|
||||
.append_header((http::header::LOCATION, "/"))
|
||||
.finish();
|
||||
}
|
||||
|
||||
let s = LoginUITemplate {
|
||||
user_email: String::new(),
|
||||
generation_time: start_time.elapsed().as_millis(),
|
||||
}
|
||||
.render()
|
||||
.unwrap();
|
||||
|
||||
HttpResponse::Ok().content_type("text/html").body(s)
|
||||
}
|
||||
|
||||
pub async fn login(
|
||||
auth_data: web::Form<Data>,
|
||||
id: Identity,
|
||||
session: Session,
|
||||
pool: web::Data<Pool>,
|
||||
) -> Result<HttpResponse, actix_web::Error> {
|
||||
let user = web::block(move || query(&auth_data.into_inner(), &pool)).await??;
|
||||
let user_string = serde_json::to_string(&user).unwrap();
|
||||
id.remember(user_string);
|
||||
session.insert("user_email", user.email)?;
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.append_header((http::header::LOCATION, "/"))
|
||||
.finish())
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_async)]
|
||||
pub async fn logout(id: Identity, session: Session) -> HttpResponse {
|
||||
id.forget();
|
||||
session.remove("user_email");
|
||||
HttpResponse::SeeOther()
|
||||
.append_header((http::header::LOCATION, "/"))
|
||||
.finish()
|
||||
}
|
||||
|
||||
fn query(auth_data: &Data, pool: &web::Data<Pool>) -> Result<SlimUser, ServiceError> {
|
||||
use crate::schema::users::dsl::{email, users};
|
||||
let conn: &PgConnection = &pool.get().unwrap();
|
||||
let mut items = users
|
||||
.filter(email.eq(&auth_data.email))
|
||||
.load::<User>(conn)?;
|
||||
|
||||
if let Some(user) = items.pop() {
|
||||
if let Ok(matching) = verify(&user.hash, &auth_data.password) {
|
||||
if matching {
|
||||
return Ok(user.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(ServiceError::Unauthorized)
|
||||
}
|
@@ -6,33 +6,65 @@
|
||||
*/
|
||||
|
||||
#![allow(clippy::unused_async, clippy::module_name_repetitions)]
|
||||
use actix_session::Session;
|
||||
use actix_web::{get, web, HttpResponse, Result};
|
||||
use askama::Template;
|
||||
|
||||
use crate::models::Pool;
|
||||
|
||||
use super::packages::{get_packages_details_inner, ResponseDetail};
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "package_details.html")]
|
||||
struct PackageDetailsTemplate {
|
||||
pkg: ResponseDetail,
|
||||
user_email: String,
|
||||
generation_time: u128,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "error.html")]
|
||||
struct PackageNotFoundTemplate {
|
||||
error_message: String,
|
||||
user_email: String,
|
||||
generation_time: u128,
|
||||
}
|
||||
|
||||
/// Construct the package detail interface for the web site
|
||||
#[get("/details/{package_name}")]
|
||||
pub async fn package_details(data: web::Path<String>) -> Result<HttpResponse> {
|
||||
pub async fn package_details(
|
||||
data: web::Path<String>,
|
||||
session: Session,
|
||||
pool: web::Data<Pool>,
|
||||
) -> Result<HttpResponse> {
|
||||
let start_time = std::time::Instant::now();
|
||||
let s = if let Ok(details) = get_packages_details_inner(&data).await {
|
||||
let user_email = if let Some(email) = session.get::<String>("user_email").unwrap() {
|
||||
email
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let s = if let Ok(details) = get_packages_details_inner(&data, pool).await {
|
||||
PackageDetailsTemplate {
|
||||
user_email,
|
||||
pkg: details,
|
||||
generation_time: start_time.elapsed().as_millis(),
|
||||
}
|
||||
.render()
|
||||
.unwrap()
|
||||
} else {
|
||||
return Ok(HttpResponse::NotFound()
|
||||
.content_type("text/html")
|
||||
.body("<html><body>404 - Page Not Found!</body></html>"));
|
||||
return Ok(HttpResponse::NotFound().content_type("text/html").body(
|
||||
PackageNotFoundTemplate {
|
||||
user_email,
|
||||
error_message: format!(
|
||||
"The specified package {} could not be found in the Artix packages database.",
|
||||
data
|
||||
),
|
||||
generation_time: start_time.elapsed().as_millis(),
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
));
|
||||
};
|
||||
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(s))
|
||||
@@ -42,6 +74,8 @@ pub async fn package_details(data: web::Path<String>) -> Result<HttpResponse> {
|
||||
mod filters {
|
||||
use chrono::NaiveDateTime;
|
||||
|
||||
use crate::handlers::packages::Dependency;
|
||||
|
||||
pub fn details_url(pkg_name: &str) -> ::askama::Result<String> {
|
||||
Ok(super::super::details_url(pkg_name))
|
||||
}
|
||||
@@ -57,4 +91,13 @@ mod filters {
|
||||
pub fn show_date(timestamp: &i64) -> ::askama::Result<NaiveDateTime> {
|
||||
Ok(NaiveDateTime::from_timestamp(*timestamp, 0))
|
||||
}
|
||||
|
||||
pub(crate) fn provides_or_replaces(deps: &[Dependency]) -> ::askama::Result<String> {
|
||||
let mut elements = Vec::new();
|
||||
elements.extend(
|
||||
deps.iter()
|
||||
.map(|dep| format!("{}{}{}", dep.name, dep.depmod, dep.ver)),
|
||||
);
|
||||
Ok(elements.join(", "))
|
||||
}
|
||||
}
|
||||
|
@@ -11,17 +11,22 @@
|
||||
clippy::cast_precision_loss
|
||||
)]
|
||||
|
||||
use actix_session::Session;
|
||||
use actix_web::{web, HttpResponse, Result};
|
||||
use askama::Template;
|
||||
|
||||
use crate::models::Pool;
|
||||
|
||||
use super::packages::{get_packages_inner, Response};
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "base_index.html")]
|
||||
struct BaseTemplate {
|
||||
packages: Vec<Response>,
|
||||
repos: Vec<String>,
|
||||
limit: usize,
|
||||
query: String,
|
||||
user_email: String,
|
||||
generation_time: u128,
|
||||
}
|
||||
|
||||
@@ -29,17 +34,23 @@ struct BaseTemplate {
|
||||
#[template(path = "packages_nav.html")]
|
||||
struct PackagesNavigation {
|
||||
packages: Vec<Response>,
|
||||
repos: Vec<String>,
|
||||
total: usize,
|
||||
offset: usize,
|
||||
limit: usize,
|
||||
query: String,
|
||||
user_email: String,
|
||||
total_pages: usize,
|
||||
generation_time: u128,
|
||||
}
|
||||
|
||||
/// Constructs the index for the web site
|
||||
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
|
||||
pub async fn index(query: web::Query<Vec<(String, String)>>) -> Result<HttpResponse> {
|
||||
pub async fn index(
|
||||
query: web::Query<Vec<(String, String)>>,
|
||||
session: Session,
|
||||
pool: web::Data<Pool>,
|
||||
) -> Result<HttpResponse> {
|
||||
let start_time = std::time::Instant::now();
|
||||
let mut offset: usize = 0;
|
||||
let mut limit: usize = 50;
|
||||
@@ -47,8 +58,22 @@ pub async fn index(query: web::Query<Vec<(String, String)>>) -> Result<HttpRespo
|
||||
let mut search_criteria: Option<&str> = None;
|
||||
let mut keyword: String = String::new();
|
||||
let mut query_url = Vec::new();
|
||||
let user_email = if let Some(email) = session.get::<String>("user_email").unwrap() {
|
||||
email
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
|
||||
let valid_repos = vec!["world", "galaxy", "system", "universe", "lib32"];
|
||||
let valid_repos = vec![
|
||||
"system",
|
||||
"world",
|
||||
"galaxy",
|
||||
"lib32",
|
||||
"system-gremlins",
|
||||
"world-gremlins",
|
||||
"galaxy-gremlins",
|
||||
"lib32-gremlins"
|
||||
];
|
||||
for parameter in query.0 {
|
||||
let key: &str = ¶meter.0;
|
||||
match key {
|
||||
@@ -96,36 +121,37 @@ pub async fn index(query: web::Query<Vec<(String, String)>>) -> Result<HttpRespo
|
||||
repos.join(":")
|
||||
};
|
||||
|
||||
let s = if let Ok(result) = get_packages_inner(&repos_criteria, limit, offset, search_criteria)
|
||||
{
|
||||
PackagesNavigation {
|
||||
packages: result.0,
|
||||
total: result.1,
|
||||
query: query_url.join("&"),
|
||||
total_pages: if limit > 0 {
|
||||
(result.1 as f64 / limit as f64).ceil() as usize
|
||||
} else {
|
||||
1
|
||||
},
|
||||
generation_time: start_time.elapsed().as_millis(),
|
||||
offset,
|
||||
limit,
|
||||
}
|
||||
.render()
|
||||
.unwrap()
|
||||
} else {
|
||||
return Ok(HttpResponse::NotFound()
|
||||
.content_type("text/html")
|
||||
.body("<html><body>404 - Page Not Found!</body></html>"));
|
||||
let mut s = PackagesNavigation {
|
||||
user_email,
|
||||
packages: Vec::new(),
|
||||
repos,
|
||||
total: 0,
|
||||
query: query_url.join("&"),
|
||||
total_pages: 1,
|
||||
generation_time: 0,
|
||||
offset,
|
||||
limit
|
||||
};
|
||||
if let Ok(result) =
|
||||
get_packages_inner(&repos_criteria, limit, offset, search_criteria, Some(&pool)).await
|
||||
{
|
||||
s.packages = result.0;
|
||||
s.total = result.1;
|
||||
s.total_pages = if limit > 0 {
|
||||
(result.1 as f64 / limit as f64).ceil() as usize
|
||||
} else {
|
||||
1
|
||||
};
|
||||
}
|
||||
s.generation_time = start_time.elapsed().as_millis();
|
||||
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(s))
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(s.render().unwrap()))
|
||||
}
|
||||
|
||||
#[allow(clippy::unnecessary_wraps)]
|
||||
mod filters {
|
||||
pub fn selected_repo(query: &str, repo: &str) -> ::askama::Result<bool> {
|
||||
Ok(query.to_lowercase().contains(&repo.to_lowercase()))
|
||||
pub fn selected_repo(repos: &Vec<String>, repo: &str) -> ::askama::Result<bool> {
|
||||
Ok(repos.iter().any(|r| r == repo))
|
||||
}
|
||||
|
||||
pub fn details_url(pkg_name: &str) -> ::askama::Result<String> {
|
||||
|
107
crates/artixweb-packages/src/handlers/invitation.rs
Normal file
107
crates/artixweb-packages/src/handlers/invitation.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
/*
|
||||
This file is part of Artix Web Packages. See LICENSE file for details
|
||||
|
||||
Copyright (c) 2022 - Artix Linux
|
||||
Copyright (c) 2022 - Oscar Campos <damnwidget@artixlinux.org>
|
||||
*/
|
||||
|
||||
use actix_web::{web, HttpResponse};
|
||||
use askama::Template;
|
||||
use diesel::{prelude::*, PgConnection};
|
||||
use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::{
|
||||
config::{SmtpOptions, SETTINGS},
|
||||
lib::templates::EmailTemplate,
|
||||
models::{Invitation, Pool},
|
||||
};
|
||||
|
||||
#[derive(Deserialize)]
|
||||
pub struct Data {
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
pub async fn post(
|
||||
req: actix_web::HttpRequest,
|
||||
invitation_data: web::Json<Data>,
|
||||
pool: web::Data<Pool>,
|
||||
smtp_cfg: web::Data<SmtpOptions>,
|
||||
) -> Result<HttpResponse, actix_web::Error> {
|
||||
if let Some(token) = req.headers().get("x-admin-token") {
|
||||
if token == &SETTINGS.api_token {
|
||||
web::block(move || {
|
||||
create_invitation(invitation_data.into_inner().email, &pool, &smtp_cfg)
|
||||
})
|
||||
.await??;
|
||||
Ok(HttpResponse::Ok().finish())
|
||||
} else {
|
||||
Ok(HttpResponse::Unauthorized().finish())
|
||||
}
|
||||
} else {
|
||||
Ok(HttpResponse::Unauthorized().finish())
|
||||
}
|
||||
}
|
||||
|
||||
fn create_invitation(
|
||||
eml: String,
|
||||
pool: &web::Data<Pool>,
|
||||
smtp_cfg: &web::Data<SmtpOptions>,
|
||||
) -> Result<(), crate::lib::errors::ServiceError> {
|
||||
let invitation = query(eml, pool)?;
|
||||
if let Err(err) = send_invitation(&invitation, smtp_cfg) {
|
||||
dbg!(err);
|
||||
println!("{:#?}", invitation);
|
||||
};
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn send_invitation(
|
||||
invitation: &Invitation,
|
||||
smtp_cfg: &web::Data<SmtpOptions>,
|
||||
) -> Result<(), crate::lib::errors::ServiceError> {
|
||||
let email = Message::builder()
|
||||
.from(
|
||||
"ArtixWeb Packages <artixweb@artixlinux.org>"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.to(invitation.email.parse().unwrap())
|
||||
.subject("You have been invited to participate in ArtixWeb Packages as a reporter")
|
||||
.body(EmailTemplate { invitation }.render().unwrap())
|
||||
.unwrap();
|
||||
|
||||
let smtp_credentials = Credentials::new(smtp_cfg.user.clone(), smtp_cfg.password.clone());
|
||||
let mailer = if let Ok(transport) = SmtpTransport::starttls_relay(&smtp_cfg.relay.clone()) {
|
||||
transport.credentials(smtp_credentials).build()
|
||||
} else {
|
||||
// open a local connection on port 25
|
||||
SmtpTransport::unencrypted_localhost()
|
||||
};
|
||||
|
||||
// send the email
|
||||
match mailer.send(&email) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(e) => {
|
||||
log::error!("{}", e);
|
||||
Err(crate::lib::errors::ServiceError::InternalServerError)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Diesel query
|
||||
fn query(
|
||||
eml: String,
|
||||
pool: &web::Data<Pool>,
|
||||
) -> Result<Invitation, crate::lib::errors::ServiceError> {
|
||||
use crate::schema::invitations::dsl::invitations;
|
||||
|
||||
let new_invitation: Invitation = eml.into();
|
||||
let conn: &PgConnection = &pool.get().unwrap();
|
||||
|
||||
let inserted_invitation = diesel::insert_into(invitations)
|
||||
.values(&new_invitation)
|
||||
.get_result(conn)?;
|
||||
|
||||
Ok(inserted_invitation)
|
||||
}
|
@@ -7,9 +7,12 @@
|
||||
|
||||
//! Define all the application handlers
|
||||
|
||||
pub mod auth;
|
||||
pub mod details;
|
||||
pub mod index;
|
||||
pub mod invitation;
|
||||
pub mod packages;
|
||||
pub mod register;
|
||||
|
||||
fn details_url(pkg_name: &str) -> String {
|
||||
let url = format!("/details/{}", pkg_name);
|
||||
|
@@ -16,20 +16,35 @@ use artix_gitea::prelude::Repository;
|
||||
use artix_pkglib::prelude::{
|
||||
get_package, retrieve_providers, Alpm, AlpmList, Dep, DepMod, Package,
|
||||
};
|
||||
use askama::Template;
|
||||
use chrono::prelude::NaiveDateTime;
|
||||
use serde::Serialize;
|
||||
use diesel::prelude::*;
|
||||
use diesel::PgConnection;
|
||||
use lettre::{transport::smtp::authentication::Credentials, Message, SmtpTransport, Transport};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::config::SmtpOptions;
|
||||
use crate::models::PackageWithFlagView;
|
||||
use crate::models::{Package as PackageMeta, PrivatePackage};
|
||||
use crate::{
|
||||
config::{sync_databases, SETTINGS},
|
||||
lib::{database::packages, errors::ArtixWebPackageError},
|
||||
lib::{
|
||||
database::packages,
|
||||
errors::{ArtixWebPackageError, ServiceError},
|
||||
templates::PackageFlagEmail,
|
||||
},
|
||||
models::{PackageFlag, Pool},
|
||||
};
|
||||
|
||||
use super::auth::LoggedUser;
|
||||
|
||||
#[derive(Serialize, PartialEq)]
|
||||
pub(crate) enum DependencyKind {
|
||||
Make,
|
||||
Depend,
|
||||
Opt,
|
||||
Soname,
|
||||
Misc,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
@@ -67,12 +82,15 @@ pub(crate) struct Response {
|
||||
pub version: String,
|
||||
pub description: String,
|
||||
pub last_update: NaiveDateTime,
|
||||
pub flag_on: Option<NaiveDateTime>,
|
||||
pub flagged: bool,
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub(crate) struct ResponseDetail {
|
||||
pub repo: String,
|
||||
pub package_name: String,
|
||||
pub version: String,
|
||||
pub architecture: String,
|
||||
pub description: String,
|
||||
pub upstream_url: String,
|
||||
@@ -87,9 +105,288 @@ pub(crate) struct ResponseDetail {
|
||||
pub sonames: Vec<Dependency>,
|
||||
pub makedepends: Vec<Dependency>,
|
||||
pub optdepends: Vec<Dependency>,
|
||||
pub provides: Vec<Dependency>,
|
||||
pub replaces: Vec<Dependency>,
|
||||
pub required_by: Vec<String>,
|
||||
pub contents: Vec<String>,
|
||||
pub maintainers: Vec<String>,
|
||||
pub flagged: bool,
|
||||
pub flagged_on: i64,
|
||||
pub flagged_by: Vec<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct FlagData {
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct PkgData {
|
||||
pub package_doublet: String,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "flag_package.html")]
|
||||
struct FlagPackageTemplate {
|
||||
package_doublet: String,
|
||||
package_name: String,
|
||||
package_version: String,
|
||||
user_email: String,
|
||||
arch: String,
|
||||
repo: String,
|
||||
generation_time: u128,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "error.html")]
|
||||
struct PackageAlreadyFlaggedTemplate {
|
||||
error_message: String,
|
||||
user_email: String,
|
||||
generation_time: u128,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "error.html")]
|
||||
struct PackageNotFoundTemplate {
|
||||
error_message: String,
|
||||
user_email: String,
|
||||
generation_time: u128,
|
||||
}
|
||||
|
||||
/// Flags the given package as outdated
|
||||
///
|
||||
/// # Notes
|
||||
///
|
||||
/// The URL path paremeter is: `package_name`
|
||||
pub async fn flag_package(
|
||||
data: web::Path<(String, String)>,
|
||||
flag_data: web::Form<FlagData>,
|
||||
logged_user: LoggedUser,
|
||||
pool: web::Data<Pool>,
|
||||
smtp_cfg: web::Data<SmtpOptions>,
|
||||
) -> Result<HttpResponse, actix_web::Error> {
|
||||
let start_time = std::time::Instant::now();
|
||||
let user_name = logged_user.email.clone();
|
||||
let package_doublet = format!("{}-{}", data.0, data.1);
|
||||
web::block(move || {
|
||||
flag_package_query(&format!("{}-{}", data.0, data.1), &logged_user.email, &pool)
|
||||
})
|
||||
.await??;
|
||||
|
||||
let email = Message::builder()
|
||||
.from(
|
||||
"ArtixWeb Packages <artixweb@artixlinux.org>"
|
||||
.parse()
|
||||
.unwrap(),
|
||||
)
|
||||
.to("artix-dev@artixlinux.org".parse().unwrap())
|
||||
.subject(format!(
|
||||
"Package {} has been flagged as ot-of-date",
|
||||
package_doublet
|
||||
))
|
||||
.body(
|
||||
PackageFlagEmail {
|
||||
package_name: package_doublet,
|
||||
flag_by: user_name,
|
||||
message: flag_data.message.clone(),
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let smtp_credentials = Credentials::new(smtp_cfg.user.clone(), smtp_cfg.password.clone());
|
||||
let mailer = if let Ok(transport) = SmtpTransport::starttls_relay(&smtp_cfg.relay.clone()) {
|
||||
transport.credentials(smtp_credentials).build()
|
||||
} else {
|
||||
// open a local connection on port 25
|
||||
SmtpTransport::unencrypted_localhost()
|
||||
};
|
||||
|
||||
// send the email
|
||||
match mailer.send(&email) {
|
||||
Ok(_) => Ok(HttpResponse::Ok().finish()),
|
||||
Err(e) => {
|
||||
log::error!("{}", e);
|
||||
Err(crate::lib::errors::ServiceError::BadRequest(
|
||||
format!("could not send email: {:?}", e),
|
||||
Some(start_time),
|
||||
)
|
||||
.into())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// delete the given package flags metadata
|
||||
///
|
||||
/// # Example Query
|
||||
///
|
||||
/// ```text
|
||||
/// curl -X DELETE -H "X-Admin-Token: <token>"" https://packages.artixlinux.org/akonadi/21.12.3-2
|
||||
/// ```
|
||||
pub async fn flag_package_delete(
|
||||
req: actix_web::HttpRequest,
|
||||
data: web::Path<(String, String)>,
|
||||
pool: web::Data<Pool>,
|
||||
) -> HttpResponse {
|
||||
let package_doublet = format!("{}-{}", data.0, data.1);
|
||||
if let Some(token) = req.headers().get("x-admin-token") {
|
||||
if token != &SETTINGS.api_token {
|
||||
return HttpResponse::Unauthorized().finish();
|
||||
}
|
||||
}
|
||||
|
||||
if let Ok(result) = web::block(move || flag_package_delete_query(&package_doublet, &pool)).await
|
||||
{
|
||||
match result {
|
||||
Ok(_) => HttpResponse::Ok().finish(),
|
||||
Err(_) => HttpResponse::NotFound().finish(),
|
||||
}
|
||||
} else {
|
||||
HttpResponse::InternalServerError().finish()
|
||||
}
|
||||
}
|
||||
|
||||
// makes the query to delete he flag package from the database
|
||||
fn flag_package_delete_query(
|
||||
package_doublet: &str,
|
||||
pool: &web::Data<Pool>,
|
||||
) -> Result<(), crate::lib::errors::ServiceError> {
|
||||
use crate::schema::package_flags::dsl::{package_flags, package_name};
|
||||
|
||||
let conn: &PgConnection = &pool.get().unwrap();
|
||||
diesel::delete(package_flags.filter(package_name.eq(package_doublet))).execute(conn)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
// create a new PackageFlag instance and insert it into the database
|
||||
fn flag_package_query(
|
||||
package_doublet: &str,
|
||||
email: &str,
|
||||
pool: &web::Data<Pool>,
|
||||
) -> Result<(), crate::lib::errors::ServiceError> {
|
||||
use crate::schema::package_flags::dsl::package_flags;
|
||||
|
||||
let pf = PackageFlag::from_details(email, package_doublet);
|
||||
let conn: &PgConnection = &pool.get().unwrap();
|
||||
diesel::insert_into(package_flags)
|
||||
.values(&pf)
|
||||
.execute(conn)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Present a form for registered users to flag a package
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// If the package can not be found returns a 404 error
|
||||
pub async fn flags_package_ui(
|
||||
data: web::Path<(String, String)>,
|
||||
logged_user: LoggedUser,
|
||||
pool: web::Data<Pool>,
|
||||
) -> Result<HttpResponse, actix_web::Error> {
|
||||
// let package_name = package_name.into_inner();
|
||||
let start_time = std::time::Instant::now();
|
||||
let package_name = data.0.clone();
|
||||
let package_version = data.1.clone();
|
||||
let package_doublet = format!("{}-{}", package_name, package_version);
|
||||
|
||||
// check if this package has been already flagged by the user
|
||||
let pkgname = data.0.clone();
|
||||
let pkgver = data.1.clone();
|
||||
let user_email = logged_user.email.clone();
|
||||
let result = web::block(move || {
|
||||
get_package_flags_query(
|
||||
&format!("{}-{}", pkgname, pkgver),
|
||||
&user_email,
|
||||
&pool,
|
||||
start_time,
|
||||
)
|
||||
})
|
||||
.await??;
|
||||
|
||||
if result {
|
||||
// the user has already flag this package_name (plus version)
|
||||
return Ok(HttpResponse::Ok().content_type("text/html").body(
|
||||
PackageAlreadyFlaggedTemplate {
|
||||
error_message: format!("You have already flag package {}", package_doublet),
|
||||
user_email: logged_user.email,
|
||||
generation_time: start_time.elapsed().as_millis(),
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
));
|
||||
}
|
||||
|
||||
let alpm = Alpm::new("/", &SETTINGS.databases_path);
|
||||
if let Ok(handle) = alpm {
|
||||
sync_databases(&handle);
|
||||
let pkg_data = get_package(&handle, &package_name);
|
||||
if let Ok(pkg) = pkg_data {
|
||||
return Ok(HttpResponse::Ok().content_type("text/html").body(
|
||||
FlagPackageTemplate {
|
||||
package_doublet,
|
||||
package_version,
|
||||
package_name: data.0.clone(),
|
||||
user_email: logged_user.email,
|
||||
arch: if let Some(arch) = pkg.arch() {
|
||||
String::from(arch)
|
||||
} else {
|
||||
String::from("unknown")
|
||||
},
|
||||
repo: if let Some(db) = pkg.db() {
|
||||
String::from(db.name())
|
||||
} else {
|
||||
String::from("unknown")
|
||||
},
|
||||
generation_time: start_time.elapsed().as_millis(),
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
return Ok(HttpResponse::NotFound().content_type("text/html").body(
|
||||
PackageNotFoundTemplate {
|
||||
user_email: logged_user.email,
|
||||
error_message: format!(
|
||||
"The specified package {} could not be found in the Artix packages database.",
|
||||
package_name
|
||||
),
|
||||
generation_time: start_time.elapsed().as_millis(),
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
));
|
||||
}
|
||||
|
||||
fn get_package_flags_query(
|
||||
pkg_id: &str,
|
||||
email: &str,
|
||||
pool: &web::Data<Pool>,
|
||||
start_time: std::time::Instant,
|
||||
) -> Result<bool, crate::lib::errors::ServiceError> {
|
||||
use crate::schema::package_flags::dsl::{package_flags, package_name, user_email};
|
||||
|
||||
let conn: &PgConnection = &pool.get().unwrap();
|
||||
package_flags
|
||||
.filter(package_name.eq_all(pkg_id))
|
||||
.filter(user_email.eq_all(email))
|
||||
.load::<PackageFlag>(conn)
|
||||
.map_err(|_db_error| {
|
||||
ServiceError::BadRequest(
|
||||
format!("Invalid Package {} and email {}", pkg_id, email),
|
||||
Some(start_time),
|
||||
)
|
||||
})
|
||||
.map(|mut result| {
|
||||
if result.pop().is_some() {
|
||||
return true;
|
||||
}
|
||||
false
|
||||
})
|
||||
}
|
||||
|
||||
/// Get list of packages orered in descending alphabetic order
|
||||
@@ -101,7 +398,7 @@ pub(crate) struct ResponseDetail {
|
||||
pub async fn get_packages<'a>(
|
||||
data: web::Path<(String, usize, usize)>,
|
||||
) -> Result<HttpResponse, ArtixWebPackageError> {
|
||||
if let Ok(result) = get_packages_inner(&data.0, data.1, data.2, None) {
|
||||
if let Ok(result) = get_packages_inner(&data.0, data.1, data.2, None, None).await {
|
||||
return Ok(HttpResponse::Ok()
|
||||
.content_type("application/json; charset=utf-8")
|
||||
.insert_header(("X-Packages-Number", result.0.len()))
|
||||
@@ -114,8 +411,9 @@ pub async fn get_packages<'a>(
|
||||
/// Get the given package details
|
||||
pub async fn get_package_details(
|
||||
data: web::Path<String>,
|
||||
pool: web::Data<Pool>,
|
||||
) -> Result<HttpResponse, ArtixWebPackageError> {
|
||||
match get_packages_details_inner(&data).await {
|
||||
match get_packages_details_inner(&data, pool).await {
|
||||
Ok(result) => Ok(HttpResponse::Ok()
|
||||
.content_type("application/json; charset=utf-8")
|
||||
.json(web::Json(result))),
|
||||
@@ -123,9 +421,10 @@ pub async fn get_package_details(
|
||||
}
|
||||
}
|
||||
|
||||
// #[allow(clippy::too_many_lines)]
|
||||
#[allow(clippy::too_many_lines)] // tell the linter to chill out a bit
|
||||
pub(crate) async fn get_packages_details_inner(
|
||||
package_name: &str,
|
||||
pool: web::Data<Pool>,
|
||||
) -> Result<ResponseDetail, ArtixWebPackageError> {
|
||||
let alpm = Alpm::new("/", &SETTINGS.databases_path);
|
||||
if let Ok(handle) = alpm {
|
||||
@@ -140,6 +439,7 @@ pub(crate) async fn get_packages_details_inner(
|
||||
String::from("unknown")
|
||||
},
|
||||
package_name: pkg.name().to_string(),
|
||||
version: pkg.version().to_string(),
|
||||
architecture: pkg.arch().unwrap_or("").to_string(),
|
||||
description: pkg.desc().unwrap_or("").to_string(),
|
||||
upstream_url: pkg.url().unwrap_or("").to_string(),
|
||||
@@ -157,7 +457,7 @@ pub(crate) async fn get_packages_details_inner(
|
||||
.iter()
|
||||
.map(std::string::ToString::to_string)
|
||||
.collect(),
|
||||
gitea_url: String::new(),
|
||||
gitea_url: format!("{}/packages/{}", SETTINGS.gitea_url, pkg.name().to_string()),
|
||||
dependencies: get_depends_from_package(
|
||||
&handle,
|
||||
pkg.arch().unwrap_or("any"),
|
||||
@@ -194,14 +494,45 @@ pub(crate) async fn get_packages_details_inner(
|
||||
.map(|file| file.name().to_string())
|
||||
.collect(),
|
||||
maintainers: Vec::new(),
|
||||
provides: get_depends_from_package(
|
||||
&handle,
|
||||
pkg.arch().unwrap_or("any"),
|
||||
pkg.provides(),
|
||||
&DependencyKind::Misc,
|
||||
),
|
||||
replaces: get_depends_from_package(
|
||||
&handle,
|
||||
pkg.arch().unwrap_or("any"),
|
||||
pkg.replaces(),
|
||||
&DependencyKind::Misc,
|
||||
),
|
||||
flagged: false,
|
||||
flagged_on: 0,
|
||||
flagged_by: Vec::new(),
|
||||
};
|
||||
|
||||
// retrieve a valid organization name for gitea packages
|
||||
let org_name = get_org_name(&pkg);
|
||||
let pkg_name_version = format!("{}-{}", pkg.name(), pkg.version());
|
||||
let p_pool = pool.clone();
|
||||
if let Ok(Ok(metadata)) =
|
||||
web::block(move || get_package_metadata(&pkg_name_version, &pool)).await
|
||||
{
|
||||
result.last_updated = metadata.last_update.timestamp();
|
||||
result.flagged = metadata.flagged;
|
||||
if let Some(flagged_on) = metadata.flag_on {
|
||||
result.flagged_on = flagged_on.timestamp_millis();
|
||||
}
|
||||
result.flagged_by = metadata
|
||||
.flaggers
|
||||
.iter()
|
||||
.filter(|f| f.is_some())
|
||||
.map(|f| f.clone().unwrap())
|
||||
.collect();
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
// look for this package in gitea
|
||||
if let Some(gitea_repo) = Repository::retrieve_repository(
|
||||
&org_name,
|
||||
&String::from("packages"),
|
||||
package_name,
|
||||
Some(&SETTINGS.gitea_api_url),
|
||||
)
|
||||
@@ -211,6 +542,18 @@ pub(crate) async fn get_packages_details_inner(
|
||||
result.gitea_url = gitea_repo.html_url();
|
||||
// result.maintainers = retrieve_maintainers(&gitea_repo).await;
|
||||
}
|
||||
// add package metadata to database (if we are here it means database has not been filled yet)
|
||||
let metadata = PackageMeta {
|
||||
package_name: format!("{}-{}", pkg.name(), pkg.version()),
|
||||
gitea_url: result.gitea_url.clone(),
|
||||
last_update: chrono::NaiveDateTime::from_timestamp(result.last_updated, 0),
|
||||
};
|
||||
if web::block(move || add_package_metadata(&metadata, &p_pool))
|
||||
.await
|
||||
.is_err()
|
||||
{
|
||||
dbg!("Some error occurred while storing package metadata at async level");
|
||||
}
|
||||
return Ok(result);
|
||||
}
|
||||
} else {
|
||||
@@ -222,39 +565,66 @@ pub(crate) async fn get_packages_details_inner(
|
||||
Err(ArtixWebPackageError::NotFound)
|
||||
}
|
||||
|
||||
// retrieves a valid (hopefully) organization name from a package name or base
|
||||
fn get_org_name(pkg: &Package<'_>) -> String {
|
||||
let mut pkg_name = pkg.name();
|
||||
let mut org_name = if let Some(base) = pkg.base() {
|
||||
pkg_name = base;
|
||||
format!(
|
||||
"packages{}",
|
||||
base.to_uppercase().chars().next().unwrap_or('_')
|
||||
)
|
||||
} else {
|
||||
format!(
|
||||
"packages{}",
|
||||
pkg_name.to_uppercase().chars().next().unwrap_or('_')
|
||||
)
|
||||
};
|
||||
// adds package metadata into the local postgres database
|
||||
fn add_package_metadata(metadata: &PackageMeta, pool: &web::Data<Pool>) {
|
||||
use crate::schema::packages::dsl::packages;
|
||||
|
||||
if pkg_name.starts_with("python-") {
|
||||
org_name = String::from("packagesPython");
|
||||
} else if pkg.name().starts_with("perl-") {
|
||||
org_name = String::from("packagesPerl");
|
||||
} else if pkg.name().starts_with("ruby-") {
|
||||
org_name = String::from("packagesRuby");
|
||||
let conn: &PgConnection = &pool.get().unwrap();
|
||||
if let Err(err) = diesel::insert_into(packages).values(metadata).execute(conn) {
|
||||
dbg!(err.to_string());
|
||||
}
|
||||
|
||||
org_name
|
||||
}
|
||||
|
||||
pub(crate) fn get_packages_inner(
|
||||
// retrieves package metadata from the local postgres database
|
||||
fn get_package_metadata<'a>(
|
||||
pkg_doublet: &'a str,
|
||||
pool: &'a web::Data<Pool>,
|
||||
) -> Result<PrivatePackage, crate::lib::errors::ServiceError> {
|
||||
use crate::schema::{package_flags, packages};
|
||||
|
||||
let start_time = std::time::Instant::now();
|
||||
let conn: &PgConnection = &pool.get().unwrap();
|
||||
let metadata: Vec<(PackageMeta, Option<chrono::NaiveDateTime>, Option<String>)> =
|
||||
packages::table
|
||||
.find(pkg_doublet)
|
||||
.left_join(
|
||||
package_flags::table.on(package_flags::package_name
|
||||
.eq(packages::package_name)
|
||||
.and(package_flags::flag_on.ge(packages::last_update))),
|
||||
)
|
||||
.select((
|
||||
packages::all_columns,
|
||||
package_flags::flag_on.nullable(),
|
||||
package_flags::user_email.nullable(),
|
||||
))
|
||||
.load(conn)
|
||||
.map_err(|_db_error| {
|
||||
ServiceError::BadRequest(
|
||||
format!("Could not find package {}", pkg_doublet),
|
||||
Some(start_time),
|
||||
)
|
||||
})?;
|
||||
|
||||
if let Some(user_metadata) = metadata.first() {
|
||||
let flaggers = metadata.iter().map(|meta| meta.2.clone()).collect();
|
||||
Ok(PrivatePackage::from(
|
||||
&user_metadata.0,
|
||||
user_metadata.1,
|
||||
flaggers,
|
||||
))
|
||||
} else {
|
||||
Err(ServiceError::InternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) async fn get_packages_inner(
|
||||
repo_names: &str,
|
||||
limit: usize,
|
||||
offset: usize,
|
||||
keyword: Option<&str>,
|
||||
pool: Option<&web::Data<Pool>>,
|
||||
) -> Result<(Vec<Response>, usize), ArtixWebPackageError> {
|
||||
let start_time = std::time::Instant::now();
|
||||
let alpm = Alpm::new("/", &SETTINGS.databases_path);
|
||||
|
||||
if let Ok(handle) = alpm {
|
||||
@@ -277,7 +647,27 @@ pub(crate) fn get_packages_inner(
|
||||
}
|
||||
let pkgs = pkgs.unwrap();
|
||||
|
||||
let flagged_packages = if let Some(pool) = pool {
|
||||
if let Ok(flagged_packages) = get_flagged_packages(start_time, pool).await {
|
||||
flagged_packages
|
||||
} else {
|
||||
Vec::new()
|
||||
}
|
||||
} else {
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
for pkg in pkgs.packages() {
|
||||
let pkg_doublet = format!("{}-{}", pkg.name(), pkg.version());
|
||||
let (flagged, flag_on) = if let Some(flag_data) = flagged_packages
|
||||
.iter()
|
||||
.find(|p| p.package_name == pkg_doublet)
|
||||
{
|
||||
(true, flag_data.flag_on)
|
||||
} else {
|
||||
(false, None)
|
||||
};
|
||||
|
||||
result.push(Response {
|
||||
repository: if pkg.db().is_none() {
|
||||
String::from("unknown")
|
||||
@@ -288,6 +678,8 @@ pub(crate) fn get_packages_inner(
|
||||
version: pkg.version().as_str().to_string(),
|
||||
last_update: NaiveDateTime::from_timestamp(pkg.build_date(), 0),
|
||||
description: String::from(pkg.desc().unwrap_or_default()),
|
||||
flag_on,
|
||||
flagged,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -300,6 +692,35 @@ pub(crate) fn get_packages_inner(
|
||||
Err(ArtixWebPackageError::NotFound)
|
||||
}
|
||||
|
||||
// get all flagged packages from the postgres database
|
||||
async fn get_flagged_packages(
|
||||
start_time: std::time::Instant,
|
||||
pool: &web::Data<Pool>,
|
||||
) -> Result<Vec<PackageWithFlagView>, crate::lib::errors::ServiceError> {
|
||||
use crate::schema::package_flags::dsl::{
|
||||
flag_on, package_flags, package_name as package_flag_name,
|
||||
};
|
||||
use crate::schema::packages::dsl::{last_update, package_name, packages};
|
||||
|
||||
let conn: &PgConnection = &pool.get().unwrap();
|
||||
let packages_data: Vec<(PackageMeta, Option<chrono::NaiveDateTime>)> = packages
|
||||
.inner_join(
|
||||
package_flags.on(flag_on
|
||||
.ge(last_update)
|
||||
.and(package_name.eq(package_flag_name))),
|
||||
)
|
||||
.select((packages::all_columns(), flag_on.nullable()))
|
||||
.load(conn)
|
||||
.map_err(|_db_error| {
|
||||
ServiceError::BadRequest(String::from("Could not find packages"), Some(start_time))
|
||||
})?;
|
||||
|
||||
Ok(packages_data
|
||||
.iter()
|
||||
.map(|pkg_data| PackageWithFlagView::from(&pkg_data.0, pkg_data.1))
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
async fn retrieve_maintainers(gitea_repo: &Repository) -> Vec<String> {
|
||||
let mut maintainers = Vec::new();
|
||||
@@ -330,7 +751,7 @@ async fn retrieve_maintainers(gitea_repo: &Repository) -> Vec<String> {
|
||||
fn get_depends_from_package<'a>(
|
||||
alpm: &'a Alpm,
|
||||
arch: &str,
|
||||
deps: AlpmList<'_, Dep<'_>>,
|
||||
deps: AlpmList<'_, &Dep>,
|
||||
kind: &DependencyKind,
|
||||
) -> Vec<Dependency> {
|
||||
match kind {
|
||||
@@ -347,12 +768,16 @@ fn get_depends_from_package<'a>(
|
||||
.iter()
|
||||
.map(|dep| construct_opt_dependency(arch, alpm, dep))
|
||||
.collect(),
|
||||
DependencyKind::Misc => deps
|
||||
.iter()
|
||||
.map(|dep| construct_dependency(arch, alpm, dep))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
// constructs a Dependency instance with the given data and returns it back
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn construct_dependency(arch: &str, alpm: &Alpm, dep: Dep<'_>) -> Dependency {
|
||||
fn construct_dependency(arch: &str, alpm: &Alpm, dep: &Dep) -> Dependency {
|
||||
let kind = if dep
|
||||
.name()
|
||||
.rsplit('.')
|
||||
@@ -365,25 +790,25 @@ fn construct_dependency(arch: &str, alpm: &Alpm, dep: Dep<'_>) -> Dependency {
|
||||
DependencyKind::Depend
|
||||
};
|
||||
|
||||
common_dependency_data(arch, alpm, &dep, kind)
|
||||
common_dependency_data(arch, alpm, dep, kind)
|
||||
}
|
||||
|
||||
// constructs a (make) Dependency instance with the given data and return it back
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn construct_make_dependency(arch: &str, alpm: &Alpm, dep: Dep<'_>) -> Dependency {
|
||||
common_dependency_data(arch, alpm, &dep, DependencyKind::Make)
|
||||
fn construct_make_dependency(arch: &str, alpm: &Alpm, dep: &Dep) -> Dependency {
|
||||
common_dependency_data(arch, alpm, dep, DependencyKind::Make)
|
||||
}
|
||||
|
||||
// constructs a (opt) Dependency instance with the given data and return it back
|
||||
#[allow(clippy::needless_pass_by_value)]
|
||||
fn construct_opt_dependency(arch: &str, alpm: &Alpm, dep: Dep<'_>) -> Dependency {
|
||||
common_dependency_data(arch, alpm, &dep, DependencyKind::Opt)
|
||||
fn construct_opt_dependency(arch: &str, alpm: &Alpm, dep: &Dep) -> Dependency {
|
||||
common_dependency_data(arch, alpm, dep, DependencyKind::Opt)
|
||||
}
|
||||
|
||||
fn common_dependency_data<'a>(
|
||||
arch: &str,
|
||||
alpm: &'a Alpm,
|
||||
dep: &Dep<'_>,
|
||||
dep: &Dep,
|
||||
kind: DependencyKind,
|
||||
) -> Dependency {
|
||||
let ver = if let Some(ver) = dep.version() {
|
||||
|
190
crates/artixweb-packages/src/handlers/register.rs
Normal file
190
crates/artixweb-packages/src/handlers/register.rs
Normal file
@@ -0,0 +1,190 @@
|
||||
/*
|
||||
This file is part of Artix Web Packages. See LICENSE file for details
|
||||
|
||||
Copyright (c) 2022 - Artix Linux
|
||||
Copyright (c) 2022 - Oscar Campos <damnwidget@artixlinux.org>
|
||||
*/
|
||||
|
||||
use actix_session::Session;
|
||||
use actix_web::{http, web, HttpResponse};
|
||||
use askama::Template;
|
||||
use diesel::prelude::*;
|
||||
use diesel::PgConnection;
|
||||
use serde::Deserialize;
|
||||
|
||||
use crate::config::{check_password_strength, hash_password};
|
||||
use crate::lib::errors::ServiceError;
|
||||
use crate::models::{Invitation, Pool, SlimUser, User};
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
pub struct UserData {
|
||||
pub password: String,
|
||||
pub repeat_password: String,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "base_register.html")]
|
||||
struct BaseRegisterTemplate {
|
||||
invitation_id: String,
|
||||
user_email: String,
|
||||
generation_time: u128,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "register.html")]
|
||||
struct RegisterTemplate {
|
||||
invitation_id: String,
|
||||
user_email: String,
|
||||
generation_time: u128,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "invalid_invitation.html")]
|
||||
struct InvalidInvitationTemplate {
|
||||
invitation_id: String,
|
||||
user_email: String,
|
||||
generation_time: u128,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "weak_password.html")]
|
||||
struct WeakPasswordTemplate {
|
||||
invitation_id: String,
|
||||
user_email: String,
|
||||
generation_time: u128,
|
||||
}
|
||||
|
||||
#[allow(clippy::unused_async)]
|
||||
pub async fn invitation(
|
||||
invitation_id: web::Path<String>,
|
||||
pool: web::Data<Pool>,
|
||||
session: Session,
|
||||
) -> Result<HttpResponse, actix_web::Error> {
|
||||
let start_time = std::time::Instant::now();
|
||||
if session.get::<String>("user_email").unwrap().is_some() {
|
||||
return Ok(HttpResponse::SeeOther()
|
||||
.append_header((http::header::LOCATION, "/"))
|
||||
.finish());
|
||||
}
|
||||
|
||||
let valid_invitation = check_invitation(&invitation_id, &pool)?;
|
||||
|
||||
let s = if valid_invitation {
|
||||
RegisterTemplate {
|
||||
user_email: String::new(),
|
||||
invitation_id: invitation_id.into_inner(),
|
||||
generation_time: start_time.elapsed().as_millis(),
|
||||
}
|
||||
.render()
|
||||
.unwrap()
|
||||
} else {
|
||||
InvalidInvitationTemplate {
|
||||
user_email: String::new(),
|
||||
invitation_id: invitation_id.into_inner(),
|
||||
generation_time: start_time.elapsed().as_millis(),
|
||||
}
|
||||
.render()
|
||||
.unwrap()
|
||||
};
|
||||
|
||||
Ok(HttpResponse::Ok().content_type("text/html").body(s))
|
||||
}
|
||||
|
||||
fn check_invitation(
|
||||
invitation_id: &str,
|
||||
pool: &web::Data<Pool>,
|
||||
) -> Result<bool, crate::lib::errors::ServiceError> {
|
||||
use crate::schema::invitations::dsl::{id, invitations};
|
||||
|
||||
let invitation_id = uuid::Uuid::parse_str(invitation_id)?;
|
||||
let conn: &PgConnection = &pool.get().unwrap();
|
||||
|
||||
let inv = invitations
|
||||
.filter(id.eq(invitation_id))
|
||||
.load::<Invitation>(conn)
|
||||
.map_err(|_db_error| {
|
||||
ServiceError::BadRequest("Invalid or expired Invitation".into(), None)
|
||||
})?;
|
||||
|
||||
Ok(!inv.is_empty())
|
||||
}
|
||||
|
||||
pub async fn new_user(
|
||||
invitation_id: web::Path<String>,
|
||||
user_data: web::Form<UserData>,
|
||||
pool: web::Data<Pool>,
|
||||
) -> Result<HttpResponse, actix_web::Error> {
|
||||
let start_time = std::time::Instant::now();
|
||||
// check that the password is "good enough"
|
||||
let user_data = user_data.into_inner();
|
||||
let password = user_data.password;
|
||||
let repeat_password = user_data.repeat_password;
|
||||
|
||||
if password != repeat_password {
|
||||
return Err(ServiceError::BadRequest(
|
||||
String::from("Introduced passwords does not match!"),
|
||||
Some(start_time),
|
||||
)
|
||||
.into());
|
||||
}
|
||||
|
||||
if !check_password_strength(&password) {
|
||||
return Ok(HttpResponse::Ok().content_type("text/html").body(
|
||||
WeakPasswordTemplate {
|
||||
user_email: String::new(),
|
||||
invitation_id: invitation_id.into_inner(),
|
||||
generation_time: start_time.elapsed().as_millis(),
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
));
|
||||
}
|
||||
|
||||
// create user from the input data in the form
|
||||
web::block(move || query(&invitation_id.into_inner(), &password, &pool)).await??;
|
||||
Ok(HttpResponse::SeeOther()
|
||||
.append_header((http::header::LOCATION, "/login"))
|
||||
.finish())
|
||||
}
|
||||
|
||||
fn query(
|
||||
invitation_id: &str,
|
||||
password: &str,
|
||||
pool: &web::Data<Pool>,
|
||||
) -> Result<SlimUser, crate::lib::errors::ServiceError> {
|
||||
use crate::schema::invitations::dsl::{id, invitations};
|
||||
use crate::schema::users::dsl::users;
|
||||
|
||||
let start_time = std::time::Instant::now();
|
||||
let invitation_id = uuid::Uuid::parse_str(invitation_id)?;
|
||||
let conn: &PgConnection = &pool.get().unwrap();
|
||||
invitations
|
||||
.filter(id.eq(invitation_id))
|
||||
.load::<Invitation>(conn)
|
||||
.map_err(|_db_error| {
|
||||
ServiceError::BadRequest(
|
||||
format!("Invalid Invitation ID: {}", invitation_id),
|
||||
Some(start_time),
|
||||
)
|
||||
})
|
||||
.and_then(|mut result| {
|
||||
if let Some(invitation) = result.pop() {
|
||||
// if invitation is not expired
|
||||
if invitation.expires_at > chrono::Local::now().naive_local() {
|
||||
// try hashing the password, else return the error that will be converted to ServiceError
|
||||
let password = hash_password(password)?;
|
||||
|
||||
let user = User::from_details(invitation.email, password);
|
||||
let inserted_user: User =
|
||||
diesel::insert_into(users).values(&user).get_result(conn)?;
|
||||
|
||||
return Ok(inserted_user.into());
|
||||
}
|
||||
}
|
||||
|
||||
Err(ServiceError::BadRequest(
|
||||
format!("Invalid Invitation ID: {}", invitation_id),
|
||||
Some(start_time),
|
||||
))
|
||||
})
|
||||
}
|
@@ -10,7 +10,10 @@ use actix_web::{
|
||||
http::{header::ContentType, StatusCode},
|
||||
HttpResponse,
|
||||
};
|
||||
use askama::Template;
|
||||
use derive_more::Display;
|
||||
use diesel::result::{DatabaseErrorKind, Error as DBError};
|
||||
use uuid::Error as ParseError;
|
||||
|
||||
#[derive(Debug, Display)]
|
||||
pub enum ArtixWebPackageError {
|
||||
@@ -35,3 +38,91 @@ impl error::ResponseError for ArtixWebPackageError {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Display)]
|
||||
pub enum ServiceError {
|
||||
#[display(fmt = "Internal Server Error")]
|
||||
InternalServerError,
|
||||
|
||||
#[display(fmt = "BadRequest: {}", _0)]
|
||||
BadRequest(String, Option<std::time::Instant>),
|
||||
|
||||
#[display(fmt = "Unauthorized")]
|
||||
Unauthorized,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "error.html")]
|
||||
struct AlreadyExistsTemplate {
|
||||
error_message: String,
|
||||
user_email: String,
|
||||
generation_time: u128,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "error.html")]
|
||||
struct UnauthorizedTemplate {
|
||||
error_message: String,
|
||||
user_email: String,
|
||||
generation_time: u128,
|
||||
}
|
||||
|
||||
impl error::ResponseError for ServiceError {
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
match self {
|
||||
ServiceError::InternalServerError => HttpResponse::InternalServerError()
|
||||
.reason("Internal Server Error, Please try later")
|
||||
.body("Internal Server Error, Please try later"),
|
||||
ServiceError::BadRequest(ref message, start_time) => {
|
||||
let t = if let Some(start_time) = start_time {
|
||||
start_time.elapsed().as_millis()
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
HttpResponse::BadRequest().body(
|
||||
AlreadyExistsTemplate {
|
||||
user_email: String::new(),
|
||||
error_message: message.clone(),
|
||||
generation_time: t,
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
ServiceError::Unauthorized => HttpResponse::Unauthorized().reason("Unauthorized").body(
|
||||
UnauthorizedTemplate {
|
||||
user_email: String::new(),
|
||||
error_message: String::from("You are not authorized to access this resource"),
|
||||
generation_time: 0,
|
||||
}
|
||||
.render()
|
||||
.unwrap(),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ParseError> for ServiceError {
|
||||
fn from(_: ParseError) -> ServiceError {
|
||||
ServiceError::BadRequest("Invalid UUID".into(), None)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DBError> for ServiceError {
|
||||
fn from(error: DBError) -> ServiceError {
|
||||
match error {
|
||||
DBError::DatabaseError(kind, info) => {
|
||||
if let DatabaseErrorKind::UniqueViolation = kind {
|
||||
let mut message = info.details().unwrap_or_else(|| info.message()).to_string();
|
||||
if message.contains("Key (email)=") {
|
||||
message = format!("Can not register user, already exists: {}", message);
|
||||
}
|
||||
return ServiceError::BadRequest(message, None);
|
||||
}
|
||||
ServiceError::InternalServerError
|
||||
}
|
||||
_ => ServiceError::InternalServerError,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -7,9 +7,26 @@
|
||||
|
||||
use askama::Template;
|
||||
|
||||
use crate::models::Invitation;
|
||||
|
||||
/// `BaseTemplate` is used as base canvas for the included simple UI
|
||||
#[derive(Template)]
|
||||
#[template(path = "base.html")]
|
||||
pub struct BaseTemplate {
|
||||
user_email: String,
|
||||
generation_time: u128,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "invitation_email.html")]
|
||||
pub struct EmailTemplate<'a> {
|
||||
pub(crate) invitation: &'a Invitation,
|
||||
}
|
||||
|
||||
#[derive(Template)]
|
||||
#[template(path = "package_flag_email.html")]
|
||||
pub struct PackageFlagEmail {
|
||||
pub(crate) package_name: String,
|
||||
pub(crate) flag_by: String,
|
||||
pub(crate) message: String,
|
||||
}
|
||||
|
@@ -15,19 +15,28 @@
|
||||
)]
|
||||
#![warn(clippy::all, clippy::pedantic)]
|
||||
|
||||
#[macro_use]
|
||||
extern crate diesel;
|
||||
|
||||
mod config;
|
||||
mod handlers;
|
||||
mod lib;
|
||||
mod models;
|
||||
mod routes;
|
||||
mod schema;
|
||||
|
||||
// use actix_session::{CookieSession, Session};
|
||||
use actix_session::CookieSession;
|
||||
use actix_identity::{CookieIdentityPolicy, IdentityService};
|
||||
use actix_session::{storage::CookieSessionStore, SessionMiddleware};
|
||||
use actix_web::{
|
||||
cookie::Key,
|
||||
middleware::{Compress, Logger},
|
||||
App, HttpServer,
|
||||
web, App, HttpServer,
|
||||
};
|
||||
use clap::{arg, command};
|
||||
use diesel::prelude::*;
|
||||
use diesel::r2d2::ConnectionManager;
|
||||
use log::{debug, warn};
|
||||
use time::Duration;
|
||||
|
||||
#[actix_web::main]
|
||||
async fn main() -> std::io::Result<()> {
|
||||
@@ -36,9 +45,37 @@ async fn main() -> std::io::Result<()> {
|
||||
.arg(
|
||||
arg!(-b --bind <BIND_ADDRESS>)
|
||||
.required(false)
|
||||
.default_value("127.0.0.1"),
|
||||
.default_value("127.0.0.1")
|
||||
.env("BIND_ADDRESS"),
|
||||
)
|
||||
.arg(arg!(-p --port <PORT>).required(false).default_value("1936"))
|
||||
.arg(
|
||||
arg!(-u --databaseurl <DATABASE_URL>)
|
||||
.required(false)
|
||||
.default_value("localhost")
|
||||
.env("DATABASE_URL"),
|
||||
)
|
||||
.arg(
|
||||
arg!(-d --domain <DOMAIN>)
|
||||
.required(false)
|
||||
.default_value("localhost"),
|
||||
)
|
||||
.arg(
|
||||
arg!(-k --key <SESSION_KEY>)
|
||||
.required(true)
|
||||
.env("SESSION_KEY"),
|
||||
)
|
||||
.arg(
|
||||
arg!(--smtp_user <SMTP_USER>)
|
||||
.required(true)
|
||||
.env("SMTP_USER"),
|
||||
)
|
||||
.arg(arg!(--smtp_pwd <SMTP_PWD>).required(true).env("SMTP_PWD"))
|
||||
.arg(
|
||||
arg!(--smtp_relay <SMTP_RELAY>)
|
||||
.required(true)
|
||||
.env("SMTP_RELAY"),
|
||||
)
|
||||
.arg(arg!(-v - -verbose ... ))
|
||||
.get_matches();
|
||||
|
||||
@@ -62,23 +99,64 @@ async fn main() -> std::io::Result<()> {
|
||||
1936
|
||||
};
|
||||
|
||||
let database_url = matches.value_of("databaseurl").unwrap_or_default();
|
||||
let domain = matches.value_of("domain").unwrap_or_default();
|
||||
let master_key = matches.value_of("key").unwrap();
|
||||
|
||||
let smtp_config = config::SmtpOptions {
|
||||
user: String::from(matches.value_of("smtp_user").unwrap_or_default()),
|
||||
password: String::from(matches.value_of("smtp_pwd").unwrap_or_default()),
|
||||
relay: String::from(matches.value_of("smtp_relay").unwrap_or_default()),
|
||||
};
|
||||
|
||||
// start http server
|
||||
debug!("starting Artix web server on {}:{}", host_addr, host_port);
|
||||
start_web_server(host_addr, host_port, false).await
|
||||
start_web_server(
|
||||
host_addr,
|
||||
host_port,
|
||||
database_url,
|
||||
master_key.as_bytes(),
|
||||
domain.to_string(),
|
||||
smtp_config,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
// starts the web server with the given configuration
|
||||
async fn start_web_server(
|
||||
host_addr: &str,
|
||||
host_port: u16,
|
||||
secure_cookies: bool,
|
||||
database_url: &str,
|
||||
master_key: &[u8],
|
||||
domain: String,
|
||||
smtp_config: config::SmtpOptions,
|
||||
) -> std::io::Result<()> {
|
||||
// create PostgreSQL connection pool
|
||||
let manager = ConnectionManager::<PgConnection>::new(database_url);
|
||||
let pool: models::Pool = r2d2::Pool::builder()
|
||||
.build(manager)
|
||||
.expect("Failed to create PostgreSQL pool.");
|
||||
|
||||
let secret_key = Key::derive_from(master_key);
|
||||
HttpServer::new(move || {
|
||||
App::new()
|
||||
.app_data(web::Data::new(pool.clone()))
|
||||
.app_data(web::Data::new(smtp_config.clone()))
|
||||
.configure(routes::config_app)
|
||||
.wrap(Logger::default())
|
||||
.wrap(Compress::default())
|
||||
.wrap(CookieSession::signed(&[0; 32]).secure(secure_cookies))
|
||||
.wrap(SessionMiddleware::new(
|
||||
CookieSessionStore::default(),
|
||||
secret_key.clone(),
|
||||
))
|
||||
.wrap(IdentityService::new(
|
||||
CookieIdentityPolicy::new(config::SECRET_KEY.as_bytes())
|
||||
.name("auth")
|
||||
.path("/")
|
||||
.domain(&domain)
|
||||
.max_age(Duration::days(1))
|
||||
.secure(false),
|
||||
))
|
||||
})
|
||||
.bind((host_addr, host_port))?
|
||||
.run()
|
||||
|
150
crates/artixweb-packages/src/models.rs
Normal file
150
crates/artixweb-packages/src/models.rs
Normal file
@@ -0,0 +1,150 @@
|
||||
/*
|
||||
This file is part of Artix Web Packages. See LICENSE file for details
|
||||
|
||||
Copyright (c) 2022 - Artix Linux
|
||||
Copyright (c) 2022 - Oscar Campos <damnwidget@artixlinux.org>
|
||||
*/
|
||||
|
||||
use diesel::{r2d2::ConnectionManager, PgConnection};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::schema::{invitations, package_flags, packages, users};
|
||||
|
||||
// type alias to use in multiple places
|
||||
pub type Pool = r2d2::Pool<ConnectionManager<PgConnection>>;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Queryable, Insertable)]
|
||||
#[table_name = "packages"]
|
||||
pub struct Package {
|
||||
pub package_name: String,
|
||||
pub gitea_url: String,
|
||||
pub last_update: chrono::NaiveDateTime,
|
||||
}
|
||||
|
||||
impl Package {
|
||||
pub fn from_details<S: Into<String>, T: Into<String>>(name: S, url: T) -> Self {
|
||||
Package {
|
||||
package_name: name.into(),
|
||||
gitea_url: url.into(),
|
||||
last_update: chrono::Local::now().naive_local(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct PrivatePackage {
|
||||
pub package_name: String,
|
||||
pub gitea_url: String,
|
||||
pub last_update: chrono::NaiveDateTime,
|
||||
pub flagged: bool,
|
||||
pub flag_on: Option<chrono::NaiveDateTime>,
|
||||
pub flaggers: Vec<Option<String>>,
|
||||
}
|
||||
|
||||
impl PrivatePackage {
|
||||
pub fn from(
|
||||
pkg: &Package,
|
||||
flag_on: Option<chrono::NaiveDateTime>,
|
||||
flaggers: Vec<Option<String>>,
|
||||
) -> Self {
|
||||
PrivatePackage {
|
||||
package_name: pkg.package_name.clone(),
|
||||
gitea_url: pkg.gitea_url.clone(),
|
||||
last_update: pkg.last_update,
|
||||
flagged: flag_on.is_some(),
|
||||
flag_on,
|
||||
flaggers,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct PackageWithFlagView {
|
||||
pub package_name: String,
|
||||
pub gitea_url: String,
|
||||
pub last_update: chrono::NaiveDateTime,
|
||||
pub flag_on: Option<chrono::NaiveDateTime>,
|
||||
}
|
||||
|
||||
impl PackageWithFlagView {
|
||||
pub fn from(pkg: &Package, flag_on: Option<chrono::NaiveDateTime>) -> Self {
|
||||
PackageWithFlagView {
|
||||
package_name: pkg.package_name.clone(),
|
||||
gitea_url: pkg.gitea_url.clone(),
|
||||
last_update: pkg.last_update,
|
||||
flag_on,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Queryable, Insertable)]
|
||||
#[table_name = "users"]
|
||||
pub struct User {
|
||||
pub email: String,
|
||||
pub hash: String,
|
||||
pub created_at: chrono::NaiveDateTime,
|
||||
pub banned: bool,
|
||||
}
|
||||
|
||||
impl User {
|
||||
pub fn from_details<S: Into<String>, T: Into<String>>(email: S, pwd: T) -> Self {
|
||||
User {
|
||||
email: email.into(),
|
||||
hash: pwd.into(),
|
||||
created_at: chrono::Local::now().naive_local(),
|
||||
banned: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Queryable, Insertable)]
|
||||
#[table_name = "invitations"]
|
||||
pub(crate) struct Invitation {
|
||||
pub id: Uuid,
|
||||
pub email: String,
|
||||
pub expires_at: chrono::NaiveDateTime,
|
||||
}
|
||||
|
||||
// any type that implements Into<String> can be used to create an invitation
|
||||
impl<T> From<T> for Invitation
|
||||
where
|
||||
T: Into<String>,
|
||||
{
|
||||
fn from(email: T) -> Self {
|
||||
Invitation {
|
||||
id: uuid::Uuid::new_v4(),
|
||||
email: email.into(),
|
||||
expires_at: chrono::Local::now().naive_local() + chrono::Duration::hours(24),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Queryable, Insertable)]
|
||||
#[table_name = "package_flags"]
|
||||
pub struct PackageFlag {
|
||||
pub user_email: String,
|
||||
pub package_name: String,
|
||||
flag_on: chrono::NaiveDateTime,
|
||||
}
|
||||
|
||||
impl PackageFlag {
|
||||
pub fn from_details<S: Into<String>, T: Into<String>>(user_email: S, package_name: T) -> Self {
|
||||
PackageFlag {
|
||||
user_email: user_email.into(),
|
||||
package_name: package_name.into(),
|
||||
flag_on: chrono::Local::now().naive_local(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct SlimUser {
|
||||
pub email: String,
|
||||
}
|
||||
|
||||
impl From<User> for SlimUser {
|
||||
fn from(user: User) -> Self {
|
||||
SlimUser { email: user.email }
|
||||
}
|
||||
}
|
@@ -8,12 +8,13 @@
|
||||
use actix_files::Files;
|
||||
use actix_web::{guard, web, HttpResponse};
|
||||
|
||||
use crate::handlers::{details, index, packages};
|
||||
use crate::handlers::{auth, details, index, invitation, packages, register};
|
||||
|
||||
/// Configure the application router
|
||||
pub fn config_app(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(
|
||||
web::scope("")
|
||||
.service(web::resource("/api/invitation").route(web::post().to(invitation::post)))
|
||||
.service(
|
||||
web::scope("/api/packages")
|
||||
.service(
|
||||
@@ -31,6 +32,27 @@ pub fn config_app(cfg: &mut web::ServiceConfig) {
|
||||
)
|
||||
.service(web::resource("/").route(web::get().to(index::index)))
|
||||
.service(details::package_details)
|
||||
.service(
|
||||
web::resource("/register/{invitation_id}")
|
||||
.route(web::get().to(register::invitation)),
|
||||
)
|
||||
.service(
|
||||
web::resource("/new_user/{invitation_id}")
|
||||
.route(web::post().to(register::new_user)),
|
||||
)
|
||||
.service(
|
||||
web::resource("/login")
|
||||
.route(web::post().to(auth::login))
|
||||
.route(web::delete().to(auth::logout))
|
||||
.route(web::get().to(auth::ui)),
|
||||
)
|
||||
.service(web::resource("/logout").route(web::post().to(auth::logout)))
|
||||
.service(
|
||||
web::resource("/flag_package/{package_name}/{package_version}")
|
||||
.route(web::post().to(packages::flag_package))
|
||||
.route(web::get().to(packages::flags_package_ui))
|
||||
.route(web::delete().to(packages::flag_package_delete)),
|
||||
)
|
||||
.service(Files::new("/assets/images", "assets/images"))
|
||||
.service(Files::new("/assets/css", "assets/css/")),
|
||||
);
|
||||
|
37
crates/artixweb-packages/src/schema.rs
Normal file
37
crates/artixweb-packages/src/schema.rs
Normal file
@@ -0,0 +1,37 @@
|
||||
table! {
|
||||
invitations (id) {
|
||||
id -> Uuid,
|
||||
email -> Varchar,
|
||||
expires_at -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
package_flags (user_email, package_name) {
|
||||
user_email -> Varchar,
|
||||
package_name -> Varchar,
|
||||
flag_on -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
packages (package_name) {
|
||||
package_name -> Varchar,
|
||||
gitea_url -> Varchar,
|
||||
last_update -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
users (email) {
|
||||
email -> Varchar,
|
||||
hash -> Varchar,
|
||||
created_at -> Timestamp,
|
||||
banned -> Bool,
|
||||
}
|
||||
}
|
||||
|
||||
joinable!(package_flags -> packages (package_name));
|
||||
joinable!(package_flags -> users (user_email));
|
||||
|
||||
allow_tables_to_appear_in_same_query!(invitations, package_flags, packages, users,);
|
@@ -18,6 +18,11 @@
|
||||
<a href = "https://gitea.artixlinux.org">Sources</a>
|
||||
</nav>
|
||||
<!-- Navigation bad end -->
|
||||
{% if !user_email.is_empty() %}
|
||||
<div class="login_logout">
|
||||
<span>Logged as {{ user_email }}(<form action="/logout" method="post" name="logout"><input type="submit" value="logout"/></form>)</span>
|
||||
</div>
|
||||
{% endif %}
|
||||
</header>
|
||||
|
||||
{% block page_content %}{% endblock %}
|
||||
|
@@ -10,11 +10,14 @@
|
||||
<div>
|
||||
<label for="repo_name" title="Repositories to include in the search">Repositories</label>
|
||||
<select id="repo_name" name="repo" multiple="">
|
||||
<option value="World" {% if query|selected_repo("World") %} selected {% endif %}>World</option>
|
||||
<option value="Galaxy" {% if query|selected_repo("Galaxy") %} selected {% endif %}>Galaxy</option>
|
||||
<option value="System" {% if query|selected_repo("System") %} selected {% endif %}>System</option>
|
||||
<option value="Universe" {% if query|selected_repo("Universe") %} selected {% endif %}>Universe</option>
|
||||
<option value="Lib32" {% if query|selected_repo("Lib32") %} selected {% endif %}>Lib32</option>
|
||||
<option value="system" {% if repos|selected_repo("system") %} selected {% endif %}>System</option>
|
||||
<option value="world" {% if repos|selected_repo("world") %} selected {% endif %}>World</option>
|
||||
<option value="galaxy" {% if repos|selected_repo("galaxy") %} selected {% endif %}>Galaxy</option>
|
||||
<option value="lib32" {% if repos|selected_repo("lib32") %} selected {% endif %}>Lib32</option>
|
||||
<option value="system-gremlins" {% if repos|selected_repo("system-gremlins") %} selected {% endif %}>System-Gremlins</option>
|
||||
<option value="world-gremlins" {% if repos|selected_repo("world-gremlins") %} selected {% endif %}>World-Gremlins</option>
|
||||
<option value="galaxy-gremlins" {% if repos|selected_repo("galaxy-gremlins") %} selected {% endif %}>Galaxy-Gremlins</option>
|
||||
<option value="lib32-gremlins" {% if repos|selected_repo("lib32-gremlins") %} selected {% endif %}>Lib32-Gremlins</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
@@ -54,6 +57,7 @@
|
||||
<th>Version</th>
|
||||
<th>Package Description</th>
|
||||
<th>Last Update (Stable)</th>
|
||||
<th>Flagged On</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -65,6 +69,11 @@
|
||||
<td>{{ data.version }}</td>
|
||||
<td>{{ data.description }}</td>
|
||||
<td>{{ data.last_update }}</td>
|
||||
{% if data.flagged %}
|
||||
<td><span class="flagged">{{ data.flag_on.unwrap().format("%Y-%m-%d %H:%M:%S") }}</span></td>
|
||||
{% else %}
|
||||
<td></td>
|
||||
{% endif %}
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
|
18
crates/artixweb-packages/templates/base_register.html
Normal file
18
crates/artixweb-packages/templates/base_register.html
Normal file
@@ -0,0 +1,18 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block page_content %}
|
||||
|
||||
<article role="information" class="panel">
|
||||
<section>
|
||||
<h2>User Registration</h2>
|
||||
<p>You have been invited with the following invitation code <span class="code">{{ invitation_id }}</span> to register into Artix Packages Website as a reporter.</p>
|
||||
<p>This means that you will be able to login into the site and flag packages as outdated. Please, do not abuse of this service and betray the
|
||||
trust that the Artix development team has put in you.</p>
|
||||
<h3>Disclaimer</h3>
|
||||
Users that misuse of this service in any way wll be immediately banned. The Artix team reserves the right of admission.
|
||||
</section>
|
||||
</article>
|
||||
|
||||
{% block register_content %}{% endblock %}
|
||||
|
||||
{% endblock %}
|
15
crates/artixweb-packages/templates/error.html
Normal file
15
crates/artixweb-packages/templates/error.html
Normal file
@@ -0,0 +1,15 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block page_content %}
|
||||
|
||||
<article role="error" class="panel">
|
||||
<section>
|
||||
<h2>Error</h2>
|
||||
<p>{{ error_message }}</p>
|
||||
<form>
|
||||
<input type="button" value="Go back" onclick="history.back()"/>
|
||||
</form>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
{% endblock %}
|
23
crates/artixweb-packages/templates/flag_package.html
Normal file
23
crates/artixweb-packages/templates/flag_package.html
Normal file
@@ -0,0 +1,23 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block page_content %}
|
||||
|
||||
<article role="flag_package" class="panel">
|
||||
<section>
|
||||
<h2>Flag package: {{ package_doublet }} ({{ arch }}) in {{ repo }}</h2>
|
||||
<p>If you are certain there is a newer <strong>stable</strong> release available of this package upstream, please, notify us filling the form below.</p>
|
||||
<form method="post" name="flag_form" action="/flag_package/{{ package_name }}/{{ package_version }}">
|
||||
<fieldset>
|
||||
<div>
|
||||
<label for="message">Add any relevant information:</label>
|
||||
<textarea id="message" name="message" cols="80", rows="10" required></textarea>
|
||||
<label> </label>
|
||||
<input type="submit" value="Flag Package"/>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
<span class="notice">notice: any misuse of this form will be punished revoking the user's ability to flag packages</span>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
{% endblock %}
|
15
crates/artixweb-packages/templates/invalid_invitation.html
Normal file
15
crates/artixweb-packages/templates/invalid_invitation.html
Normal file
@@ -0,0 +1,15 @@
|
||||
{% extends "base_register.html" %}
|
||||
|
||||
{% block register_content %}
|
||||
|
||||
<article role="invalid_invitation" class="panel">
|
||||
<section>
|
||||
<h2>The registration code is Invalid, has been used or has Expired</h2>
|
||||
<p>This registration code does not appears to be valid. Note that invitations to register into the Artix Packages Website have an expiration time limit
|
||||
of 24 hours, after that time the invitation code is not valid anymore. Another possibility is that the invitation code has been alredy used to
|
||||
register an user.</p>
|
||||
<p>Please, refer to the <a href="https://forum.artixlinux.org">forum</a> for more information</p>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
{% endblock %}
|
10
crates/artixweb-packages/templates/invitation_email.html
Normal file
10
crates/artixweb-packages/templates/invitation_email.html
Normal file
@@ -0,0 +1,10 @@
|
||||
Greetings {{ invitation.email }}!
|
||||
|
||||
You have been invited to join the ArtixWeb Packages web site as a reporter!. Visit the link below to register your account.
|
||||
|
||||
https://packages.artixlinux.org/register/{{ invitation.id }}
|
||||
|
||||
This invitation expires on {{ invitation.expires_at }}
|
||||
|
||||
Best regards,
|
||||
The Artix Team.
|
30
crates/artixweb-packages/templates/login_ui.html
Normal file
30
crates/artixweb-packages/templates/login_ui.html
Normal file
@@ -0,0 +1,30 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
{% block page_content %}
|
||||
|
||||
<article role="authentication" class="panel">
|
||||
<section>
|
||||
<h2>User Authentication</h2>
|
||||
<p>Use the form below to authenticate as a registered user. Registered users can flag packages as outdated.</p>
|
||||
<form method="post" name = "login_form" action="/login">
|
||||
<fieldset>
|
||||
<div>
|
||||
<label for="email" title="User email to login with">User Email</label>
|
||||
<input type="text" id="email" name="email"/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="password" title="User password">User Password</label>
|
||||
<input type="password" id="password" name="password"/>
|
||||
</div>
|
||||
<div>
|
||||
<label> </label>
|
||||
<input type="submit" value="Authenticate"/>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
{% block register_content %}{% endblock %}
|
||||
|
||||
{% endblock %}
|
@@ -1,9 +1,48 @@
|
||||
{% extends "base.html" %}
|
||||
|
||||
<!-- This macro allow us to do not repeat ourselves below -->
|
||||
{% macro dependencies_details(dependencies, kind) %}
|
||||
{% if dependencies.len() <= 10 %}
|
||||
{% for dependency in dependencies.iter() %}
|
||||
<section role="dependency_{{ kind }}">
|
||||
{% if dependency.providers.is_empty() %}<a href="{{ dependency.name|details_url }}">{% endif %}{{ dependency.name }}{% if dependency.providers.is_empty() %}</a>{% endif %}{% if !dependency.depmod.is_empty() %} {{ dependency.depmod }}{% endif %}{% if !dependency.ver.is_empty() %} {{ dependency.ver }}{% endif %}{% if !dependency.providers.is_empty() %} ( {% for provider in dependency.providers %}<a href="{{ provider|details_url }}">{{ provider }}</a> {% endfor %} ){% endif %}
|
||||
</section>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% for dependency in dependencies[0..=9] %}
|
||||
<section role="dependency_{{ kind }}">
|
||||
{% if dependency.providers.is_empty() %}<a href="{{ dependency.name|details_url }}">{% endif %}{{ dependency.name }}{% if dependency.providers.is_empty() %}</a>{% endif %}{% if !dependency.depmod.is_empty() %} {{ dependency.depmod }}{% endif %}{% if !dependency.ver.is_empty() %} {{ dependency.ver }}{% endif %}{% if !dependency.providers.is_empty() %} ( {% for provider in dependency.providers %}<a href="{{ provider|details_url }}">{{ provider }}</a> {% endfor %} ){% endif %}
|
||||
</section>
|
||||
{% endfor %}
|
||||
<details>
|
||||
<summary>See more...</summary>
|
||||
{% for dependency in dependencies[10..] %}
|
||||
<section role="dependency_{{ kind }}">
|
||||
{% if kind == "soname" %}
|
||||
{{ dependency.name }}{% if !dependency.depmod.is_empty() %} {{ dependency.depmod }}{% endif %}{% if !dependency.ver.is_empty() %} {{ dependency.ver }}{% endif %}{% if !dependency.providers.is_empty() %} ( {% for provider in dependency.providers %}<a href="{{ provider|details_url }}">{{ provider }}</a> {% endfor %} ){% endif %}
|
||||
{% else %}
|
||||
{% if dependency.providers.is_empty() %}<a href="{{ dependency.name|details_url }}">{% endif %}{{ dependency.name }}{% if dependency.providers.is_empty() %}</a>{% endif %}{% if !dependency.depmod.is_empty() %} {{ dependency.depmod }}{% endif %}{% if !dependency.ver.is_empty() %} {{ dependency.ver }}{% endif %}{% if !dependency.providers.is_empty() %} ( {% for provider in dependency.providers %}<a href="{{ provider|details_url }}">{{ provider }}</a> {% endfor %} ){% endif %}
|
||||
{% endif %}
|
||||
</section>
|
||||
{% endfor %}
|
||||
</details>
|
||||
{% endif %}
|
||||
{% endmacro %}
|
||||
|
||||
{% block page_content %}
|
||||
<article role="details">
|
||||
<section class="panel">
|
||||
<h2>{{ pkg.package_name }}</h2>
|
||||
<h2>{{ pkg.package_name }}-{{ pkg.version }} {% if pkg.flagged %}(<span class="flagged">flagged</span>){% endif %}</h2>
|
||||
<section role="action_panel" class="action_panel">
|
||||
<h4>Actions Panel</h4>
|
||||
<section role="action"><a href="{{ pkg.gitea_url }}">View Package Sources</a></section>
|
||||
<section role="action"><a href="{{ pkg.gitea_url }}/graph">View Package Changes</a></section>
|
||||
{% if !pkg.flagged %}
|
||||
<section role="action"><a href="/flag_package/{{ pkg.package_name }}/{{ pkg.version }}">Flag package out-of-date</a></section>
|
||||
{% else %}
|
||||
<section role="action"><span class="flagged">Flagged out-of-date</span></section>
|
||||
{% endif %}
|
||||
</section>
|
||||
<section role="container" class="grid-auto-fr">
|
||||
<section role="key">Architecture:</section>
|
||||
<section role="value">{{ pkg.architecture }}</section>
|
||||
@@ -13,74 +52,79 @@
|
||||
<section role="value">{{ pkg.description }}</section>
|
||||
<section role="key">License(s):</section>
|
||||
<section role="value">{{ pkg.licenses|join(", ") }}</section>
|
||||
<section role="key">Groups:</section>
|
||||
<section role="value">{{ pkg.groups|join(", ") }}</section>
|
||||
{% if !pkg.groups.is_empty() %}
|
||||
<section role="key">Groups:</section>
|
||||
<section role="value">{{ pkg.groups|join(", ") }}</section>
|
||||
{% endif %}
|
||||
{% if !pkg.provides.is_empty() %}
|
||||
<section role="key">Provides:</section>
|
||||
<section role="value">{{ pkg.provides|provides_or_replaces }}</section>
|
||||
{% endif %}
|
||||
{% if !pkg.replaces.is_empty() %}
|
||||
<section role="key">Provides:</section>
|
||||
<section role="value">{{ pkg.replaces|provides_or_replaces }}</section>
|
||||
{% endif %}
|
||||
<section role="key">Upstream URL:</section>
|
||||
<section role="value"><a href="{{ pkg.upstream_url }}" target="_blank">{{ pkg.upstream_url }}</a></section>
|
||||
<section role="value"><a href="{{ pkg.upstream_url }}">{{ pkg.upstream_url }}</a></section>
|
||||
<section role="key">Size:</section>
|
||||
<section role="value">{{ pkg.size|human_readable }} MB</section>
|
||||
<section role="key">Installed Size:</section>
|
||||
<section role="value">{{ pkg.installed_size|human_readable }} MB</section>
|
||||
<section role="key">Build Date:</section>
|
||||
<section role="value">{{ pkg.build_date|show_date }}</section>
|
||||
{% if pkg.flagged && !pkg.flagged_by.is_empty() %}
|
||||
<section role="key">Flagged By:</section>
|
||||
<section role="value">{{ pkg.flagged_by|join(", ") }}</section>
|
||||
{% endif %}
|
||||
<!-- <section role="key">Maintainers:</section>
|
||||
<section role="value">{{ pkg.maintainers|join(", ") }}</section> -->
|
||||
</section>
|
||||
</section>
|
||||
|
||||
|
||||
<div class="grid-50-50">
|
||||
<section role="dependencies" class="panel adjacent-pkgs">
|
||||
<h3>Package Dependencies ({{ pkg.dependencies.len() }})</h3>
|
||||
{% for dependency in pkg.dependencies.iter() %}
|
||||
<section role="dependency">
|
||||
{% if dependency.providers.is_empty() %}<a href="{{ dependency.name|details_url }}">{% endif %}{{ dependency.name }}{% if dependency.providers.is_empty() %}</a>{% endif %}{% if !dependency.depmod.is_empty() %} {{ dependency.depmod }}{% endif %}{% if !dependency.ver.is_empty() %} {{ dependency.ver }}{% endif %}{% if !dependency.providers.is_empty() %} ( {% for provider in dependency.providers %}<a href="{{ provider|details_url }}">{{ provider }}</a> {% endfor %} ){% endif %}
|
||||
</section>
|
||||
{% endfor %}
|
||||
{% call dependencies_details(pkg.dependencies, "dep") %}
|
||||
{% if !pkg.sonames.is_empty() %}
|
||||
<h4>Sonames ({{ pkg.sonames.len() }})</h4>
|
||||
{% for soname in pkg.sonames[0..=9] %}
|
||||
<section role="soname_dependency">
|
||||
{{ soname.name }}{% if !soname.depmod.is_empty() %} {{ soname.depmod }}{% endif %}{% if !soname.ver.is_empty() %} {{ soname.ver }}{% endif %}{% if !soname.providers.is_empty() %} ( {% for provider in soname.providers %}<a href="{{ provider|details_url }}">{{ provider }}</a> {% endfor %} ){% endif %}
|
||||
</section>
|
||||
{% endfor %}
|
||||
{% if pkg.sonames.len() > 10 %}
|
||||
<details>
|
||||
<summary>See more...</summary>
|
||||
{% for soname in pkg.sonames[10..] %}
|
||||
<section role="soname_dependency">
|
||||
{{ soname.name }}{% if !soname.depmod.is_empty() %} {{ soname.depmod }}{% endif %}{% if !soname.ver.is_empty() %} {{ soname.ver }}{% endif %}{% if !soname.providers.is_empty() %} ( {% for provider in soname.providers %}<a href="{{ provider|details_url }}">{{ provider }}</a> {% endfor %} ){% endif %}
|
||||
</section>
|
||||
{% endfor %}
|
||||
</details>
|
||||
{% endif %}
|
||||
{% call dependencies_details(pkg.sonames, "soname") %}
|
||||
{% endif %}
|
||||
</section>
|
||||
<section role="required_by" class="panel">
|
||||
<h3>Required by ({{ pkg.required_by.len() }})</h3>
|
||||
{% for package in pkg.required_by.iter() %}
|
||||
<section role="required_package">
|
||||
<a href="{{ package|details_url }}">{{ package }}</a>
|
||||
</section>
|
||||
{% endfor %}
|
||||
{% if pkg.required_by.len() <= 10 %}
|
||||
{% for package in pkg.required_by.iter() %}
|
||||
<section role="required_package">
|
||||
<a href="{{ package|details_url }}">{{ package }}</a>
|
||||
</section>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% for package in pkg.required_by[0..=9] %}
|
||||
<section role="required_package">
|
||||
<a href="{{ package|details_url }}">{{ package }}</a>
|
||||
</section>
|
||||
{% endfor %}
|
||||
<details>
|
||||
<summary>See more...</summary>
|
||||
{% for package in pkg.required_by[10..] %}
|
||||
<section role="dependency_package">
|
||||
<a href="{{ package|details_url }}">{{ package }}</a>
|
||||
</section>
|
||||
{% endfor %}
|
||||
</details>
|
||||
{% endif %}
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="grid-50-50">
|
||||
<section role="optdepends" class="panel adjacent-pkgs">
|
||||
<h3>Package Optional Dependencies ({{ pkg.optdepends.len() }})</h3>
|
||||
{% for dependency in pkg.optdepends.iter() %}
|
||||
<section role="dependency">
|
||||
<a href="{{ dependency.name|details_url }}">{{ dependency.name }}</a>{% if !dependency.depmod.is_empty() %} {{ dependency.depmod }}{% endif %}{% if !dependency.ver.is_empty() %} {{ dependency.ver }}{% endif %}
|
||||
</section>
|
||||
{% endfor %}
|
||||
{% call dependencies_details(pkg.optdepends, "opt") %}
|
||||
</section>
|
||||
<section role="makedepends" class="panel adjacent-pkgs">
|
||||
<h3>Package Makepkg Dependencies ({{ pkg.makedepends.len() }})</h3>
|
||||
{% for dependency in pkg.makedepends.iter() %}
|
||||
<section role="dependency">
|
||||
<a href="{{ dependency.name|details_url }}">{{ dependency.name }}</a>{% if !dependency.depmod.is_empty() %} {{ dependency.depmod }}{% endif %}{% if !dependency.ver.is_empty() %} {{ dependency.ver }}{% endif %}
|
||||
</section>
|
||||
{% endfor %}
|
||||
{% call dependencies_details(pkg.makedepends, "make") %}
|
||||
</section>
|
||||
</div>
|
||||
</article>
|
||||
|
@@ -0,0 +1,9 @@
|
||||
Hi there!
|
||||
|
||||
The package {{ package_name }} has been flagged by {{ flag_by }} as out-of-date!
|
||||
|
||||
The reporter included the following message:
|
||||
|
||||
{{ message }}
|
||||
|
||||
Thank you.
|
28
crates/artixweb-packages/templates/register.html
Normal file
28
crates/artixweb-packages/templates/register.html
Normal file
@@ -0,0 +1,28 @@
|
||||
{% extends "base_register.html" %}
|
||||
|
||||
{% block register_content %}
|
||||
|
||||
<article role="registration" class="panel">
|
||||
<section>
|
||||
<h2>Choose a Password</h2>
|
||||
<p>Please, choose a password that you can remember. Be smart, do not use the same password that you use for other important stuff</p>
|
||||
<form method = "post" name = "user_registration" action="/new_user/{{ invitation_id }}">
|
||||
<fieldset>
|
||||
<div>
|
||||
<label for="user_password" title="Choose a password for your user">User Password</label>
|
||||
<input type="password" id="password", name="password"/>
|
||||
</div>
|
||||
<div>
|
||||
<label for="repeat_password" title="Repeat password">Repeat Password</label>
|
||||
<input type="password" id="repeat_password" name="repeat_password"/>
|
||||
</div>
|
||||
<div>
|
||||
<label> </label>
|
||||
<input type="submit" value="Register"/>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
{% endblock %}
|
20
crates/artixweb-packages/templates/weak_password.html
Normal file
20
crates/artixweb-packages/templates/weak_password.html
Normal file
@@ -0,0 +1,20 @@
|
||||
{% extends "base_register.html" %}
|
||||
|
||||
{% block register_content %}
|
||||
|
||||
<article role="weak_password" class="panel">
|
||||
<section>
|
||||
<h2>Weak password detected</h2>
|
||||
<p>The password that you entered was too simple. Please follow the rules below for your password:</p>
|
||||
<section>
|
||||
<span>It must have a minimum of <strong>8</strong> characters length</span><br/>
|
||||
<span>It must include at least a cap letter</span><br/>
|
||||
<span>It must include at least a punctuation character (.,$%;:@_-!?)</span><br/>
|
||||
</section>
|
||||
<form>
|
||||
<input type="button" value="Try Again" class="try_again" onclick="history.back()"/>
|
||||
</form>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
{% endblock %}
|
5
diesel.toml
Normal file
5
diesel.toml
Normal file
@@ -0,0 +1,5 @@
|
||||
# For documentation on how to configure this file,
|
||||
# see diesel.rs/guides/configuring-diesel-cli
|
||||
|
||||
[print_schema]
|
||||
file = "src/schema.rs"
|
BIN
docs/images/adminer_import.png
Normal file
BIN
docs/images/adminer_import.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 35 KiB |
BIN
docs/images/adminer_login.png
Normal file
BIN
docs/images/adminer_login.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 21 KiB |
BIN
docs/images/adminer_schema_imported.png
Normal file
BIN
docs/images/adminer_schema_imported.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 48 KiB |
126
example_files/docker-compose.yml
Normal file
126
example_files/docker-compose.yml
Normal file
@@ -0,0 +1,126 @@
|
||||
# This file is part of Artix Web Packages. See LICENSE file for details
|
||||
# Copyright 2022 - Artix Linux
|
||||
#
|
||||
# !!!!!!!!!!!!!!!!!!!!!!!!!!! ADVERTENCE !!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
# You SHOULD NOT use this setup example for production purposes
|
||||
# You SHOULD USE docker swarm with DOCKER SECRETS to configure
|
||||
# sensitive data for a production environment
|
||||
# !!!!!!!!!!!!!!!!!!!!!!!!!!! ADVERTENCE !!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
#
|
||||
# This docker-compose file is an example of how to setup a composer or
|
||||
# swarm stack in order to provide the required services for ArtixWeb
|
||||
# Packages to work.
|
||||
#
|
||||
# This file can (and must) be used as a template for your own setup
|
||||
#
|
||||
# Services Description
|
||||
#
|
||||
# PostgreSQL Database Server (Single Instance)
|
||||
# ArtixWeb Packages Server (Single Instance)
|
||||
#
|
||||
# Networking
|
||||
#
|
||||
# artixweb-network is used by default
|
||||
#
|
||||
# Volumes
|
||||
#
|
||||
# artixweb-db-storage
|
||||
# used to store the PostgreSQL database
|
||||
#
|
||||
# How to run?
|
||||
#
|
||||
# In a terminal cd into the project root and run:
|
||||
#
|
||||
# docker compose -f ./example_files/docker-compose.yml up -d
|
||||
#
|
||||
# How to stop?
|
||||
#
|
||||
# In a terminal cd into the project root and run:
|
||||
#
|
||||
# docker compose -f ./example_files/docker-compose.yml stop
|
||||
#
|
||||
# To stop and remove all the data hold by the containers run
|
||||
#
|
||||
# docker compose -f ./example_files/docker-compose.yml down
|
||||
#
|
||||
# How to check the logs?
|
||||
#
|
||||
# docker -f ./example_files/docker-compose.yml logs -f --tail 100
|
||||
#
|
||||
# If you want to receive all the logs since the beginning
|
||||
#
|
||||
# docker -f ./example_files/docker-compose.yml logs -f --tail 100
|
||||
#
|
||||
|
||||
# use the third syntax revision for docker compose
|
||||
version: '3'
|
||||
|
||||
# service definitions
|
||||
services:
|
||||
|
||||
# single node PostgreSQL database
|
||||
postgres:
|
||||
# use the latest available postgres image
|
||||
image: postgres
|
||||
restart: always
|
||||
|
||||
# setup required environment variables
|
||||
environment:
|
||||
- POSTGRES_USER=artix
|
||||
- POSTGRES_PASSWORD=artix
|
||||
- POSTGRES_DB=artixweb
|
||||
|
||||
# mount a host volume to use to store our data
|
||||
volumes:
|
||||
- type: bind
|
||||
# the /var/db/artixweb/postgres directory MUST exists in the host
|
||||
source: /var/db/artixweb/postgres
|
||||
target: /var/lib/postgresql/data
|
||||
|
||||
# single node ArtixWeb Packages service
|
||||
artixweb:
|
||||
# use the image build with the command ran in the project root:
|
||||
# docker build -f Dockerfile.install -t artixweb-packages .
|
||||
image: artixweb_packages
|
||||
restart: always
|
||||
depends_on:
|
||||
- postgres
|
||||
|
||||
# expose the default port as port 1936 in the host
|
||||
ports:
|
||||
- "1936:1936"
|
||||
|
||||
# setup required environment variables
|
||||
environment:
|
||||
# we need to setup the bind address as 0.0.0.0 so we can access the service in the container
|
||||
- BIND_ADDRESS=0.0.0.0
|
||||
# used with header based authentication to perform certain administrative operations
|
||||
# can be generated running:
|
||||
# dd if=/dev/urandom bs=8b count=1024 iflag=fullblock 2>/dev/null | sha256sum | awk '{print $1}'
|
||||
- API_TOKEN=2f79015f627643cdba7f0196aa1b5736f2270dc512e67362b3dfa5393bdeabee
|
||||
# used to cipher and sign session cookies for authenticated users, can be generated with:
|
||||
# openssl rand -hex 64
|
||||
- SESSION_KEY=6bd696315b8e96bc98a65c6c0f965d4b63073d81afe7a94f6f69f38f1fd5b78fa35ce25b615fc4c6033c3ac591ba26fc62bfd8c28eb475e50d80241f408d6a3f
|
||||
# used to connect to the database, it matches PostgreSQL configuration above
|
||||
- DATABASE_URL=postgres://artix:artix@postgres/artixweb
|
||||
|
||||
# SMTP configuration
|
||||
- SMTP_USER=someuser@somewhere.com
|
||||
- SMTP_PWD=somepassword
|
||||
- SMTP_RELAY=smtp.somewhere.com
|
||||
|
||||
# Comment the next section if you do not need an Adminer UI
|
||||
# to manage the PostgreSQL from a web UI interface
|
||||
# Adminer Database Manager
|
||||
adminer:
|
||||
image: adminer
|
||||
|
||||
# expose the adminer port to the host
|
||||
ports:
|
||||
- "8080:8080"
|
||||
# define stack networks
|
||||
networks:
|
||||
default:
|
||||
name: artixweb-network
|
||||
|
||||
# vim:ft=yaml ts=2 sts=2 sw=2 expandtab
|
52
example_files/schema.sql
Normal file
52
example_files/schema.sql
Normal file
@@ -0,0 +1,52 @@
|
||||
-- ArtixWeb Databse Schema SQL
|
||||
|
||||
-- \connect "artixweb";
|
||||
|
||||
DROP TABLE IF EXISTS "__diesel_schema_migrations";
|
||||
CREATE TABLE "public"."__diesel_schema_migrations" (
|
||||
"version" character varying(50) NOT NULL,
|
||||
"run_on" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
CONSTRAINT "__diesel_schema_migrations_pkey" PRIMARY KEY ("version")
|
||||
) WITH (oids = false);
|
||||
|
||||
|
||||
DROP TABLE IF EXISTS "invitations";
|
||||
CREATE TABLE "public"."invitations" (
|
||||
"id" uuid NOT NULL,
|
||||
"email" character varying(100) NOT NULL,
|
||||
"expires_at" timestamp NOT NULL,
|
||||
CONSTRAINT "invitations_pkey" PRIMARY KEY ("id")
|
||||
) WITH (oids = false);
|
||||
|
||||
|
||||
DROP TABLE IF EXISTS "package_flags";
|
||||
CREATE TABLE "public"."package_flags" (
|
||||
"user_email" character varying(100) NOT NULL,
|
||||
"package_name" character varying NOT NULL,
|
||||
"flag_on" timestamp DEFAULT CURRENT_TIMESTAMP NOT NULL,
|
||||
CONSTRAINT "package_flags_pkey" PRIMARY KEY ("user_email", "package_name")
|
||||
) WITH (oids = false);
|
||||
|
||||
|
||||
DROP TABLE IF EXISTS "packages";
|
||||
CREATE TABLE "public"."packages" (
|
||||
"package_name" character varying NOT NULL,
|
||||
"gitea_url" character varying NOT NULL,
|
||||
"last_update" timestamp NOT NULL,
|
||||
CONSTRAINT "packages_pkey" PRIMARY KEY ("package_name")
|
||||
) WITH (oids = false);
|
||||
|
||||
|
||||
DROP TABLE IF EXISTS "users";
|
||||
CREATE TABLE "public"."users" (
|
||||
"email" character varying(100) NOT NULL,
|
||||
"hash" character varying(122) NOT NULL,
|
||||
"created_at" timestamp NOT NULL,
|
||||
"banned" boolean DEFAULT false NOT NULL,
|
||||
"admin" boolean DEFAULT false NOT NULL,
|
||||
CONSTRAINT "users_pkey" PRIMARY KEY ("email")
|
||||
) WITH (oids = false);
|
||||
|
||||
|
||||
ALTER TABLE ONLY "public"."package_flags" ADD CONSTRAINT "fk_package_id" FOREIGN KEY (package_name) REFERENCES packages(package_name) ON DELETE CASCADE NOT DEFERRABLE;
|
||||
ALTER TABLE ONLY "public"."package_flags" ADD CONSTRAINT "fk_user_email" FOREIGN KEY (user_email) REFERENCES users(email) ON DELETE CASCADE NOT DEFERRABLE;
|
Reference in New Issue
Block a user