quick rust website

This commit is contained in:
2025-12-08 20:54:02 +01:00
commit b0a5e51607
12 changed files with 2083 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@@ -0,0 +1,2 @@
/target
/result

1649
Cargo.lock generated Normal file

File diff suppressed because it is too large Load Diff

16
Cargo.toml Normal file
View File

@@ -0,0 +1,16 @@
[package]
name = "mycoolwebsite"
version = "0.1.1"
edition = "2024"
[dependencies]
axum = "0.8.7"
tokio = { version = "1.48.0", features = ["macros", "rt-multi-thread"] }
tracing = "0.1.43"
tracing-subscriber = "0.3.22"
tower-http = { version = "0.6.6", features = ["cors", "limit"] }
tower_governor = "0.8.0"
anyhow = "1.0.100"
chrono = { version = "0.4.42", features = ["serde"] }
askama = "0.14.0"
clap = { version = "4.5.53", features = ["derive"] }

35
README.md Normal file
View File

@@ -0,0 +1,35 @@
# Form Generator
A lightweight application for generating dynamic form websites and storing user responses. The form fields, labels, and behavior are fully configurable via a TOML configuration file.
## Usage
The server will listen on `127.0.0.1:8081` by default. You can override with:
```bash
SERVER_PORT=8082 form-generator
```
## Configuration
The application is configured via a TOML file (default: `config.toml`).
See [config.toml](./config.toml) for an example.
### Configuration Fields
* `json_output`: Path to the JSON file where responses are stored.
* `submit_button`: Text displayed on the forms submit button.
* `fields`: List of form fields
* `name`: Internal key for storage.
* `title`: Label displayed in the form.
* `description`: Short description displayed below the field.
* `answer_type`: Type of input (`text`, `number`, `email`, `password`, `url`, `tel`, `textarea`).
* `html_before` (optional): HTML snippet rendered before the field.
* `html_after` (optional): HTML snippet rendered after the field.
## Lib
This crate also provides a library so you can run the form server alongside another app.

96
flake.lock generated Normal file
View File

@@ -0,0 +1,96 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1764950072,
"narHash": "sha256-BmPWzogsG2GsXZtlT+MTcAWeDK5hkbGRZTeZNW42fwA=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "f61125a668a320878494449750330ca58b78c557",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1744536153,
"narHash": "sha256-awS2zRgF4uTwrOKwwiJcByDzDOdo3Q1rPZbiHQg/N38=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "18dd725c29603f582cf1900e0d25f9f1063dbf11",
"type": "github"
},
"original": {
"owner": "NixOS",
"ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1765161692,
"narHash": "sha256-XdY9AFzmgRPYIhP4N+WiCHMNxPoifP5/Ld+orMYBD8c=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "7ed7e8c74be95906275805db68201e74e9904f07",
"type": "github"
},
"original": {
"owner": "oxalica",
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",
"version": 7
}

57
flake.nix Normal file
View File

@@ -0,0 +1,57 @@
{
description = "My cool website";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
rust-overlay.url = "github:oxalica/rust-overlay";
};
outputs = { self, nixpkgs, flake-utils, rust-overlay, ... }:
flake-utils.lib.eachDefaultSystem (system:
let
overlays = [ rust-overlay.overlays.default ];
pkgs = import nixpkgs {
inherit system overlays;
};
rust = pkgs.rust-bin.stable.latest.default;
openssl = pkgs.openssl;
in
{
packages.default = pkgs.rustPlatform.buildRustPackage {
pname = "mycoolwebsite";
version = "0.1.1";
src = ./.;
cargoLock = {
lockFile = ./Cargo.lock;
};
nativeBuildInputs = [
rust
pkgs.pkg-config
];
buildInputs = [
openssl
];
OPENSSL_LIB_DIR = "${openssl.out}/lib";
OPENSSL_INCLUDE_DIR = "${openssl.dev}/include";
PKG_CONFIG_PATH = "${openssl.dev}/lib/pkgconfig";
};
devShells.default = pkgs.mkShell {
packages = [
rust
pkgs.cargo
pkgs.rust-analyzer
pkgs.pkg-config
openssl
];
OPENSSL_DIR = openssl.dev;
PKG_CONFIG_PATH = "${openssl.dev}/lib/pkgconfig";
};
});
}

19
src/handlers.rs Normal file
View File

@@ -0,0 +1,19 @@
use askama::Template;
use axum::{
http::StatusCode,
response::{Html, IntoResponse},
};
pub async fn render_homepage() -> impl IntoResponse {
#[derive(Template)]
#[template(path = "homepage.html")]
struct HomePageTemplate<'a> {
lang: &'a str,
}
let tmpl = HomePageTemplate { lang: "en" };
match tmpl.render() {
Ok(html) => Html(html).into_response(),
Err(_) => (StatusCode::INTERNAL_SERVER_ERROR, "Template render error").into_response(),
}
}

59
src/lib.rs Normal file
View File

@@ -0,0 +1,59 @@
use std::{net::SocketAddr, time::Duration};
use axum::{
Router,
http::{Method, header},
routing::get,
};
use tower_governor::{GovernorLayer, governor::GovernorConfigBuilder};
use tower_http::cors::{Any, CorsLayer};
use crate::handlers::render_homepage;
pub mod handlers;
/// Start the server with the given configuration and output file.
/// `addr` should be something like "127.0.0.1:8081"
pub async fn run_server(addr: &str, verbose: bool) -> anyhow::Result<()> {
// CORS
let cors = CorsLayer::new()
.allow_origin(Any)
.allow_methods([Method::GET, Method::POST])
.allow_headers([header::AUTHORIZATION, header::CONTENT_TYPE]);
// rate limiter
let governor_conf = GovernorConfigBuilder::default()
.per_second(3)
.burst_size(10)
.finish()
.unwrap();
// a separate background task to clean up
let governor_limiter = governor_conf.limiter().clone();
let interval = Duration::from_secs(60);
std::thread::spawn(move || {
loop {
std::thread::sleep(interval);
if verbose {
tracing::info!("rate limiting storage size: {}", governor_limiter.len());
}
governor_limiter.retain_recent();
}
});
let app = Router::new()
.route("/", get(render_homepage))
.layer(cors)
.layer(GovernorLayer::new(governor_conf));
let listener = tokio::net::TcpListener::bind(addr).await?;
tracing::info!("Listening on {}", addr);
axum::serve(
listener,
app.into_make_service_with_connect_info::<SocketAddr>(),
)
.await?;
Ok(())
}

26
src/main.rs Normal file
View File

@@ -0,0 +1,26 @@
use clap::Parser;
use mycoolwebsite::run_server;
#[derive(Parser, Debug)]
#[command(author, version, about, long_about = None)]
struct Cli {
#[arg(short, long)]
verbose: bool,
}
/// Cli handler
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
let subscriber = tracing_subscriber::FmtSubscriber::new();
tracing::subscriber::set_global_default(subscriber).ok();
let port = std::env::var("SERVER_PORT").unwrap_or("8081".to_string());
let addr = format!("127.0.0.1:{port}");
run_server(&addr, cli.verbose).await?;
Ok(())
}

99
templates/_layout.css Normal file
View File

@@ -0,0 +1,99 @@
html {
font-size: 15px;
}
body {
font-family: 'Inter', system-ui, -apple-system, Roboto, "Segoe UI", Arial, sans-serif;
padding: 20px;
margin: 0;
display: flex;
align-items: center;
justify-content: center;
background-color: #fff;
overflow-x: hidden;
}
h2 {
font-size: 1.8rem;
margin-bottom: 15px;
}
.container {
display: flex;
flex-direction: row;
text-align: left;
justify-content: center;
align-items: center;
background: #fff;
border-radius: 16px;
padding: 2rem;
max-width: 720px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.25);
}
img {
display: block;
margin: 0 auto;
}
label {
font-weight: 600;
display: block;
margin-bottom: 0.25rem;
font-size: 1.35rem;
color: #222;
}
input,
textarea,
select {
width: 100%;
padding: 0.6rem 0.75rem;
margin-bottom: 1rem;
border: none;
border-radius: 8px;
box-sizing: border-box;
font-size: 1rem;
color: #333333aa;
background-color: #e0f0ff;
border: 1px solid #88c1ff;
transition: all 0.2s ease-in-out;
}
input:focus,
textarea:focus,
select:focus {
outline: #66aaffaa;
background-color: #cce5ff;
}
textarea {
resize: none;
}
button {
padding: 0.5rem 1rem;
font-size: 1.3rem;
border: none;
border-radius: 0.25rem;
cursor: pointer;
background-color: #66aaff;
color: #fff;
transition: background-color 0.2s;
transition: color 0.2s;
}
button:hover {
background-color: #3390ff;
}
@media (max-width: 720px) {
html {
font-size: 20px;
}
.container {
max-width: 100%;
width: 100%;
}
}

16
templates/_layout.html Normal file
View File

@@ -0,0 +1,16 @@
<!DOCTYPE html>
<html lang="{{ lang }}">
<head>
<meta charset="utf-8"/>
<title>{% block title %}Dynamic Form{% endblock %}</title>
<style>
/*<![CDATA[*/
{%~ include "_layout.css" ~%}
/*]]>*/
</style>
</head>
<body>
{% block content %}{% endblock %}
</body>
</html>

9
templates/homepage.html Normal file
View File

@@ -0,0 +1,9 @@
{% extends "_layout.html" %}
{% block title %}Home Page{% endblock %}
{% block content %}
<div class="container">
<p>Hello world!</p>
</div>
{% endblock %}