diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..bcce266 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,8 @@ +.github +.vscode +LICENSE +README.md +.gitignore +Dockerfile +node_modules +target diff --git a/.github/workflows/github-actions-demo.yml b/.github/workflows/github-actions-demo.yml new file mode 100644 index 0000000..3376f3f --- /dev/null +++ b/.github/workflows/github-actions-demo.yml @@ -0,0 +1,98 @@ +# .gitea/workflows/build.yaml + +name: Build Crate +run-name: Build + +on: [push] + +env: + CARGO_TERM_COLOR: always + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Check out repository code + # TODO -> Change to actions/checkout@v3 once this is resolved https://github.com/actions/checkout/issues/1370 + uses: https://gitea.com/ScMi1/checkout@v1 + - name: Get rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable + profile: minimal + components: rustfmt, clippy + override: true + - name: Add wasm32 to cargo + run: rustup target add wasm32-unknown-unknown + - name: Cache rust + uses: Swatinem/rust-cache@v2 + with: + cache-on-failure: "true" + - name: Install perseus + uses: baptiste0928/cargo-install@v2 + with: + crate: perseus-cli + - name: Install npm + uses: actions/setup-node@v3 + with: + node-version: latest + - name: Install sass + run: npm install -g sass + - name: Install tailwindcss via npm + run: npm install -D tailwindcss + - name: Compile css + run: npm run build + - name: Build the project + run: perseus deploy + - name: Download private docker registry cert + uses: nicklasfrahm/scp-action@v1.0.1 + with: + direction: download + host: ${{ secrets.REGISTRY_HOST }} + username: ${{ secrets.REGISTRY_HOST_USERNAME }} + insecure_password: ${{ secrets.REGISTRY_HOST_PASSWORD }} + source: ${{ secrets.REGISTRY_CRT_PATH }} + target: ca.crt + insecure_ignore_fingerprint: true + + - name: Add directory for docker registry cert + run: 'mkdir -p /etc/docker/certs.d/${{ secrets.REGISTRY_HOST }}' + + - name: Move private docker registry cert + run: 'mv ca.crt /etc/docker/certs.d/${{ secrets.REGISTRY_HOST }}' + + - name: Install docker + uses: papodaca/install-docker-action@main + - name: Set up QEMU + uses: docker/setup-qemu-action@v2 + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v2 + with: + config-inline: | + [registry."${{ secrets.REGISTRY_HOST }}"] + insecure = true + ca=["/etc/docker/certs.d/${{ secrets.REGISTRY_HOST }}/ca.crt"] + + - name: Login to private docker registry + uses: docker/login-action@v2 + with: + registry: ${{ secrets.REGISTRY_HOST }} + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD }} + + - name: Build and push + uses: docker/build-push-action@v4 + with: + context: . + push: true + tags: ${{ secrets.REGISTRY_HOST }}/test/test:${{ gitea.ref_name }} + + - name: Trigger deployment + uses: fjogeleit/http-request-action@v1 + with: + url: ${{ secrets.TRIGGER_DEPLOY_URL }} + method: POST + customHeaders: '{"Content-Type": "application/json"}' + data: '{"actionName": "deploy", "arguments": []}' + preventFailureOnNoResponse: 'true' diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4d0f0c7 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules +Cargo.lock +dist +target +static +pkg +./Cargo.lock diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..58e3479 --- /dev/null +++ b/Cargo.toml @@ -0,0 +1,22 @@ +[package] +resolver = "2" +name = "pool-elo" +version = "0.1.0" +edition = "2021" + +[dependencies] +perseus = { version = "0.4.2", features = [ "hydrate" ] } +sycamore = "0.8.2" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +env_logger = "0.10.0" +log = "0.4.20" + +[target.'cfg(engine)'.dev-dependencies] +fantoccini = "0.19" + +[target.'cfg(engine)'.dependencies] +tokio = { version = "1", features = [ "macros", "rt", "rt-multi-thread" ] } +perseus-axum = { version = "0.4.2", features = [ "dflt-server" ] } + +[target.'cfg(client)'.dependencies] diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..db6fb3b --- /dev/null +++ b/Dockerfile @@ -0,0 +1,14 @@ +FROM debian:stable-slim +WORKDIR /var/www/app + +COPY pkg server + +RUN addgroup --system server && \ + usermod -a -G server www-data && \ + chown -R www-data:server /var/www/app + +USER www-data + +EXPOSE 80 + +CMD ["./server/server"] diff --git a/README.md b/README.md index 0921259..4db8f72 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,53 @@ -# pool-elo +# Installing requirements -Pool ELO website written in rust \ No newline at end of file +## 1. Install rust: +### Windows: + +Download installer from https://www.rust-lang.org/tools/install + +### Unix based systems: + +Run `curl --proto '=https' --tlsv1.3 https://sh.rustup.rs -sSf | sh` + +## 2. Install npm + +### Windows: + +https://nodejs.org/en + +### Unix based systems: + +`sudo apt install nodejs` +`sudo apt install npm` + +## 3. Install Perseus, for real-time updates while developing + +`cargo install perseus-cli` +`cargo build --target wasm32-unknown-unknown` + +## 4. Install tailwindcss, for styling + +`npm install -D tailwindcss` + +Also take a look at + +Website: +https://framesurge.sh/perseus/en-US/ + +Simple tutorial: +https://blog.logrocket.com/building-rust-app-perseus/ + +# Building the project + +To build CSS run: +`npm run build` + +To build the project for testing, run +`perseus serve` + +# Deploying the project + +First run +`perseus deploy` + +The folder with everything necessary will be in `/pkg` diff --git a/package.json b/package.json new file mode 100644 index 0000000..66afa07 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "pool-elo", + "version": "1.0.0", + "description": "pool-elo", + "main": "index.js", + "scripts": { + "build": "npx tailwindcss -i ./style/tailwind.css -o ./static/style.css", + "watch": "npx tailwindcss -i ./style/tailwind.css -o ./static/style.css --watch" + }, + "keywords": [], + "author": "", + "license": "ISC", + "devDependencies": { + "tailwindcss": "^3.3.3" + } +} diff --git a/rust-toolchain.toml b/rust-toolchain.toml new file mode 100644 index 0000000..e918eb3 --- /dev/null +++ b/rust-toolchain.toml @@ -0,0 +1,3 @@ +[toolchain] +channel = "stable" +targets = ["wasm32-unknown-unknown"] diff --git a/src/components/layout.rs b/src/components/layout.rs new file mode 100644 index 0000000..d8550d8 --- /dev/null +++ b/src/components/layout.rs @@ -0,0 +1,30 @@ +use sycamore::prelude::*; + +#[component] +pub fn Layout<'a, G: Html>( + cx: Scope<'a>, + LayoutProps { title, children }: LayoutProps<'a, G>, +) -> View { + let children = children.call(cx); + + view! { cx, + // These elements are styled with bright colors for demonstration purposes + header(style = "background-color: red; color: white; padding: 1rem") { + p { (title.to_string()) } + } + main(style = "padding: 1rem") { + (children) + } + footer(style = "background-color: black; color: white; padding: 1rem") { + p { "Hey there, I'm a footer!" } + } + } +} + +#[derive(Prop)] +pub struct LayoutProps<'a, G: Html> { + /// The title of the page, which will be displayed in the header. + pub title: &'a str, + /// The content to put inside the layout. + pub children: Children<'a, G>, +} diff --git a/src/components/mod.rs b/src/components/mod.rs new file mode 100644 index 0000000..dd64619 --- /dev/null +++ b/src/components/mod.rs @@ -0,0 +1 @@ +pub mod layout; diff --git a/src/error_views.rs b/src/error_views.rs new file mode 100644 index 0000000..bf523a1 --- /dev/null +++ b/src/error_views.rs @@ -0,0 +1,62 @@ +use perseus::errors::ClientError; +use perseus::prelude::*; +use sycamore::prelude::*; + +pub fn get_error_views() -> ErrorViews { + ErrorViews::new(|cx, err, _err_info, _err_pos| { + match err { + ClientError::ServerError { status, message: _ } => match status { + 404 => ( + view! { cx, + title { "Page not found" } + }, + view! { cx, + p { "Sorry, that page doesn't seem to exist." } + }, + ), + // 4xx is a client error + _ if (400..500).contains(&status) => ( + view! { cx, + title { "Error" } + }, + view! { cx, + p { "There was something wrong with the last request, please try reloading the page." } + }, + ), + // 5xx is a server error + _ => ( + view! { cx, + title { "Error" } + }, + view! { cx, + p { "Sorry, our server experienced an internal error. Please try reloading the page." } + }, + ), + }, + ClientError::Panic(_) => ( + view! { cx, + title { "Critical error" } + }, + view! { cx, + p { "Sorry, but a critical internal error has occurred. This has been automatically reported to our team, who'll get on it as soon as possible. In the mean time, please try reloading the page." } + }, + ), + ClientError::FetchError(_) => ( + view! { cx, + title { "Error" } + }, + view! { cx, + p { "A network error occurred, do you have an internet connection? (If you do, try reloading the page.)" } + }, + ), + _ => ( + view! { cx, + title { "Error" } + }, + view! { cx, + p { (format!("An internal error has occurred: '{}'.", err)) } + }, + ), + } + }) +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..1046da3 --- /dev/null +++ b/src/main.rs @@ -0,0 +1,33 @@ +mod components; +mod templates; +mod error_views; + +use perseus::prelude::*; +use sycamore::prelude::view; + +#[perseus::main(perseus_axum::dflt_server)] +pub fn main() -> PerseusApp { + use log::{info, warn}; + env_logger::init(); + + PerseusApp::new() + .template(crate::templates::index::get_template()) + .template(crate::templates::long::get_template()) + .error_views(crate::error_views::get_error_views()) + .index_view(|cx| { + view! { cx, + html { + head { + meta(charset = "UTF-8") + meta(name = "viewport", content = "width=device-width, initial-scale=1.0") + // Perseus automatically resolves `/.perseus/static/` URLs to the contents of the `static/` directory at the project root + link(rel = "stylesheet", href = ".perseus/static/style.css") + } + body { + // Quirk: this creates a wrapper `
` around the root `
` by necessity + PerseusRoot() + } + } + } + }) +} diff --git a/src/templates/index.rs b/src/templates/index.rs new file mode 100644 index 0000000..9f2c7ec --- /dev/null +++ b/src/templates/index.rs @@ -0,0 +1,29 @@ +use crate::components::layout::Layout; +use perseus::prelude::*; +use sycamore::prelude::*; +use crate::templates::get_path; + +fn index_page(cx: Scope) -> View { + view! { cx, + Layout(title = "Index") { + // Anything we put in here will be rendered inside the `
` block of the layout + p { "Hello World!" } + br {} + a(href = "long") { "Long page" } + } + } +} + +#[engine_only_fn] +fn head(cx: Scope) -> View { + view! { cx, + title { "Index Page" } + } +} + +pub fn get_template() -> Template { + Template::build(get_path("").as_str()) + .view(index_page) + .head(head) + .build() +} diff --git a/src/templates/long.rs b/src/templates/long.rs new file mode 100644 index 0000000..a1d1c7d --- /dev/null +++ b/src/templates/long.rs @@ -0,0 +1,86 @@ +use crate::components::layout::Layout; +use perseus::prelude::*; +use serde::{Deserialize, Serialize}; +use std::fs; +use sycamore::prelude::*; +use crate::templates::get_path; + +// Reactive page + +#[derive(Serialize, Deserialize, Clone, ReactiveState)] +#[rx(alias = "PageStateRx")] +struct PageState { + ip: String, + text: String, +} + +fn request_state_page<'a, G: Html>(cx: BoundedScope<'_, 'a>, state: &'a PageStateRx) -> View { + view! { cx, + p { + ( + format!("Your IP address is {}.", state.ip.get()) + ) + } + p { + ( + state.text.get().repeat(5000) + ) + } + } +} + +#[engine_only_fn] +async fn get_request_state( + _info: StateGeneratorInfo<()>, + req: Request, +) -> Result> { + let text = fs::read_to_string("static/test.txt") + .unwrap_or("~".to_string()) + .parse() + .unwrap(); + + Ok(PageState { + ip: format!( + "{:?}", + req.headers() + // NOTE: This header can be trivially spoofed, and may well not be the user's actual + // IP address + .get("X-Forwarded-For") + .unwrap_or(&perseus::http::HeaderValue::from_str("hidden from view!").unwrap()) + ), + text, + }) +} + +// Regular page + +fn long_page(cx: Scope) -> View { + view! { cx, + Layout(title = "Long") { + // Anything we put in here will be rendered inside the `
` block of the layout + a(href = "") { "Index" } + br {} + p { + ("This is a test. ".repeat(5000)) + } + } + } +} + +#[engine_only_fn] +fn head(cx: Scope) -> View { + view! { cx, + title { "Long Page" } + } +} + +// Template + +pub fn get_template() -> Template { + // Template::build(get_full_path("long")).view(long_page).head(head).build() + Template::build(get_path("long").as_str()) + .request_state_fn(get_request_state) + .view_with_state(request_state_page) + .head(head) + .build() +} diff --git a/src/templates/mod.rs b/src/templates/mod.rs new file mode 100644 index 0000000..e61db68 --- /dev/null +++ b/src/templates/mod.rs @@ -0,0 +1,17 @@ +use std::env; + +pub mod index; +pub mod long; + +pub fn get_path(path: &str) -> String { + // Get base path + match env::var("PERSEUS_BASE_PATH") { + Ok(env_path) => { + // Strip the slash on both sides for directory consistency + // let stripped_env = env_path.trim_start_matches("/").trim_end_matches("/"); + // format!("{stripped_env}/{path}").to_string().trim_end_matches("/").to_owned() + path.to_owned() + } + Err(_) => path.to_owned(), + } +} diff --git a/tailwind.config.js b/tailwind.config.js new file mode 100644 index 0000000..596829f --- /dev/null +++ b/tailwind.config.js @@ -0,0 +1,14 @@ +module.exports = { + purge: { + mode: "all", + content: [ + "./src/**/*.rs", + "./index.html", + "./src/**/*.html", + "./src/**/*.css", + ], + }, + theme: {}, + variants: {}, + plugins: [], +};