├── .gitignore ├── static ├── favicon │ ├── favicon.ico │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── apple-touch-icon.png │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── site.webmanifest │ └── about.txt ├── sitemap-index.xml ├── browserconfig.xml ├── manifest.json ├── sitemap-0.xml ├── home.svg ├── safari-pinned-tab.svg ├── cassidoo.js ├── glitch.css ├── custom.css ├── particles-cfg.js └── particles.js ├── templates ├── hello.html ├── 404.html ├── fetch-zip.html ├── footer.html ├── nav.html ├── home.html ├── head.html └── posts.html ├── src ├── error.rs ├── db │ ├── mod.rs │ ├── sqlite.rs │ └── surreal.rs ├── form_zip.rs ├── middleware.rs ├── handlers.rs └── main.rs ├── compose.yml ├── README.md ├── LICENCE-MIT ├── Makefile ├── Dockerfile ├── Cargo.toml └── LICENCE-APACHE /.gitignore: -------------------------------------------------------------------------------- 1 | /target 2 | *zip 3 | *sqlite3 4 | -------------------------------------------------------------------------------- /static/favicon/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olekspickle/axum-template/HEAD/static/favicon/favicon.ico -------------------------------------------------------------------------------- /static/favicon/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olekspickle/axum-template/HEAD/static/favicon/favicon-16x16.png -------------------------------------------------------------------------------- /static/favicon/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olekspickle/axum-template/HEAD/static/favicon/favicon-32x32.png -------------------------------------------------------------------------------- /static/favicon/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olekspickle/axum-template/HEAD/static/favicon/apple-touch-icon.png -------------------------------------------------------------------------------- /static/favicon/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olekspickle/axum-template/HEAD/static/favicon/android-chrome-192x192.png -------------------------------------------------------------------------------- /static/favicon/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/olekspickle/axum-template/HEAD/static/favicon/android-chrome-512x512.png -------------------------------------------------------------------------------- /templates/hello.html: -------------------------------------------------------------------------------- 1 | {% extends "home.html" %} 2 | 3 | {% block content %} 4 |

Hello {{ name }}

5 | {% endblock content %} 6 | -------------------------------------------------------------------------------- /static/sitemap-index.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | https://olekspickle.github.com/axum-template/sitemap-0.xml 4 | 5 | 6 | -------------------------------------------------------------------------------- /templates/404.html: -------------------------------------------------------------------------------- 1 | {% extends "home.html" %} 2 | 3 | {% block content %} 4 | 5 |

404

6 |

{{ uri }}

7 | 8 | {% endblock content %} 9 | -------------------------------------------------------------------------------- /templates/fetch-zip.html: -------------------------------------------------------------------------------- 1 | {% extends "home.html" %} 2 | 3 | {% block content %} 4 |

{{ title }}

5 | {% if title == "Fetch" %} 6 | Get zip 7 | {% endif %} 8 | {% endblock content %} 9 | -------------------------------------------------------------------------------- /static/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | #eb4888 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /static/favicon/site.webmanifest: -------------------------------------------------------------------------------- 1 | {"name":"","short_name":"","icons":[{"src":"/android-chrome-192x192.png","sizes":"192x192","type":"image/png"},{"src":"/android-chrome-512x512.png","sizes":"512x512","type":"image/png"}],"theme_color":"#ffffff","background_color":"#ffffff","display":"standalone"} -------------------------------------------------------------------------------- /static/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "olekspickle", 3 | "icons": [ 4 | { 5 | "src": "\/static\/home.svg", 6 | "sizes": "192x192", 7 | "type": "image\/png" 8 | } 9 | ], 10 | "theme_color": "#24252d", 11 | "display": "standalone" 12 | } 13 | -------------------------------------------------------------------------------- /templates/footer.html: -------------------------------------------------------------------------------- 1 | 10 | -------------------------------------------------------------------------------- /static/sitemap-0.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | https://olekspickle.github.com/axum-template/ 4 | 5 | 6 | https://olekspickle.github.com/axum-template/hello 7 | 8 | 9 | https://olekspickle.github.com/axum-template/posts 10 | 11 | 12 | -------------------------------------------------------------------------------- /static/favicon/about.txt: -------------------------------------------------------------------------------- 1 | This favicon was generated using the following font: 2 | 3 | - Font Title: Leckerli One 4 | - Font Author: Copyright (c) 2011 Gesine Todt (www.gesine-todt.de), with Reserved Font Names "Leckerli" 5 | - Font Source: http://fonts.gstatic.com/s/leckerlione/v16/V8mCoQH8VCsNttEnxnGQ-1itLZxcBtItFw.ttf 6 | - Font License: SIL Open Font License, 1.1 (http://scripts.sil.org/OFL)) 7 | -------------------------------------------------------------------------------- /static/home.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /templates/nav.html: -------------------------------------------------------------------------------- 1 | 2 | 16 | -------------------------------------------------------------------------------- /templates/home.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {% include "head.html" %} 4 | 5 | 6 | {% include "nav.html" %} 7 |
8 |
9 | {% block content %}{% endblock content %} 10 |
11 | 12 | 13 |
14 |
15 |
16 | 17 | 18 | {% include "footer.html" %} 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/error.rs: -------------------------------------------------------------------------------- 1 | use displaydoc::Display; 2 | use thiserror::Error; 3 | 4 | pub(crate) type Result = std::result::Result; 5 | 6 | #[derive(Display, Error, Debug)] 7 | pub(crate) enum Error { 8 | /// IO: {0} 9 | IO(#[from] std::io::Error), 10 | /// Could not create file 11 | CreateFile, 12 | /// Failed to form encrypted zip file 13 | Zip(#[from] zip::result::ZipError), 14 | /// Failed to execute sqlite query 15 | #[cfg(feature = "sqlite")] 16 | Sqlite(#[from] rusqlite::Error), 17 | /// Failed to execute surrealdb query 18 | #[cfg(feature = "surreal")] 19 | #[error(transparent)] 20 | Surreal(#[from] surrealdb::error::Error), 21 | } 22 | -------------------------------------------------------------------------------- /src/db/mod.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | 3 | #[cfg(feature = "sqlite")] 4 | mod sqlite; 5 | #[cfg(feature = "surreal")] 6 | mod surreal; 7 | 8 | #[cfg(feature = "sqlite")] 9 | pub(crate) use sqlite::{add_post, delete_post, get_all_posts, init, update_post, DB}; 10 | #[cfg(feature = "surreal")] 11 | pub(crate) use surreal::{add_post, delete_post, get_all_posts, init, update_post, DB}; 12 | 13 | #[derive(Debug, Clone, Default, Deserialize, Serialize)] 14 | pub(crate) struct NewPost { 15 | user: String, 16 | content: String, 17 | } 18 | 19 | #[derive(Debug, Clone, Default, Deserialize, Serialize)] 20 | pub(crate) struct Post { 21 | pub id: u32, 22 | pub user: String, 23 | pub created_at: String, 24 | pub content: String, 25 | } 26 | -------------------------------------------------------------------------------- /static/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | Created by potrace 1.11, written by Peter Selinger 2001-2013 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /compose.yml: -------------------------------------------------------------------------------- 1 | services: 2 | actix-template: 3 | hostname: server 4 | entrypoint: /app/actix-template 5 | build: 6 | context: . 7 | ports: 8 | - "7777:7777" 9 | links: 10 | - surrealdb 11 | depends_on: 12 | - surrealdb 13 | networks: 14 | - spider-web 15 | environment: 16 | - RUST_LOG=info,actix_template=trace 17 | - SURREALDB_URL=ws://surrealdb:8000 # SurrealDB WebSocket URL 18 | - SURREALDB_USER=root 19 | - SURREALDB_PASS=root 20 | surrealdb: 21 | hostname: surrealdb 22 | image: surrealdb/surrealdb:latest 23 | command: start --log debug --user root --pass root memory 24 | networks: 25 | - spider-web 26 | ports: 27 | - "8000:8000" # SurrealDB HTTP/WebSocket interface port 28 | environment: 29 | - SURREALDB_USER=root 30 | - SURREALDB_PASS=root 31 | networks: 32 | spider-web: 33 | external: 34 | name: spider-web 35 | -------------------------------------------------------------------------------- /static/cassidoo.js: -------------------------------------------------------------------------------- 1 | 2 | let colors = ["#24d05a", "#eb4888", "#10a2f5", "#e9bc3f"]; 3 | 4 | function getRandomColor() { 5 | return colors[Math.floor(Math.random() * colors.length)]; 6 | } 7 | 8 | function setRandomLinkColor() { 9 | Array.from(document.getElementsByTagName("a")).forEach((e) => { 10 | e.style.textDecorationColor = getRandomColor(); 11 | }); 12 | 13 | Array.from(document.getElementsByTagName("h2")).forEach((e) => { 14 | e.style.textDecorationColor = getRandomColor(); 15 | }); 16 | 17 | Array.from(document.getElementsByTagName("input")).forEach((e) => { 18 | e.style.borderBottomColor = getRandomColor(); 19 | }); 20 | } 21 | 22 | function setColorHoverListener() { 23 | Array.from(document.querySelectorAll("h2, a, button, input")).forEach((e) => { 24 | e.addEventListener("mouseover", setRandomLinkColor); 25 | }); 26 | } 27 | 28 | (function () { 29 | setRandomLinkColor(); 30 | setColorHoverListener(); 31 | })(); 32 | 33 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # axum-template 2 | 3 | ![axum-template](https://github.com/user-attachments/assets/a16843e7-7537-4c73-a550-52a37b6fbf73) 4 | 5 | ### Overview 6 | Template to have something to get-go in some situations 7 | 8 | This template provides: 9 | - [x] Axum server(with middleware) 10 | - [x] Askama templates 11 | - [x] Containerization(with compose) 12 | - [x] Greeter page with query param name 13 | - [x] Sqlite backend 14 | - [ ] SurrealDB backend 15 | 16 | ## Running 17 | ```bash 18 | # Sqlite3 backend: 19 | make run 20 | 21 | # surrealdb backend 22 | make surreal 23 | 24 | ``` 25 | 26 | You can peek into Makefile for build details 27 | 28 | ### Afterthoughts and issues 29 | I found axum to be the most ergonomic web framework out there, and while there might be not 30 | enough examples at the moment, it is quite a breeze to use 31 | - static files was sure one noticeable pain in the rear to figure out 32 | - surrealdb sure adds complexity, I'm adding it under a feature because sqlite integration is 33 | so much less crates to compile(190+ vs 500+) 34 | 35 | -------------------------------------------------------------------------------- /LICENCE-MIT: -------------------------------------------------------------------------------- 1 | Permission is hereby granted, free of charge, to any 2 | person obtaining a copy of this software and associated 3 | documentation files (the "Software"), to deal in the 4 | Software without restriction, including without 5 | limitation the rights to use, copy, modify, merge, 6 | publish, distribute, sublicense, and/or sell copies of 7 | the Software, and to permit persons to whom the Software 8 | is furnished to do so, subject to the following 9 | conditions: 10 | 11 | The above copyright notice and this permission notice 12 | shall be included in all copies or substantial portions 13 | of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF 16 | ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED 17 | TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A 18 | PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT 19 | SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 20 | CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR 22 | IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .PHONY: docs test lint build build-release tag run-local 2 | 3 | .ONESHELL: # Use one shell per target 4 | SHELL := /bin/bash 5 | # Stop excecution on any error 6 | .SHELLFLAGS = -ec 7 | 8 | crate=axum-template 9 | 10 | docs: 11 | cargo docs --open 12 | 13 | lint: 14 | cargo clippy -- -D warnings 15 | cargo fmt --all -- --check 16 | cargo machete 17 | 18 | pack: 19 | # TODO: query crate name with 20 | # cargo pkgid | rev | cut -d'/' -f1 | rev | sed 's/#.*//' 21 | docker build -t $(crate):local . 22 | 23 | tag: pack 24 | # TODO: user can be sub-d with 25 | # git config --get user.name | cut -d " " -f 1 26 | # and version with 27 | # cargo pkgid | grep -oP '#\K[^#]+$' 28 | docker tag $(crate):local olekspickle/$(crate):v0.1.0 29 | 30 | log_level=RUST_LOG=trace,tower_http=debug,axum::rejection=trace,axum_template=trace 31 | 32 | run: 33 | $(log_level) cargo run 34 | 35 | run-surreal: 36 | docker compose -f compose.yml up --build 37 | 38 | run-docker-restricted: pack 39 | docker run -d \ 40 | -p 7777:7777 \ 41 | --hostname $(crate) \ 42 | --cpus="0.25" --memory="0.5g" \ 43 | -e $(log_level) \ 44 | $(crate):local 45 | -------------------------------------------------------------------------------- /src/form_zip.rs: -------------------------------------------------------------------------------- 1 | use crate::error::Result; 2 | use std::{env, fs, io::Write, process::Command}; 3 | use tracing::{trace, warn}; 4 | use zip::{write::SimpleFileOptions, AesMode}; 5 | 6 | pub(crate) const FILE: &str = "my.txt"; 7 | pub(crate) const ZIP: &str = "my.zip"; 8 | 9 | pub fn create_zip() -> Result<()> { 10 | let Ok(file) = fs::File::create(ZIP).inspect_err(|e| warn!("create file {e}")) else { 11 | return Err(crate::error::Error::CreateFile); 12 | }; 13 | let mut zip = zip::ZipWriter::new(file); 14 | let secret = env::var("SECRET").unwrap_or_else(|_| "test".into()); 15 | let opts = SimpleFileOptions::default().with_aes_encryption(AesMode::Aes256, &secret); 16 | 17 | zip.start_file(FILE, opts) 18 | .inspect_err(|e| warn!("create file in zip {e}"))?; 19 | zip.write_all(b"============MACHINE INFO==============")?; 20 | // will fail in docker 21 | match Command::new("docker").args(["system", "df", "-v"]).output() { 22 | Ok(info) => { 23 | trace!("writing docker info to the zip"); 24 | zip.write_all(&info.stdout)?; 25 | } 26 | Err(e) => warn!(err=%e.to_string(), "Failed to query docker info"), 27 | } 28 | 29 | Ok(()) 30 | } 31 | -------------------------------------------------------------------------------- /src/middleware.rs: -------------------------------------------------------------------------------- 1 | //! # Middleware 2 | //! 3 | //! You can do whatever you want with incoming requests before they reach handles 4 | //! 5 | use axum::{ 6 | body::Body, 7 | http::{Request, Response, StatusCode}, 8 | middleware::Next, 9 | }; 10 | use hyper::HeaderMap; 11 | use tracing::{info, trace, warn}; 12 | 13 | /// Logging middleware 14 | pub(crate) async fn log(req: Request, next: Next) -> Result, StatusCode> { 15 | let (parts, body) = req.into_parts(); 16 | 17 | if !parts.uri.to_string().contains("static") { 18 | //trace!("{}", parts.uri); 19 | } 20 | 21 | let req = Request::from_parts(parts, body); 22 | Ok(next.run(req).await) 23 | } 24 | 25 | /// Auth middleware 26 | pub(crate) async fn auth(req: Request, next: Next) -> Result, StatusCode> { 27 | let (parts, body) = req.into_parts(); 28 | 29 | if parts.uri == "/secret" && check_bearer(&parts.headers).is_err() { 30 | warn!("[secret] auth header is not present"); 31 | return Err(StatusCode::BAD_REQUEST); 32 | } 33 | 34 | let req = Request::from_parts(parts, body); 35 | Ok(next.run(req).await) 36 | } 37 | 38 | fn check_bearer(header_map: &HeaderMap) -> Result<(), StatusCode> { 39 | const TOKEN: &str = "super-secret"; 40 | 41 | if let Some(token) = header_map.get("Authorization") { 42 | if !token.is_empty() && token == TOKEN { 43 | trace!("Authorized!"); 44 | return Ok(()); 45 | } 46 | } 47 | 48 | Err(StatusCode::FORBIDDEN) 49 | } 50 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Run detached and remove container when it stopped 2 | # Warning: Docker logs can mess up current terminal pane if not detached 3 | # 4 | # docker build -t axum-template:local . 5 | # docker run -d -p 8000:8000 --rm --name axum-template --hostname axum-template 6 | 7 | ############################CACHE############################################## 8 | 9 | FROM docker.io/rust:slim AS builder 10 | 11 | # it is common to name cached image `build` but this messes up 12 | # some frameworks fileserver which is configured in compile-time, 13 | # so the build image and resulting image WORKDIR should match 14 | WORKDIR /app 15 | 16 | # copy the project 17 | COPY . . 18 | 19 | # 1. install stable Rust 20 | # 2. run release build with cached rustup, cargo registry and target build artifacts 21 | # 3. copy release binary with compressed debug symbols to the root 22 | RUN --mount=type=cache,target=/app/target \ 23 | --mount=type=cache,target=/usr/local/cargo/registry \ 24 | --mount=type=cache,target=/usr/local/cargo/git \ 25 | --mount=type=cache,target=/usr/local/rustup \ 26 | set -eux; \ 27 | rustup install stable; \ 28 | cargo build --release; \ 29 | # in case you don't do that in cargo config, you can strip debug symbols here 30 | objcopy --compress-debug-sections target/release/axum-template ./axum-template 31 | 32 | ################################################################################ 33 | 34 | FROM docker.io/debian:stable-slim 35 | 36 | WORKDIR /app 37 | 38 | # copy server files 39 | COPY --from=builder /app/static ./static 40 | COPY --from=builder /app/axum-template ./axum-template 41 | CMD ./axum-template 42 | -------------------------------------------------------------------------------- /static/glitch.css: -------------------------------------------------------------------------------- 1 | 2 | /* 404 glitch */ 3 | @import url('https://fonts.googleapis.com/css?family=Fira+Mono:400'); 4 | 5 | .oops { 6 | color: #131313; 7 | font-size: 96px; 8 | margin: 0; 9 | font-family: 'Fira Mono', monospace; 10 | letter-spacing: -7px; 11 | animation: glitch 1s linear infinite; 12 | } 13 | 14 | .oopsie { 15 | font-size: 64px; 16 | } 17 | 18 | @keyframes glitch { 19 | 20 | 2%, 21 | 64% { 22 | transform: translate(2px, 0) skew(0deg); 23 | } 24 | 25 | 4%, 26 | 60% { 27 | transform: translate(-2px, 0) skew(0deg); 28 | } 29 | 30 | 62% { 31 | transform: translate(0, 0) skew(5deg); 32 | } 33 | } 34 | 35 | .oops:before, 36 | .oops:after { 37 | content: attr(title); 38 | position: absolute; 39 | left: 0; 40 | } 41 | 42 | .oops:before { 43 | animation: glitchTop 1s linear infinite; 44 | clip-path: polygon(0 0, 100% 0, 100% 33%, 0 33%); 45 | -webkit-clip-path: polygon(0 0, 100% 0, 100% 33%, 0 33%); 46 | } 47 | 48 | @keyframes glitchTop { 49 | 50 | 2%, 51 | 64% { 52 | transform: translate(2px, -2px); 53 | } 54 | 55 | 4%, 56 | 60% { 57 | transform: translate(-2px, 2px); 58 | } 59 | 60 | 62% { 61 | transform: translate(13px, -1px) skew(-13deg); 62 | } 63 | } 64 | 65 | .oops:after { 66 | animation: glitchBotom 1.5s linear infinite; 67 | clip-path: polygon(0 67%, 100% 67%, 100% 100%, 0 100%); 68 | -webkit-clip-path: polygon(0 67%, 100% 67%, 100% 100%, 0 100%); 69 | } 70 | 71 | @keyframes glitchBotom { 72 | 73 | 2%, 74 | 64% { 75 | transform: translate(-2px, 0); 76 | } 77 | 78 | 4%, 79 | 60% { 80 | transform: translate(-2px, 0); 81 | } 82 | 83 | 62% { 84 | transform: translate(-22px, 5px) skew(21deg); 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /static/custom.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --white: #faf5f6; 3 | --transwhite: rgba(250, 245, 246, .7); 4 | --moretranswhite: rgba(239, 239, 239, .1); 5 | --black: #252525; 6 | --transblack: rgba(37, 37, 37, .7); 7 | --moretransblack: rgba(37, 37, 37, .1); 8 | --gray: #6b6b6b; 9 | --highlight: #e5ffc3; 10 | --red: #e4002b; 11 | --green: #24d05a; 12 | --pink: #eb4888; 13 | --blue: #10a2f5; 14 | --yellow: #e9bc3f; 15 | } 16 | 17 | body { 18 | margin: 0; 19 | padding: 0; 20 | font-family: iA Writer Mono,monospace 21 | } 22 | 23 | /*main {*/ 24 | /* content: '';*/ 25 | /* display: block;*/ 26 | /* z-index: -1;*/ 27 | /* position: absolute;*/ 28 | /* top: 0;*/ 29 | /* right: 0;*/ 30 | /* bottom: 0;*/ 31 | /* left: 0;*/ 32 | /*}*/ 33 | 34 | section { 35 | height: 100%; 36 | } 37 | 38 | canvas { 39 | display: block; 40 | vertical-align: bottom; 41 | } 42 | 43 | /* ---- particles.js container ---- */ 44 | 45 | #particles-js { 46 | z-index: -2; 47 | position: absolute; 48 | width: 100%; 49 | height: 90vh; 50 | background-image: url(""); 51 | background-repeat: no-repeat; 52 | background-size: cover; 53 | background-position: 50% 50%; 54 | } 55 | 56 | @keyframes rotate-gradient { 57 | to { 58 | transform: rotate(360deg) 59 | } 60 | } 61 | 62 | a { 63 | color: var(--black); 64 | text-decoration-thickness: .3ex; 65 | text-underline-offset: .3ex; 66 | } 67 | 68 | a:hover { 69 | font-weight: bolder; 70 | text-decoration: underline; 71 | } 72 | 73 | footer { 74 | position: absolute; 75 | bottom: 0; 76 | left: 0; 77 | width: 100%; 78 | text-align: center; 79 | } 80 | 81 | 82 | footer a { 83 | text-decoration: none; 84 | text-transform: uppercase; 85 | width: 100%; 86 | display: block; 87 | margin: 0; 88 | padding: 1.25rem 0; 89 | } 90 | 91 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "axum-template" 3 | version = "0.1.0" 4 | authors = ["Oleks Pickle <22867443+olekspickle@users.noreply.github.com>"] 5 | edition = "2021" 6 | licence = "MIT OR Apache-2.0" 7 | repository = "https://github.com/olekspickle/axum-template" 8 | description = """ 9 | Axum template with askama templates and a bit of htmx magic and simple surrealdb setup 10 | """ 11 | 12 | [features] 13 | default = ["sqlite"] 14 | sqlite = ["deadpool", "deadpool-sqlite", "rusqlite", "serde_rusqlite"] 15 | surreal = ["surrealdb"] 16 | 17 | [dependencies] 18 | anyhow = "1" 19 | askama = "0.12" 20 | axum = { version = "0.7", features = ["macros"] } 21 | hyper = "1" 22 | rustls = "0.23.15" 23 | serde = { version = "1", features = ["derive"] } 24 | serde_json = "1" 25 | thiserror = "1" 26 | tokio = { version = "1", features = ["macros", "rt-multi-thread"] } 27 | tower-http = { version = "0.5", features = ["fs", "trace"] } 28 | tracing = "0.1" 29 | tracing-subscriber = { version = "0.3", features = ["env-filter"] } 30 | zip = { version = "2", default-features = false, features = ["aes-crypto"] } 31 | 32 | #sqlite 33 | deadpool = { version = "0.12.1", optional = true } 34 | deadpool-sqlite = { version = "0.9.0", optional = true } 35 | rusqlite = { version = "0.32.1", features = ["bundled"], optional = true } 36 | serde_rusqlite = { version ="0.36.0", optional = true } 37 | 38 | # surrealdb 39 | surrealdb = { version = "2", optional = true } 40 | http = "1.1.0" 41 | displaydoc = "0.2.5" 42 | 43 | # Optimizations 44 | 45 | # Optimized to compile times 46 | # [profile.dev] 47 | # debug = 0 48 | # [profile.release] 49 | # debug = 0 50 | 51 | # Optimized to minimum size bin target 52 | # https://doc.rust-lang.org/cargo/reference/profiles.html 53 | # if you want more optimization there is only no_std path and other hacks 54 | [profile.release] 55 | opt-level = "z" # Optimize for size. 56 | lto = true # Enable Link Time Optimization 57 | panic = "abort" # Abort on panic 58 | strip = "symbols" # (or true) Automatically strip symbols from the binary (). 59 | codegen-units = 1 # Reduce number of codegen units to increase optimizations. 60 | 61 | # Optimized to runtime performance 62 | # [profile.release] 63 | # lto = true 64 | # codegen-units = 1 65 | # panic = "abort" 66 | 67 | # statically link C runtime on *-musl targets 68 | # https://github.com/rust-lang/rust/issues/59302 69 | # [target.x86_64-unknown-linux-musl] 70 | # rustflags = "-Ctarget-feature=-crt-static" 71 | -------------------------------------------------------------------------------- /templates/head.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | {{ title }} 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | {% if title=="404" %} 47 | 48 | {% endif %} 49 | 50 | -------------------------------------------------------------------------------- /static/particles-cfg.js: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------- 2 | /* Author : Vincent Garreau - vincentgarreau.com 3 | /* MIT license: http://opensource.org/licenses/MIT 4 | /* Demo / Generator : vincentgarreau.com/particles.js 5 | /* GitHub : github.com/VincentGarreau/particles.js 6 | /* How to use? : Check the GitHub README 7 | /* v2.0.0 8 | /* ----------------------------------------------- */ 9 | 10 | const FG_COLOR = "#000000"; 11 | const BG_COLOR = "#ffffff"; 12 | const OTHER_COLOR = "#ff0000"; 13 | const N = 100; 14 | 15 | /* ---- particles.js config ---- */ 16 | particlesJS("particles-js", { 17 | "particles": { 18 | "number": { 19 | "value": N, 20 | "density": { 21 | "enable": true, 22 | "value_area": 800 23 | } 24 | }, 25 | "color": { 26 | "value": FG_COLOR 27 | }, 28 | "shape": { 29 | "type": "circle", 30 | "stroke": { 31 | "width": 0, 32 | "color": FG_COLOR 33 | }, 34 | "polygon": { 35 | "nb_sides": 5 36 | }, 37 | "image": { 38 | "src": "img/github.svg", 39 | "width": 100, 40 | "height": 100 41 | } 42 | }, 43 | "opacity": { 44 | "value": 0.5, 45 | "random": false, 46 | "anim": { 47 | "enable": false, 48 | "speed": 1, 49 | "opacity_min": 0.1, 50 | "sync": false 51 | } 52 | }, 53 | "size": { 54 | "value": 3, 55 | "random": true, 56 | "anim": { 57 | "enable": false, 58 | "speed": 40, 59 | "size_min": 0.1, 60 | "sync": false 61 | } 62 | }, 63 | "line_linked": { 64 | "enable": true, 65 | "distance": 150, 66 | "color": FG_COLOR, 67 | "opacity": 0.4, 68 | "width": 1 69 | }, 70 | "move": { 71 | "enable": true, 72 | "speed": 6, 73 | "direction": "none", 74 | "random": false, 75 | "straight": false, 76 | "out_mode": "out", 77 | "bounce": false, 78 | "attract": { 79 | "enable": false, 80 | "rotateX": 600, 81 | "rotateY": 1200 82 | } 83 | } 84 | }, 85 | "interactivity": { 86 | "detect_on": "canvas", 87 | "events": { 88 | "onhover": { 89 | "enable": true, 90 | "mode": "grab" 91 | }, 92 | "onclick": { 93 | "enable": true, 94 | "mode": "push" 95 | }, 96 | "resize": true 97 | }, 98 | "modes": { 99 | "grab": { 100 | "distance": 140, 101 | "line_linked": { 102 | "opacity": 1 103 | } 104 | }, 105 | "bubble": { 106 | "distance": 400, 107 | "size": 40, 108 | "duration": 2, 109 | "opacity": 8, 110 | "speed": 3 111 | }, 112 | "repulse": { 113 | "distance": 200, 114 | "duration": 0.4 115 | }, 116 | "push": { 117 | "particles_nb": 4 118 | }, 119 | "remove": { 120 | "particles_nb": 2 121 | } 122 | } 123 | }, 124 | "retina_detect": true 125 | }); 126 | 127 | -------------------------------------------------------------------------------- /src/db/sqlite.rs: -------------------------------------------------------------------------------- 1 | use super::{NewPost, Post}; 2 | use anyhow::Result; 3 | use deadpool_sqlite::{Config, Pool, Runtime}; 4 | use serde_rusqlite::*; 5 | use std::sync::LazyLock; 6 | use tracing::debug; 7 | 8 | /// SQlite connection pool singleton 9 | pub static DB: LazyLock = LazyLock::new(|| { 10 | let cfg = Config::new("posts.sqlite3"); 11 | debug!("Initializing sqlite connection pool..."); 12 | cfg.create_pool(Runtime::Tokio1) 13 | .expect("failed to initialize pool") 14 | }); 15 | 16 | /// Init DB: create posts table 17 | pub async fn init() -> Result<()> { 18 | let conn = DB.get().await?; 19 | debug!("Creating posts table..."); 20 | if let Ok(conn) = conn.try_lock() { 21 | let _ = conn.execute( 22 | "CREATE TABLE IF NOT EXISTS posts ( 23 | id INTEGER PRIMARY KEY, 24 | user TEXT NOT NULL, 25 | content TEXT NOT NULL, 26 | created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP 27 | );", 28 | (), 29 | ); 30 | } 31 | 32 | Ok(()) 33 | } 34 | 35 | pub(crate) async fn get_all_posts() -> Result> { 36 | let conn = DB.get().await?; 37 | if let Ok(conn) = conn.try_lock() { 38 | let mut stmt = conn.prepare_cached("SELECT * FROM posts LIMIT 100")?; 39 | let rows = stmt.query_and_then([], from_row::)?; 40 | 41 | // TODO: I have no idea why this repack is needed to be honest 42 | // I'm sure it should be possible to just deserialize rows query using serde_rusqlite 43 | let mut posts = Vec::new(); 44 | for post_result in rows { 45 | posts.push(post_result?); 46 | } 47 | 48 | return Ok(posts); 49 | } 50 | 51 | Ok(Default::default()) 52 | } 53 | 54 | pub(crate) async fn delete_post(id: u32) -> Result<()> { 55 | let conn = DB.get().await?; 56 | if let Ok(conn) = conn.try_lock() { 57 | let mut stmt = conn.prepare_cached("DELETE FROM posts WHERE id=(?1)")?; 58 | let _ = stmt.execute([id])?; 59 | return Ok(()); 60 | } 61 | 62 | Err(anyhow::format_err!( 63 | "Could not lock on pool for some reason" 64 | )) 65 | } 66 | 67 | pub(crate) async fn add_post(post: NewPost) -> Result<()> { 68 | let conn = DB.get().await?; 69 | if let Ok(conn) = conn.try_lock() { 70 | let mut stmt = conn.prepare_cached( 71 | r#" 72 | INSERT INTO posts (user, content) VALUES (?1, ?2)"#, 73 | )?; 74 | let _ = stmt.execute([post.user, post.content])?; 75 | return Ok(()); 76 | } 77 | 78 | Err(anyhow::format_err!( 79 | "Could not lock on pool for some reason" 80 | )) 81 | } 82 | 83 | pub(crate) async fn update_post(id: u32, post: NewPost) -> Result<()> { 84 | let conn = DB.get().await?; 85 | if let Ok(conn) = conn.try_lock() { 86 | let mut stmt = 87 | conn.prepare_cached("UPDATE posts SET user=(?2), content=(?3) WHERE id=(?1)")?; 88 | let _ = stmt.execute([id.to_string(), post.user, post.content])?; 89 | return Ok(()); 90 | } 91 | 92 | Err(anyhow::format_err!( 93 | "Could not lock on pool for some reason" 94 | )) 95 | } 96 | -------------------------------------------------------------------------------- /src/db/surreal.rs: -------------------------------------------------------------------------------- 1 | //! ## surrealDB api wrappers 2 | //! 3 | //! [Rust SDK reference](https://surrealdb.com/docs/sdk/rust) 4 | use crate::handlers::{NewPost, Post}; 5 | use std::{env, sync::LazyLock}; 6 | use surrealdb::{ 7 | engine::remote::ws::{Client, Ws}, 8 | opt::auth::Root, 9 | RecordId, Surreal, 10 | }; 11 | 12 | /// SurrealDB connection singleton 13 | /// TODO: should be possible to marry the deadpool with it in the future 14 | /// There IS a crate for it: https://docs.rs/deadpool-surrealdb/latest/deadpool_surrealdb/ 15 | /// but one guy wrote it, the other has published, it is a mess 16 | pub static DB: LazyLock> = LazyLock::new(|| { 17 | tracing::debug!(endpoint=%SURREAL_URL.clone(), "trying to connect to DB"); 18 | let db = Surreal::init(); 19 | // Connect to the server 20 | db.connect::(SURREAL_URL.clone()).await?; 21 | 22 | // Signin as a namespace, database, or root user 23 | db.signin(Root { 24 | username: &SURREAL_USER, 25 | password: &SURREAL_PASS, 26 | }) 27 | .await?; 28 | db.use_ns("test").use_db("actix-template").await?; 29 | 30 | db 31 | }); 32 | 33 | /// Fetch SURREAL_URL var or substitute with local 34 | static SURREAL_URL: LazyLock = 35 | LazyLock::new(|| env::var("SURREALDB_URL").unwrap_or("localhost:8000".into())); 36 | static SURREAL_USER: LazyLock = 37 | LazyLock::new(|| env::var("SURREALDB_USER").unwrap_or("root".into())); 38 | static SURREAL_PASS: LazyLock = 39 | LazyLock::new(|| env::var("SURREALDB_PASS").unwrap_or("root".into())); 40 | 41 | pub async fn init() -> anyhow::Result<()> { 42 | // some more fields to experiment with 43 | //DEFINE FIELD likes ON TABLE posts TYPE int; -- Number of likes 44 | //DEFINE FIELD comments ON TABLE posts TYPE array; -- Comments on the post 45 | //DEFINE FIELD image_url ON TABLE posts TYPE string; -- Image URL, if any 46 | //DEFINE FIELD tags ON TABLE posts TYPE array; -- Tags or hashtags 47 | DB.query( 48 | " 49 | DEFINE TABLE IF NOT EXISTS posts SCHEMAFULL 50 | // create/select only when authorized and update/delete only 51 | // the records authenticated connection created 52 | PERMISSIONS FOR 53 | CREATE, SELECT WHERE $auth, 54 | FOR UPDATE, DELETE WHERE created_by = $auth; 55 | // username 56 | DEFINE FIELD IF NOT EXISTS user ON TABLE posts TYPE string; 57 | // post content 58 | DEFINE FIELD IF NOT EXISTS content ON TABLE posts TYPE string; 59 | // timestamp post was created at 60 | DEFINE FIELD IF NOT EXISTS created_at ON TABLE posts TYPE datetime; 61 | // authenticated connection id 62 | DEFINE FIELD IF NOT EXISTS created_by ON TABLE person VALUE $auth READONLY; 63 | ", 64 | ) 65 | .await?; 66 | Ok(()) 67 | } 68 | 69 | pub async fn get_all_posts() -> anyhow::Result> { 70 | let posts: Vec = DB.select("post").await?; 71 | dbg!(&posts); 72 | Ok(posts) 73 | } 74 | 75 | pub async fn add_post(post: NewPost) -> anyhow::Result<()> { 76 | // some more fields to experiment with 77 | // "likes": 0, 78 | // "comments": [], 79 | // "image_url": "https://example.com/image.jpg", 80 | // "tags": ["food", "lunch", "amazing"] 81 | let created: Option = DB.create("post").content(post).await?; 82 | let id = created.unwrap_or_default(); 83 | tracing::debug!(id, "Inserted new post"); 84 | Ok(()) 85 | } 86 | -------------------------------------------------------------------------------- /templates/posts.html: -------------------------------------------------------------------------------- 1 | {% extends "home.html" %} 2 | 3 | {% block content %} 4 | 5 |
6 |
7 | 8 |
9 | 10 | 18 | 19 | 20 |
21 | 22 | 23 |
    24 | {% for post in posts %} 25 | 26 |
  • 27 |
    28 |

    {{ post.user }}

    29 |

    35 | {{ post.content }} 36 |

    37 |
    38 |
    39 | 44 |
    45 |
    46 | 51 |
    52 |
    53 |
    54 |
  • 55 | {% endfor %} 56 |
57 |
58 |
59 | 60 | 103 | {% endblock content %} 104 | -------------------------------------------------------------------------------- /src/handlers.rs: -------------------------------------------------------------------------------- 1 | use crate::{db, form_zip, form_zip::ZIP}; 2 | use askama::Template; 3 | use axum::{ 4 | extract::{Path, Query}, 5 | http::{StatusCode, Uri}, 6 | response::{Html, IntoResponse, Redirect, Response}, 7 | Form, 8 | }; 9 | use hyper::{header, HeaderMap}; 10 | use serde::Deserialize; 11 | use tracing::{error, trace, warn}; 12 | 13 | pub async fn home() -> impl IntoResponse { 14 | let template = templates::Home { 15 | title: "Home".to_owned(), 16 | }; 17 | HtmlTemplate(template) 18 | } 19 | 20 | #[derive(Deserialize)] 21 | pub struct Hello { 22 | name: Option, 23 | } 24 | 25 | pub async fn hello(Query(hello): Query) -> impl IntoResponse { 26 | let name = hello.name.clone().map_or("stranger".to_string(), |l| l); 27 | 28 | let html = templates::Hello { 29 | name, 30 | title: "Hello".into(), 31 | }; 32 | 33 | HtmlTemplate(html) 34 | } 35 | 36 | pub async fn posts() -> impl IntoResponse { 37 | let posts = db::get_all_posts().await.expect("getting all posts failed"); 38 | trace!("fetched posts: {}", posts.len()); 39 | 40 | let html = templates::Posts { 41 | title: "Posts".into(), 42 | posts, 43 | }; 44 | 45 | HtmlTemplate(html) 46 | } 47 | 48 | pub async fn add_post(Form(post): Form) -> impl IntoResponse { 49 | dbg!(&post); 50 | trace!(?post, "Adding new post"); 51 | db::add_post(post).await.expect("failed to add post"); 52 | 53 | Redirect::to("/posts").into_response() 54 | } 55 | 56 | pub async fn update_post(Path(id): Path, Form(post): Form) -> impl IntoResponse { 57 | trace!(%id, ?post, "Update"); 58 | db::update_post(id, post).await.expect("failed to add post"); 59 | 60 | Redirect::permanent("/posts").into_response() 61 | } 62 | 63 | pub async fn delete_post(Path(id): Path) -> impl IntoResponse { 64 | trace!(%id, "Delete"); 65 | db::delete_post(id).await.expect("failed to add post"); 66 | 67 | Redirect::permanent("/posts").into_response() 68 | } 69 | 70 | /// Just a test handle that will fail in docker container 71 | /// to illustrate how axum can silently fail if you encounter unhandled error 72 | pub async fn fetch_zip() -> impl IntoResponse { 73 | let zip_res = form_zip::create_zip(); 74 | let Ok(body) = zip_res else { 75 | warn!("Erorr forming zip file:{zip_res:?}"); 76 | return StatusCode::INTERNAL_SERVER_ERROR.into_response(); 77 | }; 78 | 79 | let mut headers = HeaderMap::new(); 80 | headers.insert(header::CONTENT_TYPE, "application/zip".parse().unwrap()); 81 | headers.insert( 82 | header::CONTENT_DISPOSITION, 83 | format!("attachment; filename=\"{ZIP}\"").parse().unwrap(), 84 | ); 85 | 86 | (headers, body).into_response() 87 | } 88 | 89 | pub async fn handle_404(uri: Uri) -> impl IntoResponse { 90 | error!("404 `{uri}`"); 91 | let template = templates::NotFoundTemplate { 92 | title: "404".to_owned(), 93 | uri: uri.to_string(), 94 | }; 95 | HtmlTemplate(template) 96 | } 97 | 98 | /// Basically all templates handling 99 | pub mod templates { 100 | use super::*; 101 | 102 | #[derive(Template)] 103 | #[template(path = "home.html")] 104 | pub struct Home { 105 | pub title: String, 106 | } 107 | 108 | #[derive(Template)] 109 | #[template(path = "hello.html")] 110 | pub struct Hello { 111 | pub title: String, 112 | pub name: String, 113 | } 114 | 115 | #[derive(Template)] 116 | #[template(path = "posts.html")] 117 | pub struct Posts { 118 | pub title: String, 119 | pub posts: Vec, 120 | } 121 | 122 | #[derive(Template)] 123 | #[template(path = "404.html")] 124 | pub struct NotFoundTemplate { 125 | pub title: String, 126 | pub uri: String, 127 | } 128 | } 129 | 130 | struct HtmlTemplate(T); 131 | 132 | impl IntoResponse for HtmlTemplate 133 | where 134 | T: Template, 135 | { 136 | fn into_response(self) -> Response { 137 | match self.0.render() { 138 | Ok(html) => Html(html).into_response(), 139 | Err(err) => ( 140 | StatusCode::INTERNAL_SERVER_ERROR, 141 | format!("Failed to render the template. Error: {}", err), 142 | ) 143 | .into_response(), 144 | } 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /src/main.rs: -------------------------------------------------------------------------------- 1 | //! ![axum-template](https://github.com/user-attachments/assets/a16843e7-7537-4c73-a550-52a37b6fbf73) 2 | //! 3 | //! ## Overview 4 | //! Template to have something to get-go in some situations 5 | //! 6 | //! This template provides: 7 | //! - [x] Axum server(with middleware) 8 | //! - [x] Askama templates 9 | //! - [x] Containerization(with compose) 10 | //! - [x] Greeter page with query param name 11 | //! - [x] Sqlite backend 12 | //! - [ ] SurrealDB backend 13 | //! 14 | //! # Running 15 | //! ```bash 16 | //! # Sqlite3 backend: 17 | //! make run 18 | //! 19 | //! # surrealdb backend 20 | //! make surreal 21 | //! 22 | //! ``` 23 | //! 24 | //! You can peek into Makefile for build details 25 | //! 26 | //! ## Afterthoughts and issues 27 | //! I found axum to be the most ergonomic web framework out there, and while there might be not 28 | //! enough examples at the moment, it is quite a breeze to use 29 | //! - static files was sure one noticeable pain in the rear to figure out 30 | //! - surrealdb sure adds complexity, I'm adding it under a feature because sqlite integration is 31 | //! so much less crates to compile(190+ vs 500+) 32 | //! 33 | use axum::{ 34 | middleware::from_fn, 35 | routing::{delete, get, patch, post}, 36 | Router, 37 | }; 38 | use std::net::SocketAddr; 39 | use tokio::net::TcpListener; 40 | use tower_http::{services::ServeDir, trace::TraceLayer}; 41 | use tracing::info; 42 | 43 | mod db; 44 | mod error; 45 | mod form_zip; 46 | mod handlers; 47 | mod middleware; 48 | 49 | #[tokio::main] 50 | async fn main() -> anyhow::Result<()> { 51 | tracing_init(); 52 | 53 | // init DB in the background 54 | tokio::spawn(async move { 55 | let res = db::init().await; 56 | if let Err(e) = res { 57 | eprintln!("connection error: {}", e); 58 | } 59 | }); 60 | 61 | // Static asset service 62 | let serve_dir = ServeDir::new("static").not_found_service(ServeDir::new("templates/404.html")); 63 | let router = Router::new() 64 | .route("/", get(handlers::home)) 65 | .route("/hello", get(handlers::hello)) 66 | .route("/posts", get(handlers::posts)) 67 | .route("/add-post", post(handlers::add_post)) 68 | .route("/update-post/:id", patch(handlers::update_post)) 69 | .route("/delete-post/:id", delete(handlers::delete_post)) 70 | .route("/fetch-zip", get(handlers::fetch_zip)) 71 | .nest_service("/static", serve_dir.clone()) 72 | .fallback(handlers::handle_404) 73 | .layer(from_fn(middleware::auth)) 74 | .layer(from_fn(middleware::log)) 75 | .layer(TraceLayer::new_for_http()) 76 | .into_make_service(); 77 | 78 | let addr = SocketAddr::from(([0, 0, 0, 0], 7777)); 79 | let listener = TcpListener::bind(addr).await?; 80 | info!("listening on {}", addr); 81 | 82 | axum::serve(listener, router).await.unwrap(); 83 | Ok(()) 84 | } 85 | 86 | fn tracing_init() { 87 | use tracing::Level; 88 | use tracing_subscriber::{ 89 | filter, fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter, 90 | }; 91 | 92 | const NAME: &str = env!("CARGO_PKG_NAME"); 93 | 94 | let event_format = fmt::format().with_line_number(true); 95 | let sub_fmt = tracing_subscriber::fmt::layer().event_format(event_format); 96 | 97 | let fallback_log_level: EnvFilter = match cfg!(debug_assertions) { 98 | true => format!("info,{NAME}=debug").into(), 99 | _ => "info".into(), 100 | }; 101 | let log_level = EnvFilter::try_from_default_env().unwrap_or(fallback_log_level); 102 | let fltr = filter::Targets::new() 103 | .with_target("tower_http::trace::on_response", Level::TRACE) 104 | //.with_target("tower_http::trace::on_request", Level::TRACE) 105 | .with_target("tower_http::trace::make_span", Level::DEBUG) 106 | .with_default(Level::INFO); 107 | 108 | info!(%log_level, "Using tracing"); 109 | tracing_subscriber::registry() 110 | .with(sub_fmt) 111 | .with(log_level) 112 | .with(fltr) 113 | .init(); 114 | } 115 | 116 | ///// use openssl to generate ssl certs 117 | ///// openssl req -newkey rsa:2048 -new -nodes -keyout key.pem -out csr.pem 118 | ///// 119 | ///// or for dev purposes 120 | ///// 121 | ///// openssl req -newkey rsa:2048 -new -nodes -x509 -days 3650 -keyout key.pem -out cert.pem -addext "subjectAltName = DNS:mydnsname.com" 122 | // fn _load_rustls_config() -> rustls::ServerConfig { 123 | // use std::{fs::File, io::BufReader}; 124 | // 125 | // use rustls::{pki_types::PrivateKeyDer, ServerConfig}; 126 | // use rustls_pemfile::{certs, pkcs8_private_keys}; 127 | // 128 | // rustls::crypto::aws_lc_rs::default_provider() 129 | // .install_default() 130 | // .unwrap(); 131 | // 132 | // // init server config builder with safe defaults 133 | // let config = ServerConfig::builder().with_no_client_auth(); 134 | // 135 | // // load TLS key/cert files 136 | // let cert_file = &mut BufReader::new(File::open("cert.pem").unwrap()); 137 | // let key_file = &mut BufReader::new(File::open("key.pem").unwrap()); 138 | // 139 | // // convert files to key/cert objects 140 | // let cert_chain = certs(cert_file).collect::, _>>().unwrap(); 141 | // let mut keys = pkcs8_private_keys(key_file) 142 | // .map(|key| key.map(PrivateKeyDer::Pkcs8)) 143 | // .collect::, _>>() 144 | // .unwrap(); 145 | // 146 | // // exit if no keys could be parsed 147 | // if keys.is_empty() { 148 | // eprintln!("Could not locate PKCS 8 private keys."); 149 | // std::process::exit(1); 150 | // } 151 | // 152 | // config.with_single_cert(cert_chain, keys.remove(0)).unwrap() 153 | // } 154 | -------------------------------------------------------------------------------- /LICENCE-APACHE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | -------------------------------------------------------------------------------- /static/particles.js: -------------------------------------------------------------------------------- 1 | /* ----------------------------------------------- 2 | /* Author : Vincent Garreau - vincentgarreau.com 3 | /* MIT license: http://opensource.org/licenses/MIT 4 | /* Demo / Generator : vincentgarreau.com/particles.js 5 | /* GitHub : github.com/VincentGarreau/particles.js 6 | /* How to use? : Check the GitHub README 7 | /* v2.0.0 8 | /* --------------------------------------------- 9 | 10 | /* ---- particles.js body ---- */ 11 | function hexToRgb(e){var a=/^#?([a-f\d])([a-f\d])([a-f\d])$/i;e=e.replace(a,function(e,a,t,i){return a+a+t+t+i+i});var t=/^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(e);return t?{r:parseInt(t[1],16),g:parseInt(t[2],16),b:parseInt(t[3],16)}:null}function clamp(e,a,t){return Math.min(Math.max(e,a),t)}function isInArray(e,a){return a.indexOf(e)>-1}var pJS=function(e,a){var t=document.querySelector("#"+e+" > .particles-js-canvas-el");this.pJS={canvas:{el:t,w:t.offsetWidth,h:t.offsetHeight},particles:{number:{value:400,density:{enable:!0,value_area:800}},color:{value:OTHER_COLOR},shape:{type:"circle",stroke:{width:0,color:BG_COLOR},polygon:{nb_sides:5},image:{src:"",width:100,height:100}},opacity:{value:1,random:!1,anim:{enable:!1,speed:2,opacity_min:0,sync:!1}},size:{value:20,random:!1,anim:{enable:!1,speed:20,size_min:0,sync:!1}},line_linked:{enable:!0,distance:100,color:OTHER_COLOR,opacity:1,width:1},move:{enable:!0,speed:2,direction:"none",random:!1,straight:!1,out_mode:"out",bounce:!1,attract:{enable:!1,rotateX:3e3,rotateY:3e3}},array:[]},interactivity:{detect_on:"canvas",events:{onhover:{enable:!0,mode:"grab"},onclick:{enable:!0,mode:"push"},resize:!0},modes:{grab:{distance:100,line_linked:{opacity:1}},bubble:{distance:200,size:80,duration:.4},repulse:{distance:200,duration:.4},push:{particles_nb:4},remove:{particles_nb:2}},mouse:{}},retina_detect:!1,fn:{interact:{},modes:{},vendors:{}},tmp:{}};var i=this.pJS;a&&Object.deepExtend(i,a),i.tmp.obj={size_value:i.particles.size.value,size_anim_speed:i.particles.size.anim.speed,move_speed:i.particles.move.speed,line_linked_distance:i.particles.line_linked.distance,line_linked_width:i.particles.line_linked.width,mode_grab_distance:i.interactivity.modes.grab.distance,mode_bubble_distance:i.interactivity.modes.bubble.distance,mode_bubble_size:i.interactivity.modes.bubble.size,mode_repulse_distance:i.interactivity.modes.repulse.distance},i.fn.retinaInit=function(){i.retina_detect&&window.devicePixelRatio>1?(i.canvas.pxratio=window.devicePixelRatio,i.tmp.retina=!0):(i.canvas.pxratio=1,i.tmp.retina=!1),i.canvas.w=i.canvas.el.offsetWidth*i.canvas.pxratio,i.canvas.h=i.canvas.el.offsetHeight*i.canvas.pxratio,i.particles.size.value=i.tmp.obj.size_value*i.canvas.pxratio,i.particles.size.anim.speed=i.tmp.obj.size_anim_speed*i.canvas.pxratio,i.particles.move.speed=i.tmp.obj.move_speed*i.canvas.pxratio,i.particles.line_linked.distance=i.tmp.obj.line_linked_distance*i.canvas.pxratio,i.interactivity.modes.grab.distance=i.tmp.obj.mode_grab_distance*i.canvas.pxratio,i.interactivity.modes.bubble.distance=i.tmp.obj.mode_bubble_distance*i.canvas.pxratio,i.particles.line_linked.width=i.tmp.obj.line_linked_width*i.canvas.pxratio,i.interactivity.modes.bubble.size=i.tmp.obj.mode_bubble_size*i.canvas.pxratio,i.interactivity.modes.repulse.distance=i.tmp.obj.mode_repulse_distance*i.canvas.pxratio},i.fn.canvasInit=function(){i.canvas.ctx=i.canvas.el.getContext("2d")},i.fn.canvasSize=function(){i.canvas.el.width=i.canvas.w,i.canvas.el.height=i.canvas.h,i&&i.interactivity.events.resize&&window.addEventListener("resize",function(){i.canvas.w=i.canvas.el.offsetWidth,i.canvas.h=i.canvas.el.offsetHeight,i.tmp.retina&&(i.canvas.w*=i.canvas.pxratio,i.canvas.h*=i.canvas.pxratio),i.canvas.el.width=i.canvas.w,i.canvas.el.height=i.canvas.h,i.particles.move.enable||(i.fn.particlesEmpty(),i.fn.particlesCreate(),i.fn.particlesDraw(),i.fn.vendors.densityAutoParticles()),i.fn.vendors.densityAutoParticles()})},i.fn.canvasPaint=function(){i.canvas.ctx.fillRect(0,0,i.canvas.w,i.canvas.h)},i.fn.canvasClear=function(){i.canvas.ctx.clearRect(0,0,i.canvas.w,i.canvas.h)},i.fn.particle=function(e,a,t){if(this.radius=(i.particles.size.random?Math.random():1)*i.particles.size.value,i.particles.size.anim.enable&&(this.size_status=!1,this.vs=i.particles.size.anim.speed/100,i.particles.size.anim.sync||(this.vs=this.vs*Math.random())),this.x=t?t.x:Math.random()*i.canvas.w,this.y=t?t.y:Math.random()*i.canvas.h,this.x>i.canvas.w-2*this.radius?this.x=this.x-this.radius:this.x<2*this.radius&&(this.x=this.x+this.radius),this.y>i.canvas.h-2*this.radius?this.y=this.y-this.radius:this.y<2*this.radius&&(this.y=this.y+this.radius),i.particles.move.bounce&&i.fn.vendors.checkOverlap(this,t),this.color={},"object"==typeof e.value)if(e.value instanceof Array){var s=e.value[Math.floor(Math.random()*i.particles.color.value.length)];this.color.rgb=hexToRgb(s)}else void 0!=e.value.r&&void 0!=e.value.g&&void 0!=e.value.b&&(this.color.rgb={r:e.value.r,g:e.value.g,b:e.value.b}),void 0!=e.value.h&&void 0!=e.value.s&&void 0!=e.value.l&&(this.color.hsl={h:e.value.h,s:e.value.s,l:e.value.l});else"random"==e.value?this.color.rgb={r:Math.floor(256*Math.random())+0,g:Math.floor(256*Math.random())+0,b:Math.floor(256*Math.random())+0}:"string"==typeof e.value&&(this.color=e,this.color.rgb=hexToRgb(this.color.value));this.opacity=(i.particles.opacity.random?Math.random():1)*i.particles.opacity.value,i.particles.opacity.anim.enable&&(this.opacity_status=!1,this.vo=i.particles.opacity.anim.speed/100,i.particles.opacity.anim.sync||(this.vo=this.vo*Math.random()));var n={};switch(i.particles.move.direction){case"top":n={x:0,y:-1};break;case"top-right":n={x:.5,y:-.5};break;case"right":n={x:1,y:-0};break;case"bottom-right":n={x:.5,y:.5};break;case"bottom":n={x:0,y:1};break;case"bottom-left":n={x:-.5,y:1};break;case"left":n={x:-1,y:0};break;case"top-left":n={x:-.5,y:-.5};break;default:n={x:0,y:0}}i.particles.move.straight?(this.vx=n.x,this.vy=n.y,i.particles.move.random&&(this.vx=this.vx*Math.random(),this.vy=this.vy*Math.random())):(this.vx=n.x+Math.random()-.5,this.vy=n.y+Math.random()-.5),this.vx_i=this.vx,this.vy_i=this.vy;var r=i.particles.shape.type;if("object"==typeof r){if(r instanceof Array){var c=r[Math.floor(Math.random()*r.length)];this.shape=c}}else this.shape=r;if("image"==this.shape){var o=i.particles.shape;this.img={src:o.image.src,ratio:o.image.width/o.image.height},this.img.ratio||(this.img.ratio=1),"svg"==i.tmp.img_type&&void 0!=i.tmp.source_svg&&(i.fn.vendors.createSvgImg(this),i.tmp.pushing&&(this.img.loaded=!1))}},i.fn.particle.prototype.draw=function(){function e(){i.canvas.ctx.drawImage(r,a.x-t,a.y-t,2*t,2*t/a.img.ratio)}var a=this;if(void 0!=a.radius_bubble)var t=a.radius_bubble;else var t=a.radius;if(void 0!=a.opacity_bubble)var s=a.opacity_bubble;else var s=a.opacity;if(a.color.rgb)var n="rgba("+a.color.rgb.r+","+a.color.rgb.g+","+a.color.rgb.b+","+s+")";else var n="hsla("+a.color.hsl.h+","+a.color.hsl.s+"%,"+a.color.hsl.l+"%,"+s+")";switch(i.canvas.ctx.fillStyle=n,i.canvas.ctx.beginPath(),a.shape){case"circle":i.canvas.ctx.arc(a.x,a.y,t,0,2*Math.PI,!1);break;case"edge":i.canvas.ctx.rect(a.x-t,a.y-t,2*t,2*t);break;case"triangle":i.fn.vendors.drawShape(i.canvas.ctx,a.x-t,a.y+t/1.66,2*t,3,2);break;case"polygon":i.fn.vendors.drawShape(i.canvas.ctx,a.x-t/(i.particles.shape.polygon.nb_sides/3.5),a.y-t/.76,2.66*t/(i.particles.shape.polygon.nb_sides/3),i.particles.shape.polygon.nb_sides,1);break;case"star":i.fn.vendors.drawShape(i.canvas.ctx,a.x-2*t/(i.particles.shape.polygon.nb_sides/4),a.y-t/1.52,2*t*2.66/(i.particles.shape.polygon.nb_sides/3),i.particles.shape.polygon.nb_sides,2);break;case"image":if("svg"==i.tmp.img_type)var r=a.img.obj;else var r=i.tmp.img_obj;r&&e()}i.canvas.ctx.closePath(),i.particles.shape.stroke.width>0&&(i.canvas.ctx.strokeStyle=i.particles.shape.stroke.color,i.canvas.ctx.lineWidth=i.particles.shape.stroke.width,i.canvas.ctx.stroke()),i.canvas.ctx.fill()},i.fn.particlesCreate=function(){for(var e=0;e=i.particles.opacity.value&&(a.opacity_status=!1),a.opacity+=a.vo):(a.opacity<=i.particles.opacity.anim.opacity_min&&(a.opacity_status=!0),a.opacity-=a.vo),a.opacity<0&&(a.opacity=0)),i.particles.size.anim.enable&&(1==a.size_status?(a.radius>=i.particles.size.value&&(a.size_status=!1),a.radius+=a.vs):(a.radius<=i.particles.size.anim.size_min&&(a.size_status=!0),a.radius-=a.vs),a.radius<0&&(a.radius=0)),"bounce"==i.particles.move.out_mode)var s={x_left:a.radius,x_right:i.canvas.w,y_top:a.radius,y_bottom:i.canvas.h};else var s={x_left:-a.radius,x_right:i.canvas.w+a.radius,y_top:-a.radius,y_bottom:i.canvas.h+a.radius};switch(a.x-a.radius>i.canvas.w?(a.x=s.x_left,a.y=Math.random()*i.canvas.h):a.x+a.radius<0&&(a.x=s.x_right,a.y=Math.random()*i.canvas.h),a.y-a.radius>i.canvas.h?(a.y=s.y_top,a.x=Math.random()*i.canvas.w):a.y+a.radius<0&&(a.y=s.y_bottom,a.x=Math.random()*i.canvas.w),i.particles.move.out_mode){case"bounce":a.x+a.radius>i.canvas.w?a.vx=-a.vx:a.x-a.radius<0&&(a.vx=-a.vx),a.y+a.radius>i.canvas.h?a.vy=-a.vy:a.y-a.radius<0&&(a.vy=-a.vy)}if(isInArray("grab",i.interactivity.events.onhover.mode)&&i.fn.modes.grabParticle(a),(isInArray("bubble",i.interactivity.events.onhover.mode)||isInArray("bubble",i.interactivity.events.onclick.mode))&&i.fn.modes.bubbleParticle(a),(isInArray("repulse",i.interactivity.events.onhover.mode)||isInArray("repulse",i.interactivity.events.onclick.mode))&&i.fn.modes.repulseParticle(a),i.particles.line_linked.enable||i.particles.move.attract.enable)for(var n=e+1;n0){var c=i.particles.line_linked.color_rgb_line;i.canvas.ctx.strokeStyle="rgba("+c.r+","+c.g+","+c.b+","+r+")",i.canvas.ctx.lineWidth=i.particles.line_linked.width,i.canvas.ctx.beginPath(),i.canvas.ctx.moveTo(e.x,e.y),i.canvas.ctx.lineTo(a.x,a.y),i.canvas.ctx.stroke(),i.canvas.ctx.closePath()}}},i.fn.interact.attractParticles=function(e,a){var t=e.x-a.x,s=e.y-a.y,n=Math.sqrt(t*t+s*s);if(n<=i.particles.line_linked.distance){var r=t/(1e3*i.particles.move.attract.rotateX),c=s/(1e3*i.particles.move.attract.rotateY);e.vx-=r,e.vy-=c,a.vx+=r,a.vy+=c}},i.fn.interact.bounceParticles=function(e,a){var t=e.x-a.x,i=e.y-a.y,s=Math.sqrt(t*t+i*i),n=e.radius+a.radius;n>=s&&(e.vx=-e.vx,e.vy=-e.vy,a.vx=-a.vx,a.vy=-a.vy)},i.fn.modes.pushParticles=function(e,a){i.tmp.pushing=!0;for(var t=0;e>t;t++)i.particles.array.push(new i.fn.particle(i.particles.color,i.particles.opacity.value,{x:a?a.pos_x:Math.random()*i.canvas.w,y:a?a.pos_y:Math.random()*i.canvas.h})),t==e-1&&(i.particles.move.enable||i.fn.particlesDraw(),i.tmp.pushing=!1)},i.fn.modes.removeParticles=function(e){i.particles.array.splice(0,e),i.particles.move.enable||i.fn.particlesDraw()},i.fn.modes.bubbleParticle=function(e){function a(){e.opacity_bubble=e.opacity,e.radius_bubble=e.radius}function t(a,t,s,n,c){if(a!=t)if(i.tmp.bubble_duration_end){if(void 0!=s){var o=n-p*(n-a)/i.interactivity.modes.bubble.duration,l=a-o;d=a+l,"size"==c&&(e.radius_bubble=d),"opacity"==c&&(e.opacity_bubble=d)}}else if(r<=i.interactivity.modes.bubble.distance){if(void 0!=s)var v=s;else var v=n;if(v!=a){var d=n-p*(n-a)/i.interactivity.modes.bubble.duration;"size"==c&&(e.radius_bubble=d),"opacity"==c&&(e.opacity_bubble=d)}}else"size"==c&&(e.radius_bubble=void 0),"opacity"==c&&(e.opacity_bubble=void 0)}if(i.interactivity.events.onhover.enable&&isInArray("bubble",i.interactivity.events.onhover.mode)){var s=e.x-i.interactivity.mouse.pos_x,n=e.y-i.interactivity.mouse.pos_y,r=Math.sqrt(s*s+n*n),c=1-r/i.interactivity.modes.bubble.distance;if(r<=i.interactivity.modes.bubble.distance){if(c>=0&&"mousemove"==i.interactivity.status){if(i.interactivity.modes.bubble.size!=i.particles.size.value)if(i.interactivity.modes.bubble.size>i.particles.size.value){var o=e.radius+i.interactivity.modes.bubble.size*c;o>=0&&(e.radius_bubble=o)}else{var l=e.radius-i.interactivity.modes.bubble.size,o=e.radius-l*c;o>0?e.radius_bubble=o:e.radius_bubble=0}if(i.interactivity.modes.bubble.opacity!=i.particles.opacity.value)if(i.interactivity.modes.bubble.opacity>i.particles.opacity.value){var v=i.interactivity.modes.bubble.opacity*c;v>e.opacity&&v<=i.interactivity.modes.bubble.opacity&&(e.opacity_bubble=v)}else{var v=e.opacity-(i.particles.opacity.value-i.interactivity.modes.bubble.opacity)*c;v=i.interactivity.modes.bubble.opacity&&(e.opacity_bubble=v)}}}else a();"mouseleave"==i.interactivity.status&&a()}else if(i.interactivity.events.onclick.enable&&isInArray("bubble",i.interactivity.events.onclick.mode)){if(i.tmp.bubble_clicking){var s=e.x-i.interactivity.mouse.click_pos_x,n=e.y-i.interactivity.mouse.click_pos_y,r=Math.sqrt(s*s+n*n),p=((new Date).getTime()-i.interactivity.mouse.click_time)/1e3;p>i.interactivity.modes.bubble.duration&&(i.tmp.bubble_duration_end=!0),p>2*i.interactivity.modes.bubble.duration&&(i.tmp.bubble_clicking=!1,i.tmp.bubble_duration_end=!1)}i.tmp.bubble_clicking&&(t(i.interactivity.modes.bubble.size,i.particles.size.value,e.radius_bubble,e.radius,"size"),t(i.interactivity.modes.bubble.opacity,i.particles.opacity.value,e.opacity_bubble,e.opacity,"opacity"))}},i.fn.modes.repulseParticle=function(e){function a(){var a=Math.atan2(d,p);if(e.vx=u*Math.cos(a),e.vy=u*Math.sin(a),"bounce"==i.particles.move.out_mode){var t={x:e.x+e.vx,y:e.y+e.vy};t.x+e.radius>i.canvas.w?e.vx=-e.vx:t.x-e.radius<0&&(e.vx=-e.vx),t.y+e.radius>i.canvas.h?e.vy=-e.vy:t.y-e.radius<0&&(e.vy=-e.vy)}}if(i.interactivity.events.onhover.enable&&isInArray("repulse",i.interactivity.events.onhover.mode)&&"mousemove"==i.interactivity.status){var t=e.x-i.interactivity.mouse.pos_x,s=e.y-i.interactivity.mouse.pos_y,n=Math.sqrt(t*t+s*s),r={x:t/n,y:s/n},c=i.interactivity.modes.repulse.distance,o=100,l=clamp(1/c*(-1*Math.pow(n/c,2)+1)*c*o,0,50),v={x:e.x+r.x*l,y:e.y+r.y*l};"bounce"==i.particles.move.out_mode?(v.x-e.radius>0&&v.x+e.radius0&&v.y+e.radius=m&&a()}else 0==i.tmp.repulse_clicking&&(e.vx=e.vx_i,e.vy=e.vy_i)},i.fn.modes.grabParticle=function(e){if(i.interactivity.events.onhover.enable&&"mousemove"==i.interactivity.status){var a=e.x-i.interactivity.mouse.pos_x,t=e.y-i.interactivity.mouse.pos_y,s=Math.sqrt(a*a+t*t);if(s<=i.interactivity.modes.grab.distance){var n=i.interactivity.modes.grab.line_linked.opacity-s/(1/i.interactivity.modes.grab.line_linked.opacity)/i.interactivity.modes.grab.distance;if(n>0){var r=i.particles.line_linked.color_rgb_line;i.canvas.ctx.strokeStyle="rgba("+r.r+","+r.g+","+r.b+","+n+")",i.canvas.ctx.lineWidth=i.particles.line_linked.width,i.canvas.ctx.beginPath(),i.canvas.ctx.moveTo(e.x,e.y),i.canvas.ctx.lineTo(i.interactivity.mouse.pos_x,i.interactivity.mouse.pos_y),i.canvas.ctx.stroke(),i.canvas.ctx.closePath()}}}},i.fn.vendors.eventsListeners=function(){"window"==i.interactivity.detect_on?i.interactivity.el=window:i.interactivity.el=i.canvas.el,(i.interactivity.events.onhover.enable||i.interactivity.events.onclick.enable)&&(i.interactivity.el.addEventListener("mousemove",function(e){if(i.interactivity.el==window)var a=e.clientX,t=e.clientY;else var a=e.offsetX||e.clientX,t=e.offsetY||e.clientY;i.interactivity.mouse.pos_x=a,i.interactivity.mouse.pos_y=t,i.tmp.retina&&(i.interactivity.mouse.pos_x*=i.canvas.pxratio,i.interactivity.mouse.pos_y*=i.canvas.pxratio),i.interactivity.status="mousemove"}),i.interactivity.el.addEventListener("mouseleave",function(e){i.interactivity.mouse.pos_x=null,i.interactivity.mouse.pos_y=null,i.interactivity.status="mouseleave"})),i.interactivity.events.onclick.enable&&i.interactivity.el.addEventListener("click",function(){if(i.interactivity.mouse.click_pos_x=i.interactivity.mouse.pos_x,i.interactivity.mouse.click_pos_y=i.interactivity.mouse.pos_y,i.interactivity.mouse.click_time=(new Date).getTime(),i.interactivity.events.onclick.enable)switch(i.interactivity.events.onclick.mode){case"push":i.particles.move.enable?i.fn.modes.pushParticles(i.interactivity.modes.push.particles_nb,i.interactivity.mouse):1==i.interactivity.modes.push.particles_nb?i.fn.modes.pushParticles(i.interactivity.modes.push.particles_nb,i.interactivity.mouse):i.interactivity.modes.push.particles_nb>1&&i.fn.modes.pushParticles(i.interactivity.modes.push.particles_nb);break;case"remove":i.fn.modes.removeParticles(i.interactivity.modes.remove.particles_nb);break;case"bubble":i.tmp.bubble_clicking=!0;break;case"repulse":i.tmp.repulse_clicking=!0,i.tmp.repulse_count=0,i.tmp.repulse_finish=!1,setTimeout(function(){i.tmp.repulse_clicking=!1},1e3*i.interactivity.modes.repulse.duration)}})},i.fn.vendors.densityAutoParticles=function(){if(i.particles.number.density.enable){var e=i.canvas.el.width*i.canvas.el.height/1e3;i.tmp.retina&&(e/=2*i.canvas.pxratio);var a=e*i.particles.number.value/i.particles.number.density.value_area,t=i.particles.array.length-a;0>t?i.fn.modes.pushParticles(Math.abs(t)):i.fn.modes.removeParticles(t)}},i.fn.vendors.checkOverlap=function(e,a){for(var t=0;tv;v++)e.lineTo(i,0),e.translate(i,0),e.rotate(l);e.fill(),e.restore()},i.fn.vendors.exportImg=function(){window.open(i.canvas.el.toDataURL("image/png"),"_blank")},i.fn.vendors.loadImg=function(e){if(i.tmp.img_error=void 0,""!=i.particles.shape.image.src)if("svg"==e){var a=new XMLHttpRequest;a.open("GET",i.particles.shape.image.src),a.onreadystatechange=function(e){4==a.readyState&&(200==a.status?(i.tmp.source_svg=e.currentTarget.response,i.fn.vendors.checkBeforeDraw()):(console.log("Error pJS - Image not found"),i.tmp.img_error=!0))},a.send()}else{var t=new Image;t.addEventListener("load",function(){i.tmp.img_obj=t,i.fn.vendors.checkBeforeDraw()}),t.src=i.particles.shape.image.src}else console.log("Error pJS - No image.src"),i.tmp.img_error=!0},i.fn.vendors.draw=function(){"image"==i.particles.shape.type?"svg"==i.tmp.img_type?i.tmp.count_svg>=i.particles.number.value?(i.fn.particlesDraw(),i.particles.move.enable?i.fn.drawAnimFrame=requestAnimFrame(i.fn.vendors.draw):cancelRequestAnimFrame(i.fn.drawAnimFrame)):i.tmp.img_error||(i.fn.drawAnimFrame=requestAnimFrame(i.fn.vendors.draw)):void 0!=i.tmp.img_obj?(i.fn.particlesDraw(),i.particles.move.enable?i.fn.drawAnimFrame=requestAnimFrame(i.fn.vendors.draw):cancelRequestAnimFrame(i.fn.drawAnimFrame)):i.tmp.img_error||(i.fn.drawAnimFrame=requestAnimFrame(i.fn.vendors.draw)):(i.fn.particlesDraw(),i.particles.move.enable?i.fn.drawAnimFrame=requestAnimFrame(i.fn.vendors.draw):cancelRequestAnimFrame(i.fn.drawAnimFrame))},i.fn.vendors.checkBeforeDraw=function(){"image"==i.particles.shape.type?"svg"==i.tmp.img_type&&void 0==i.tmp.source_svg?i.tmp.checkAnimFrame=requestAnimFrame(check):(cancelRequestAnimFrame(i.tmp.checkAnimFrame),i.tmp.img_error||(i.fn.vendors.init(),i.fn.vendors.draw())):(i.fn.vendors.init(),i.fn.vendors.draw())},i.fn.vendors.init=function(){i.fn.retinaInit(),i.fn.canvasInit(),i.fn.canvasSize(),i.fn.canvasPaint(),i.fn.particlesCreate(),i.fn.vendors.densityAutoParticles(),i.particles.line_linked.color_rgb_line=hexToRgb(i.particles.line_linked.color)},i.fn.vendors.start=function(){isInArray("image",i.particles.shape.type)?(i.tmp.img_type=i.particles.shape.image.src.substr(i.particles.shape.image.src.length-3),i.fn.vendors.loadImg(i.tmp.img_type)):i.fn.vendors.checkBeforeDraw()},i.fn.vendors.eventsListeners(),i.fn.vendors.start()};Object.deepExtend=function(e,a){for(var t in a)a[t]&&a[t].constructor&&a[t].constructor===Object?(e[t]=e[t]||{},arguments.callee(e[t],a[t])):e[t]=a[t];return e},window.requestAnimFrame=function(){return window.requestAnimationFrame||window.webkitRequestAnimationFrame||window.mozRequestAnimationFrame||window.oRequestAnimationFrame||window.msRequestAnimationFrame||function(e){window.setTimeout(e,1e3/60)}}(),window.cancelRequestAnimFrame=function(){return window.cancelAnimationFrame||window.webkitCancelRequestAnimationFrame||window.mozCancelRequestAnimationFrame||window.oCancelRequestAnimationFrame||window.msCancelRequestAnimationFrame||clearTimeout}(),window.pJSDom=[],window.particlesJS=function(e,a){"string"!=typeof e&&(a=e,e="particles-js"),e||(e="particles-js");var t=document.getElementById(e),i="particles-js-canvas-el",s=t.getElementsByClassName(i);if(s.length)for(;s.length>0;)t.removeChild(s[0]);var n=document.createElement("canvas");n.className=i,n.style.width="100%",n.style.height="100%";var r=document.getElementById(e).appendChild(n);null!=r&&pJSDom.push(new pJS(e,a))},window.particlesJS.load=function(e,a,t){var i=new XMLHttpRequest;i.open("GET",a),i.onreadystatechange=function(a){if(4==i.readyState)if(200==i.status){var s=JSON.parse(a.currentTarget.response);window.particlesJS(e,s),t&&t()}else console.log("Error pJS - XMLHttpRequest status: "+i.status),console.log("Error pJS - File config not found")},i.send()}; 12 | --------------------------------------------------------------------------------