quick rust website
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
/target
|
||||||
|
/result
|
||||||
1649
Cargo.lock
generated
Normal file
1649
Cargo.lock
generated
Normal file
File diff suppressed because it is too large
Load Diff
16
Cargo.toml
Normal file
16
Cargo.toml
Normal 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
35
README.md
Normal 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 form’s 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
96
flake.lock
generated
Normal 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
57
flake.nix
Normal 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
19
src/handlers.rs
Normal 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
59
src/lib.rs
Normal 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
26
src/main.rs
Normal 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
99
templates/_layout.css
Normal 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
16
templates/_layout.html
Normal 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
9
templates/homepage.html
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
{% extends "_layout.html" %}
|
||||||
|
|
||||||
|
{% block title %}Home Page{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container">
|
||||||
|
<p>Hello world!</p>
|
||||||
|
</div>
|
||||||
|
{% endblock %}
|
||||||
Reference in New Issue
Block a user