33 Commits

Author SHA1 Message Date
4a10945992 use printf instead of echo
All checks were successful
Docker Image CI / build (pull_request) Successful in 7m1s
2024-06-13 16:28:23 -05:00
6d3cd0566e feat: drop universe, add gremlins repos
All checks were successful
Docker Image CI / build (pull_request) Successful in 7m20s
2024-06-13 15:40:48 -05:00
42fd6f186f Back to development: 0.1.2 (#11)
All checks were successful
Docker Image CI / build (push) Successful in 7m22s
Reviewed-on: #11
2024-06-13 17:40:29 +02:00
98699fcdeb Merge pull request 'fix gitea image name' (#10) from gitea-deploy into master
Some checks failed
Docker Image CI / build (push) Failing after 7m29s
Reviewed-on: #10
2024-06-13 16:45:57 +02:00
e2c8035b99 fix gitea image name
Some checks failed
Docker Image CI / build (pull_request) Has been cancelled
2024-06-13 09:38:06 -05:00
7bc9725f63 Merge pull request 'fix workflow' (#9) from action_fix into master
Some checks failed
Docker Image CI / build (push) Has been cancelled
Reviewed-on: #9
2024-06-13 06:55:06 +02:00
eb12a979b6 fix workflow
All checks were successful
Docker Image CI / build (pull_request) Successful in 8m13s
2024-06-12 23:46:15 -05:00
1e33c89223 Merge pull request 'change version to v0.1.1' (#8) from corysanin/artixweb_packages:first-release into master
Some checks failed
Docker Image CI / build (push) Failing after 1m0s
Reviewed-on: #8
2024-06-13 00:04:35 +02:00
784fe9a438 change version to v0.1.1
Some checks failed
Docker Image CI / build (pull_request) Has been cancelled
2024-06-12 16:53:22 -05:00
3e2857dcb1 Merge pull request 'enable builds on pushes to master' (#7) from corysanin/artixweb_packages:build-on-push into master
All checks were successful
Docker Image CI / build (push) Successful in 12m35s
Reviewed-on: #7
2024-06-12 22:59:24 +02:00
0f6cbfa52a enable builds on pushes to master
Some checks failed
Docker Image CI / build (pull_request) Failing after 1m38s
2024-06-12 15:58:50 -05:00
eae61ea52e Merge pull request 'Update dependencies, create action' (#6) from corysanin/artixweb_packages:action into master
Reviewed-on: #6
2024-06-12 04:34:11 +02:00
dc3bc810bb bump alpm to 3.0.4 2024-03-25 22:47:31 -05:00
f57e542126 fix: remove unused step 2024-03-19 21:55:36 -05:00
0752725119 feat: new streamlined Dockerfile 2024-03-19 21:41:10 -05:00
80015040f1 feat: use gitea pipeline to build docker image 2024-03-19 20:54:41 -05:00
19045da969 chore: disable nightly tests for now
Some checks failed
continuous-integration/drone/push Build is failing
2022-04-15 16:42:07 +01:00
49ad457b95 Merge pull request 'feat: mobile friendly styles' (#5) from corysanin/artixweb_packages:mobile into master
Some checks failed
continuous-integration/drone/push Build is failing
Reviewed-on: artix/artixweb_packages#5
2022-04-15 17:40:34 +02:00
6847216f86 feat: better table spacing
Some checks failed
continuous-integration/drone/pr Build is failing
2022-04-15 01:53:38 -05:00
8b5ca214d3 feat: single column layout on mobile 2022-04-15 01:53:38 -05:00
634d056823 fix: fix typos
Some checks failed
continuous-integration/drone/push Build is failing
2022-04-14 20:51:46 +01:00
dd4290c51c fix: fix typo in message subject 2022-04-14 19:54:17 +01:00
587ce73f2d fix: fix smtp access to use STARTTLS 2022-04-14 19:48:49 +01:00
b4b76576da docs: amend README.md documentation
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-27 15:31:42 +01:00
3af9ba8f5c fix: fixed Docker compose example, add documentation
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-27 15:13:22 +01:00
0a34423f5a fix: improvements over SMTP support, Docker, add documentation and others
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-26 18:23:58 +00:00
53a423def3 chore: finish tagging capabilities 2022-03-25 19:22:46 +00:00
bd4a897637 feat: add postgres database
Some checks failed
continuous-integration/drone/push Build is failing
PostgreSQL database support has been added to add the following
    functionality to the site:

        * User registration through invitations
        * User login and sessions
        * Package flagging (by registered users)
        * Pacakge Metadata cache (so we do not hit gitea so often)

    Note: this is a work in progress
2022-03-20 23:55:57 +00:00
2e0e116934 feat: add database and login (wip) 2022-03-18 00:57:16 +00:00
985d375459 Merge pull request 'feat: tweak summary sections for details' (#3) from corysanin/artixweb_packages:cs/summarytweaks into master
All checks were successful
continuous-integration/drone/push Build is passing
Reviewed-on: artix/artixweb_packages#3
2022-03-13 22:22:46 +01:00
74975936cb feat: tweak summary sections for details
All checks were successful
continuous-integration/drone/pr Build is passing
2022-03-11 22:40:59 -06:00
8943f49bfe fix: be conside on match branches
All checks were successful
continuous-integration/drone/push Build is passing
2022-03-12 01:10:10 +00:00
a8b38b74ff feat: add summaries for all deps list, also add groups, provides and replaces 2022-03-12 01:09:06 +00:00
57 changed files with 5166 additions and 150 deletions

View File

@@ -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

View 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
View File

@@ -1,6 +1,9 @@
# Cargo stuff
target/
*Cargo.lock
*/Cargo.lock
# editor stuff
.vscode/
# diesel
.env

2627
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

58
Dockerfile.ci Normal file
View 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 "\n[lib32]\nInclude = /etc/pacman.d/mirrorlist" >> /etc/pacman.conf \
&& printf "\n[system-gremlins]\nInclude = /etc/pacman.d/mirrorlist" >> /etc/pacman.conf \
&& printf "\n[world-gremlins]\nInclude = /etc/pacman.d/mirrorlist" >> /etc/pacman.conf \
&& printf "\n[galaxy-gremlins]\nInclude = /etc/pacman.d/mirrorlist" >> /etc/pacman.conf \
&& printf "\n[lib32-gremlins]\nInclude = /etc/pacman.d/mirrorlist" >> /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
View 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
View 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

View File

@@ -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.2"
[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"] }

View File

@@ -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.2"
[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"

View File

@@ -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());
}

View File

@@ -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);

View File

@@ -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.2"
[[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.2" }
artix-pkglib = { path = "../artix-pkglib", version = "=0.1.2" }
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"] }

View File

@@ -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;
}
}

View File

@@ -0,0 +1,2 @@
[print_schema]
file = "src/schema.rs"

View 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();

View File

@@ -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;

View File

@@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE packages

View File

@@ -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
)

View File

@@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE users

View File

@@ -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'
)

View File

@@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE invitations

View File

@@ -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
)

View File

@@ -0,0 +1,2 @@
-- This file should undo anything in `up.sql`
DROP TABLE package_flags

View File

@@ -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
)

View File

@@ -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
}

View 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)
}

View File

@@ -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(", "))
}
}

View File

@@ -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 = &parameter.0;
match key {
@@ -96,10 +121,13 @@ 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)
let s = if let Ok(result) =
get_packages_inner(&repos_criteria, limit, offset, search_criteria, Some(&pool)).await
{
PackagesNavigation {
user_email,
packages: result.0,
repos,
total: result.1,
query: query_url.join("&"),
total_pages: if limit > 0 {
@@ -124,8 +152,8 @@ pub async fn index(query: web::Query<Vec<(String, String)>>) -> Result<HttpRespo
#[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> {

View 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)
}

View File

@@ -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);

View File

@@ -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(),
@@ -194,14 +494,46 @@ 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.gitea_url = metadata.gitea_url;
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 +543,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 +566,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 +648,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 +679,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 +693,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 +752,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 +769,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 +791,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() {

View 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),
))
})
}

View File

@@ -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,
}
}
}

View File

@@ -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,
}

View File

@@ -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()

View 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 }
}
}

View File

@@ -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/")),
);

View 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,);

View File

@@ -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 %}

View File

@@ -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>

View 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 %}

View 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 %}

View 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>&nbsp;</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 %}

View 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 %}

View 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.

View 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>&nbsp;</label>
<input type="submit" value="Authenticate"/>
</div>
</fieldset>
</form>
</section>
</article>
{% block register_content %}{% endblock %}
{% endblock %}

View File

@@ -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 }}" target="blank">View Package Sources</a></section>
<section role="action"><a href="{{ pkg.gitea_url }}/graph" target="blank">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,8 +52,18 @@
<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="key">Size:</section>
@@ -23,64 +72,59 @@
<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>

View File

@@ -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.

View 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>&nbsp;</label>
<input type="submit" value="Register"/>
</div>
</fieldset>
</form>
</section>
</article>
{% endblock %}

View 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
View 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"

Binary file not shown.

After

Width:  |  Height:  |  Size: 35 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

View 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
View 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;