From d608cc2d0a2c7dc5d921557e5ddfc4740660fe38 Mon Sep 17 00:00:00 2001
From: Paul Maruhn <paulmaruhn@posteo.de>
Date: Wed, 12 May 2021 01:39:11 +0200
Subject: [PATCH] wip and updates and stuff

---
 Cargo.lock                          | 636 +++++++++++++++++++++++++++-
 README.md                           |   2 +-
 ffddns-web/Cargo.toml               |   5 +
 ffddns-web/src/db.rs                |  32 ++
 ffddns-web/src/main.rs              | 141 +-----
 ffddns-web/src/web/dns.rs           |  68 +++
 ffddns-web/src/web/mod.rs           | 215 ++++++++++
 ffddns-web/templates/head.html      |   9 +
 ffddns-web/templates/index.html     |   7 +
 ffddns-web/templates/navbar.html    |  40 ++
 ffddns-web/templates/newdomain.html |  33 ++
 ffddns-web/templates/node.html      |  15 +
 ffddns-web/templates/nodelist.html  |  32 ++
 13 files changed, 1087 insertions(+), 148 deletions(-)
 create mode 100644 ffddns-web/src/web/dns.rs
 create mode 100644 ffddns-web/src/web/mod.rs
 create mode 100644 ffddns-web/templates/head.html
 create mode 100644 ffddns-web/templates/index.html
 create mode 100644 ffddns-web/templates/navbar.html
 create mode 100644 ffddns-web/templates/newdomain.html
 create mode 100644 ffddns-web/templates/node.html
 create mode 100644 ffddns-web/templates/nodelist.html

diff --git a/Cargo.lock b/Cargo.lock
index 09c1329..41452ab 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -1,5 +1,7 @@
 # This file is automatically @generated by Cargo.
 # It is not intended for manual editing.
+version = 3
+
 [[package]]
 name = "aead"
 version = "0.2.0"
@@ -55,6 +57,15 @@ dependencies = [
  "opaque-debug",
 ]
 
+[[package]]
+name = "aho-corasick"
+version = "0.7.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7404febffaa47dac81aa44dba71523c9d069b1bdc50a77db41195149e17f68e5"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "atty"
 version = "0.2.14"
@@ -63,7 +74,7 @@ checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
 dependencies = [
  "hermit-abi",
  "libc",
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -124,6 +135,15 @@ dependencies = [
  "byte-tools",
 ]
 
+[[package]]
+name = "bstr"
+version = "0.2.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a40b47ad93e1a5404e6c18dec46b628214fee441c70f4ab5d6942142cc268a3d"
+dependencies = [
+ "memchr",
+]
+
 [[package]]
 name = "byte-tools"
 version = "0.3.1"
@@ -158,7 +178,17 @@ dependencies = [
  "num-integer",
  "num-traits",
  "time",
- "winapi",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "chrono-tz"
+version = "0.5.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2554a3155fec064362507487171dcc4edc3df60cb10f3a1fb10ed8094822b120"
+dependencies = [
+ "chrono",
+ "parse-zoneinfo",
 ]
 
 [[package]]
@@ -177,6 +207,17 @@ dependencies = [
  "time",
 ]
 
+[[package]]
+name = "crossbeam-utils"
+version = "0.8.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e7e9d99fa91428effe99c5c6d4634cdeba32b8cf784fc428a2a687f61a952c49"
+dependencies = [
+ "autocfg",
+ "cfg-if 1.0.0",
+ "lazy_static",
+]
+
 [[package]]
 name = "crypto-mac"
 version = "0.7.0"
@@ -187,6 +228,12 @@ dependencies = [
  "subtle 1.0.0",
 ]
 
+[[package]]
+name = "deunicode"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "850878694b7933ca4c9569d30a34b55031b9b139ee1fc7b94a527c4ef960d690"
+
 [[package]]
 name = "devise"
 version = "0.2.0"
@@ -204,7 +251,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "066ceb7928ca93a9bedc6d0e612a8a0424048b0ab1f75971b203d01420c055d7"
 dependencies = [
  "devise_core",
- "quote",
+ "quote 0.6.13",
 ]
 
 [[package]]
@@ -214,9 +261,9 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cf41c59b22b5e3ec0ea55c7847e5f358d340f3a8d6d53a5cf4f1564967f96487"
 dependencies = [
  "bitflags",
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 0.4.30",
+ "quote 0.6.13",
+ "syn 0.15.44",
 ]
 
 [[package]]
@@ -251,11 +298,69 @@ name = "ffddns-web"
 version = "0.1.0"
 dependencies = [
  "chrono",
+ "log 0.4.14",
  "rand 0.8.3",
  "rocket",
+ "rocket_contrib",
  "rusqlite",
+ "serde",
+ "serde_json",
+ "tera",
 ]
 
+[[package]]
+name = "filetime"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1d34cfa13a63ae058bfa601fe9e313bbdb3746427c1459185464ce0fcf62e1e8"
+dependencies = [
+ "cfg-if 1.0.0",
+ "libc",
+ "redox_syscall",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "fnv"
+version = "1.0.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1"
+
+[[package]]
+name = "fsevent"
+version = "0.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5ab7d1bd1bd33cc98b0889831b72da23c0aa4df9cec7e0702f46ecea04b35db6"
+dependencies = [
+ "bitflags",
+ "fsevent-sys",
+]
+
+[[package]]
+name = "fsevent-sys"
+version = "2.0.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f41b048a94555da0f42f1d632e2e19510084fb8e303b0daa2816e733fb3644a0"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "fuchsia-zircon"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2e9763c69ebaae630ba35f74888db465e49e259ba1bc0eda7d06f4a067615d82"
+dependencies = [
+ "bitflags",
+ "fuchsia-zircon-sys",
+]
+
+[[package]]
+name = "fuchsia-zircon-sys"
+version = "0.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3dcaa9ae7725d12cdb85b3ad99a434db70b468c09ded17e012d86b5c1010f7a7"
+
 [[package]]
 name = "generic-array"
 version = "0.12.3"
@@ -302,6 +407,30 @@ version = "0.3.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
 
+[[package]]
+name = "globset"
+version = "0.4.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c152169ef1e421390738366d2f796655fec62621dabbd0fd476f905934061e4a"
+dependencies = [
+ "aho-corasick",
+ "bstr",
+ "fnv",
+ "log 0.4.14",
+ "regex",
+]
+
+[[package]]
+name = "globwalk"
+version = "0.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93e3af942408868f6934a7b85134a3230832b9977cf66125df2f9edcfce4ddcc"
+dependencies = [
+ "bitflags",
+ "ignore",
+ "walkdir",
+]
+
 [[package]]
 name = "hashbrown"
 version = "0.9.1"
@@ -343,6 +472,12 @@ version = "1.3.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "615caabe2c3160b313d52ccc905335f4ed5f10881dd63dc5699d47e90be85691"
 
+[[package]]
+name = "humansize"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6cab2627acfc432780848602f3f558f7e9dd427352224b0d9324025796d2a5e"
+
 [[package]]
 name = "hyper"
 version = "0.10.16"
@@ -373,6 +508,24 @@ dependencies = [
  "unicode-normalization",
 ]
 
+[[package]]
+name = "ignore"
+version = "0.4.17"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b287fb45c60bb826a0dc68ff08742b9d88a2fea13d6e0c286b3172065aaf878c"
+dependencies = [
+ "crossbeam-utils",
+ "globset",
+ "lazy_static",
+ "log 0.4.14",
+ "memchr",
+ "regex",
+ "same-file",
+ "thread_local",
+ "walkdir",
+ "winapi-util",
+]
+
 [[package]]
 name = "indexmap"
 version = "1.6.1"
@@ -383,18 +536,69 @@ dependencies = [
  "hashbrown",
 ]
 
+[[package]]
+name = "inotify"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4816c66d2c8ae673df83366c18341538f234a26d65a9ecea5c348b453ac1d02f"
+dependencies = [
+ "bitflags",
+ "inotify-sys",
+ "libc",
+]
+
+[[package]]
+name = "inotify-sys"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e05c02b5e89bff3b946cedeca278abc628fe811e604f027c45a8aa3cf793d0eb"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "iovec"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2b3ea6ff95e175473f8ffe6a7eb7c00d054240321b84c57051175fe3c1e075e"
+dependencies = [
+ "libc",
+]
+
 [[package]]
 name = "itoa"
 version = "0.4.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
 
+[[package]]
+name = "kernel32-sys"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7507624b29483431c0ba2d82aece8ca6cdba9382bff4ddd0f7490560c056098d"
+dependencies = [
+ "winapi 0.2.8",
+ "winapi-build",
+]
+
 [[package]]
 name = "language-tags"
 version = "0.2.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "a91d884b6667cd606bb5a69aa0c99ba811a115fc68915e7056ec08a46e93199a"
 
+[[package]]
+name = "lazy_static"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
+
+[[package]]
+name = "lazycell"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
+
 [[package]]
 name = "libc"
 version = "0.2.86"
@@ -444,6 +648,12 @@ dependencies = [
  "linked-hash-map",
 ]
 
+[[package]]
+name = "maplit"
+version = "1.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3e2e65a1a2e43cfcb47a895c4c8b10d1f4a61097f9f254f183aee60cad9c651d"
+
 [[package]]
 name = "matches"
 version = "0.1.8"
@@ -465,6 +675,78 @@ dependencies = [
  "log 0.3.9",
 ]
 
+[[package]]
+name = "mio"
+version = "0.6.23"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4afd66f5b91bf2a3bc13fad0e21caedac168ca4c707504e75585648ae80e4cc4"
+dependencies = [
+ "cfg-if 0.1.10",
+ "fuchsia-zircon",
+ "fuchsia-zircon-sys",
+ "iovec",
+ "kernel32-sys",
+ "libc",
+ "log 0.4.14",
+ "miow",
+ "net2",
+ "slab",
+ "winapi 0.2.8",
+]
+
+[[package]]
+name = "mio-extras"
+version = "2.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "52403fe290012ce777c4626790c8951324a2b9e3316b3143779c72b029742f19"
+dependencies = [
+ "lazycell",
+ "log 0.4.14",
+ "mio",
+ "slab",
+]
+
+[[package]]
+name = "miow"
+version = "0.2.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ebd808424166322d4a38da87083bfddd3ac4c131334ed55856112eb06d46944d"
+dependencies = [
+ "kernel32-sys",
+ "net2",
+ "winapi 0.2.8",
+ "ws2_32-sys",
+]
+
+[[package]]
+name = "net2"
+version = "0.2.37"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "391630d12b68002ae1e25e8f974306474966550ad82dac6886fb8910c19568ae"
+dependencies = [
+ "cfg-if 0.1.10",
+ "libc",
+ "winapi 0.3.9",
+]
+
+[[package]]
+name = "notify"
+version = "4.0.16"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2599080e87c9bd051ddb11b10074f4da7b1223298df65d4c2ec5bcf309af1533"
+dependencies = [
+ "bitflags",
+ "filetime",
+ "fsevent",
+ "fsevent-sys",
+ "inotify",
+ "libc",
+ "mio",
+ "mio-extras",
+ "walkdir",
+ "winapi 0.3.9",
+]
+
 [[package]]
 name = "num-integer"
 version = "0.1.44"
@@ -494,12 +776,27 @@ dependencies = [
  "libc",
 ]
 
+[[package]]
+name = "once_cell"
+version = "1.7.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3"
+
 [[package]]
 name = "opaque-debug"
 version = "0.2.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2839e79665f131bdb5782e51f2c6c9599c133c6098982a54c794358bf432529c"
 
+[[package]]
+name = "parse-zoneinfo"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41"
+dependencies = [
+ "regex",
+]
+
 [[package]]
 name = "pear"
 version = "0.1.4"
@@ -515,9 +812,9 @@ version = "0.1.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "bfc1c836fdc3d1ef87c348b237b5b5c4dff922156fb2d968f57734f9669768ca"
 dependencies = [
- "proc-macro2",
- "quote",
- "syn",
+ "proc-macro2 0.4.30",
+ "quote 0.6.13",
+ "syn 0.15.44",
  "version_check 0.9.2",
  "yansi",
 ]
@@ -534,6 +831,49 @@ version = "2.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
 
+[[package]]
+name = "pest"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "10f4872ae94d7b90ae48754df22fd42ad52ce740b8f370b03da4835417403e53"
+dependencies = [
+ "ucd-trie",
+]
+
+[[package]]
+name = "pest_derive"
+version = "2.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "833d1ae558dc601e9a60366421196a8d94bc0ac980476d0b67e1d0988d72b2d0"
+dependencies = [
+ "pest",
+ "pest_generator",
+]
+
+[[package]]
+name = "pest_generator"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "99b8db626e31e5b81787b9783425769681b347011cc59471e33ea46d2ea0cf55"
+dependencies = [
+ "pest",
+ "pest_meta",
+ "proc-macro2 1.0.24",
+ "quote 1.0.9",
+ "syn 1.0.61",
+]
+
+[[package]]
+name = "pest_meta"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "54be6e404f5317079812fc8f9f5279de376d8856929e21c184ecf6bbd692a11d"
+dependencies = [
+ "maplit",
+ "pest",
+ "sha-1",
+]
+
 [[package]]
 name = "pkg-config"
 version = "0.3.19"
@@ -562,7 +902,16 @@ version = "0.4.30"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "cf3d2011ab5c909338f7887f4fc896d35932e29146c12c8d01da6b22a80ba759"
 dependencies = [
- "unicode-xid",
+ "unicode-xid 0.1.0",
+]
+
+[[package]]
+name = "proc-macro2"
+version = "1.0.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1e0704ee1a7e00d7bb417d0770ea303c1bccbabf0ef1667dae92b5967f5f8a71"
+dependencies = [
+ "unicode-xid 0.2.1",
 ]
 
 [[package]]
@@ -571,7 +920,16 @@ version = "0.6.13"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6ce23b6b870e8f94f81fb0a363d65d86675884b34a09043c81e5562f11c1f8e1"
 dependencies = [
- "proc-macro2",
+ "proc-macro2 0.4.30",
+]
+
+[[package]]
+name = "quote"
+version = "1.0.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
+dependencies = [
+ "proc-macro2 1.0.24",
 ]
 
 [[package]]
@@ -655,6 +1013,33 @@ dependencies = [
  "rand_core 0.6.1",
 ]
 
+[[package]]
+name = "redox_syscall"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "742739e41cd49414de871ea5e549afb7e2a3ac77b589bcbebe8c82fab37147fc"
+dependencies = [
+ "bitflags",
+]
+
+[[package]]
+name = "regex"
+version = "1.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9251239e129e16308e70d853559389de218ac275b515068abc96829d05b948a"
+dependencies = [
+ "aho-corasick",
+ "memchr",
+ "regex-syntax",
+ "thread_local",
+]
+
+[[package]]
+name = "regex-syntax"
+version = "0.6.22"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b5eb417147ba9860a96cfe72a0b93bf88fee1744b5636ec99ab20c1aa9376581"
+
 [[package]]
 name = "rocket"
 version = "0.4.7"
@@ -685,12 +1070,25 @@ dependencies = [
  "devise",
  "glob",
  "indexmap",
- "quote",
+ "quote 0.6.13",
  "rocket_http",
  "version_check 0.9.2",
  "yansi",
 ]
 
+[[package]]
+name = "rocket_contrib"
+version = "0.4.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d7954a707f9ca18aa74ca8c1f5d1f900f52a4dceb68e96e3112143f759cfd20e"
+dependencies = [
+ "log 0.4.14",
+ "notify",
+ "rocket",
+ "serde",
+ "serde_json",
+]
+
 [[package]]
 name = "rocket_http"
 version = "0.4.7"
@@ -705,7 +1103,7 @@ dependencies = [
  "smallvec",
  "state",
  "time",
- "unicode-xid",
+ "unicode-xid 0.1.0",
 ]
 
 [[package]]
@@ -737,23 +1135,58 @@ version = "0.3.3"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ef703b7cb59335eae2eb93ceb664c0eb7ea6bf567079d843e09420219668e072"
 
+[[package]]
+name = "same-file"
+version = "1.0.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502"
+dependencies = [
+ "winapi-util",
+]
+
 [[package]]
 name = "serde"
-version = "1.0.123"
+version = "1.0.125"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "92d5161132722baa40d802cc70b15262b98258453e85e5d1d365c757c73869ae"
+checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171"
+dependencies = [
+ "serde_derive",
+]
+
+[[package]]
+name = "serde_derive"
+version = "1.0.125"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d"
+dependencies = [
+ "proc-macro2 1.0.24",
+ "quote 1.0.9",
+ "syn 1.0.61",
+]
 
 [[package]]
 name = "serde_json"
-version = "1.0.62"
+version = "1.0.64"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea1c6153794552ea7cf7cf63b1231a25de00ec90db326ba6264440fa08e31486"
+checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79"
 dependencies = [
  "itoa",
  "ryu",
  "serde",
 ]
 
+[[package]]
+name = "sha-1"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7d94d0bede923b3cea61f3f1ff57ff8cdfd77b400fb8f9998949e0cf04163df"
+dependencies = [
+ "block-buffer",
+ "digest",
+ "fake-simd",
+ "opaque-debug",
+]
+
 [[package]]
 name = "sha2"
 version = "0.8.2"
@@ -766,6 +1199,21 @@ dependencies = [
  "opaque-debug",
 ]
 
+[[package]]
+name = "slab"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527"
+
+[[package]]
+name = "slug"
+version = "0.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b3bc762e6a4b6c6fcaade73e77f9ebc6991b676f88bb2358bddb56560f073373"
+dependencies = [
+ "deunicode",
+]
+
 [[package]]
 name = "smallvec"
 version = "1.6.1"
@@ -796,9 +1244,51 @@ version = "0.15.44"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9ca4b3b69a77cbe1ffc9e198781b7acb0c7365a883670e8f1c1bc66fba79a5c5"
 dependencies = [
- "proc-macro2",
- "quote",
- "unicode-xid",
+ "proc-macro2 0.4.30",
+ "quote 0.6.13",
+ "unicode-xid 0.1.0",
+]
+
+[[package]]
+name = "syn"
+version = "1.0.61"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ed22b90a0e734a23a7610f4283ac9e5acfb96cbb30dfefa540d66f866f1c09c5"
+dependencies = [
+ "proc-macro2 1.0.24",
+ "quote 1.0.9",
+ "unicode-xid 0.2.1",
+]
+
+[[package]]
+name = "tera"
+version = "1.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "eac6ab7eacf40937241959d540670f06209c38ceadb62116999db4a950fbf8dc"
+dependencies = [
+ "chrono",
+ "chrono-tz",
+ "globwalk",
+ "humansize",
+ "lazy_static",
+ "percent-encoding 2.1.0",
+ "pest",
+ "pest_derive",
+ "rand 0.8.3",
+ "regex",
+ "serde",
+ "serde_json",
+ "slug",
+ "unic-segment",
+]
+
+[[package]]
+name = "thread_local"
+version = "1.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd"
+dependencies = [
+ "once_cell",
 ]
 
 [[package]]
@@ -809,7 +1299,7 @@ checksum = "6db9e6914ab8b1ae1c260a4ae7a49b6c5611b40328a735b21862567685e73255"
 dependencies = [
  "libc",
  "wasi 0.10.0+wasi-snapshot-preview1",
- "winapi",
+ "winapi 0.3.9",
 ]
 
 [[package]]
@@ -854,6 +1344,62 @@ version = "1.12.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "373c8a200f9e67a0c95e62a4f52fbf80c23b4381c05a17845531982fa99e6b33"
 
+[[package]]
+name = "ucd-trie"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56dee185309b50d1f11bfedef0fe6d036842e3fb77413abef29f8f8d1c5d4c1c"
+
+[[package]]
+name = "unic-char-property"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221"
+dependencies = [
+ "unic-char-range",
+]
+
+[[package]]
+name = "unic-char-range"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc"
+
+[[package]]
+name = "unic-common"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
+
+[[package]]
+name = "unic-segment"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23"
+dependencies = [
+ "unic-ucd-segment",
+]
+
+[[package]]
+name = "unic-ucd-segment"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700"
+dependencies = [
+ "unic-char-property",
+ "unic-char-range",
+ "unic-ucd-version",
+]
+
+[[package]]
+name = "unic-ucd-version"
+version = "0.9.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4"
+dependencies = [
+ "unic-common",
+]
+
 [[package]]
 name = "unicase"
 version = "1.4.2"
@@ -887,6 +1433,12 @@ version = "0.1.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "fc72304796d0818e357ead4e000d19c9c174ab23dc11093ac919054d20a6a7fc"
 
+[[package]]
+name = "unicode-xid"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f7fe0bb3479651439c9112f72b6c505038574c9fbb575ed1bf3b797fa39dd564"
+
 [[package]]
 name = "universal-hash"
 version = "0.3.0"
@@ -926,6 +1478,17 @@ version = "0.9.2"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "b5a972e5669d67ba988ce3dc826706fb0a8b01471c088cb0b6110b805cc36aed"
 
+[[package]]
+name = "walkdir"
+version = "2.3.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "777182bc735b6424e1a57516d35ed72cb8019d85c8c9bf536dccb3445c1a2f7d"
+dependencies = [
+ "same-file",
+ "winapi 0.3.9",
+ "winapi-util",
+]
+
 [[package]]
 name = "wasi"
 version = "0.9.0+wasi-snapshot-preview1"
@@ -938,6 +1501,12 @@ version = "0.10.0+wasi-snapshot-preview1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1a143597ca7c7793eff794def352d41792a93c481eb1042423ff7ff72ba2c31f"
 
+[[package]]
+name = "winapi"
+version = "0.2.8"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "167dc9d6949a9b857f3451275e911c3f44255842c1f7a76f33c55103a909087a"
+
 [[package]]
 name = "winapi"
 version = "0.3.9"
@@ -948,18 +1517,43 @@ dependencies = [
  "winapi-x86_64-pc-windows-gnu",
 ]
 
+[[package]]
+name = "winapi-build"
+version = "0.1.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "2d315eee3b34aca4797b2da6b13ed88266e6d612562a0c46390af8299fc699bc"
+
 [[package]]
 name = "winapi-i686-pc-windows-gnu"
 version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
 
+[[package]]
+name = "winapi-util"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "70ec6ce85bb158151cae5e5c87f95a8e97d2c0c4b001223f33a334e3ce5de178"
+dependencies = [
+ "winapi 0.3.9",
+]
+
 [[package]]
 name = "winapi-x86_64-pc-windows-gnu"
 version = "0.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
 
+[[package]]
+name = "ws2_32-sys"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d59cefebd0c892fa2dd6de581e937301d8552cb44489cdff035c6187cb63fa5e"
+dependencies = [
+ "winapi 0.2.8",
+ "winapi-build",
+]
+
 [[package]]
 name = "yansi"
 version = "0.5.0"
diff --git a/README.md b/README.md
index 605bc9c..c487aa3 100644
--- a/README.md
+++ b/README.md
@@ -1,4 +1,4 @@
-# FFDDNS
+# FFdynDNS
 
 Freifunk dynamic DNS Service
 
diff --git a/ffddns-web/Cargo.toml b/ffddns-web/Cargo.toml
index 50459fd..41e6fdd 100644
--- a/ffddns-web/Cargo.toml
+++ b/ffddns-web/Cargo.toml
@@ -8,6 +8,11 @@ edition = "2018"
 
 [dependencies]
 rocket = "0.4.7"
+rocket_contrib = "*"
 rusqlite = {version = "0.21", features = ["chrono", "serde_json"]}
 chrono = "0.4.19"
 rand = "0.8.3"
+tera = "1.6.1"
+log = "*"
+serde_json = "1.0.64"
+serde = {version = "1.0.125", features = ["derive"]}
diff --git a/ffddns-web/src/db.rs b/ffddns-web/src/db.rs
index 27c7f17..494d562 100644
--- a/ffddns-web/src/db.rs
+++ b/ffddns-web/src/db.rs
@@ -102,3 +102,35 @@ impl Domain {
 		}
 	}
 }
+
+impl Domain {
+	pub fn new_with_token(domain: String, token: String) -> Self {
+		Self {
+			domainname: domain,
+			token: token,
+			lastupdate: None,
+			ipv4: None,
+			ipv6: None
+		}
+	}
+
+	/// creates a new Domain object and generates a random token
+	pub fn new(domain: String) -> Self {
+		Self {
+			domainname: domain,
+			token: generate_token(),
+			lastupdate: None,
+			ipv4: None,
+			ipv6: None
+		}
+	}
+}
+
+
+pub fn generate_token() -> String {
+	let mut token = String::new();
+	for _ in 0..8 {
+		token.push_str(&format!("{:02x}", rand::random::<u8>()));
+	}
+	token
+}
diff --git a/ffddns-web/src/main.rs b/ffddns-web/src/main.rs
index b7f3c69..4435386 100644
--- a/ffddns-web/src/main.rs
+++ b/ffddns-web/src/main.rs
@@ -1,59 +1,25 @@
 #![feature(proc_macro_hygiene, decl_macro)]
 
 mod db;
-use chrono::{DateTime, Utc};
-use db::{
-    Database,
-    Domain,
-};
-use rocket::{
-    self,
-    get,
-    post,
-    routes,
-    State,
-};
-use rocket::request::{
-    Request,
-    FromRequest,
-    Outcome,
-};
+mod web;
+
+use chrono::DateTime;
+use chrono::Utc;
+use crate::db::Database;
+use crate::db::Domain;
+use rocket;
+use rocket::get;
+use rocket::post;
+use rocket::routes;
+use rocket::State;
+use rocket::request::Request;
+use rocket::request::FromRequest;
+use rocket::request::Outcome;
 use std::fmt::{self, Display};
 use std::net::IpAddr;
 use rand;
 
 
-pub struct ClientIp(IpAddr);
-
-
-impl ClientIp {
-    pub fn inner(&self) -> &IpAddr {
-        let ClientIp(ip) = self;
-        ip
-    }
-    pub fn into_inner(self) -> IpAddr {
-        let ClientIp(ip) = self;
-        ip
-    }
-}
-
-
-impl Display for ClientIp {
-    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
-        write!(f, "{}", self.inner().to_string())
-    }
-}
-
-
-impl<'a, 'r> FromRequest<'a, 'r> for ClientIp {
-    type Error = String;
-
-    fn from_request(request: &'a Request<'r>) -> Outcome<Self, Self::Error> {
-        let ip = request.client_ip().unwrap();
-        Outcome::Success(ClientIp(ip))
-    }
-}
-
 
 #[derive(Debug, Clone)]
 pub struct DomainUpdate {
@@ -62,84 +28,7 @@ pub struct DomainUpdate {
 }
 
 
-#[get("/update?<token>&<domain>&<ip>")]
-fn update(db: State<Database>, clientip: ClientIp, token: String, domain: String, ip: Option<String>) -> String {
-    let new_ip: IpAddr = {
-        if let Some(iip) = ip {
-            iip.parse::<IpAddr>().unwrap()
-        }
-        else {
-            clientip.into_inner()
-        }
-    };
-
-    let d = db.get_domain(&domain).unwrap();
-
-    if d.token != token {
-        return "not a valid token".to_string();
-    }
-
-    match new_ip {
-        IpAddr::V4(addr) => db.update_ipv4(&domain, addr),
-        IpAddr::V6(addr) => db.update_ipv6(&domain, addr),
-    }
-
-    db.update_lastupdate(&domain, Utc::now());
-
-    format!("{} updated to {:?}", domain, new_ip)
-}
-
-
-#[get("/create?<domain>")]
-fn create(db: State<Database>, domain: String) -> String {
-    let token = generate_token();
-    let d = Domain {
-        domainname: domain.clone(),
-        token: token.clone(),
-        lastupdate: None,
-        ipv4: None,
-        ipv6: None
-    };
-
-    db.insert_new_domain(&d);
-
-    format!("your token for {}: {}", domain, token)
-}
-
-
-#[get("/status?<domain>")]
-fn status(db: State<Database>, domain: String) -> String {
-    let domaininfo = match db.get_domain(&domain) {
-        None => return "domain not found".to_string(),
-        Some(r) => r,
-    };
-
-    format!("{:#?}", domaininfo)
-}
-
-
-
 fn main() {
     let db = db::Database::new("./ffddns.sqlite".into());
-
-    rocket::ignite()
-        .mount("/", routes![
-            update,
-            create,
-            status
-        ])
-        .manage(db)
-        .launch();
-}
-
-
-
-fn generate_token() -> String {
-    let mut token = String::new();
-
-    for _ in 0..8 {
-        token.push_str(&format!("{:02x}", rand::random::<u8>()));
-    }
-
-    token
+    web::start_web(db);
 }
diff --git a/ffddns-web/src/web/dns.rs b/ffddns-web/src/web/dns.rs
new file mode 100644
index 0000000..604bd1a
--- /dev/null
+++ b/ffddns-web/src/web/dns.rs
@@ -0,0 +1,68 @@
+use log::{error, info, debug};
+use tera::{self};
+
+use crate::db::Database;
+use crate::db::Domain;
+use chrono::DateTime;
+use chrono::Utc;
+use rand;
+use rocket;
+use rocket::get;
+use rocket::post;
+use rocket::request::FromRequest;
+use rocket::request::Outcome;
+use rocket::request::Request;
+use rocket::response::content;
+use rocket::response::content::Html;
+use rocket::routes;
+use rocket::State;
+use serde_json as json;
+use serde_json::json;
+use serde::{Serialize, Deserialize};
+use std::fmt::{self, Display};
+use std::net::IpAddr;
+use super::AppState;
+use rocket_contrib::json::Json;
+
+#[derive(Clone, Debug, Serialize)]
+pub enum QType {
+	A,
+	AAAA,
+	SOA,
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct DnsResponse {
+	result: Vec<DnsRecord>
+}
+
+#[derive(Clone, Debug, Serialize)]
+pub struct DnsRecord {
+	// AAAA
+	qtype: QType,
+	// www.example.com
+	qname: String,
+	// 203.0.113.2
+	content: String,
+	// 60
+	ttl: usize
+}
+
+#[get("/lookup/<domain>/<record>")]
+pub fn lookup(state: State<AppState>, domain: String, record: String) -> Json<DnsResponse> {
+	info!("{:?} {:?}", record, domain);
+	let res = DnsResponse {
+		result: vec![
+			DnsRecord {
+				qtype: QType::A,
+				qname: "ffhl.de.".to_string(),
+				content: "1.1.1.1".to_string(),
+				ttl: 60
+			}
+		]
+	};
+
+	info!("{:#?}", res);
+
+	Json(res)
+}
diff --git a/ffddns-web/src/web/mod.rs b/ffddns-web/src/web/mod.rs
new file mode 100644
index 0000000..545998c
--- /dev/null
+++ b/ffddns-web/src/web/mod.rs
@@ -0,0 +1,215 @@
+mod dns;
+
+use log::{error, info};
+use tera::{self};
+
+use crate::db::{self, Database, Domain};
+use chrono::DateTime;
+use chrono::Utc;
+use rand;
+use rocket;
+use rocket::get;
+use rocket::post;
+use rocket::request::FromRequest;
+use rocket::request::Outcome;
+use rocket::request::Request;
+use rocket::response::content;
+use rocket::response::content::Html;
+use rocket::routes;
+use rocket::State;
+use serde_json as json;
+use serde_json::json;
+use std::fmt::{self, Display};
+use std::net::IpAddr;
+use tera::Tera;
+
+const TEMPLATES: &[(&str, &str)] = &[
+	("index", include_str!("../../templates/index.html")),
+	("nodelist", include_str!("../../templates/nodelist.html")),
+	("head", include_str!("../../templates/head.html")),
+	("node", include_str!("../../templates/node.html")),
+	("navbar", include_str!("../../templates/navbar.html")),
+	("newdomain", include_str!("../../templates/newdomain.html")),
+];
+
+pub struct AppState {
+	templates: Tera,
+	db: Database,
+}
+
+pub struct ClientIp(IpAddr);
+
+impl ClientIp {
+	pub fn inner(&self) -> &IpAddr {
+		let ClientIp(ip) = self;
+		ip
+	}
+
+	pub fn into_inner(self) -> IpAddr {
+		let ClientIp(ip) = self;
+		ip
+	}
+}
+
+impl Display for ClientIp {
+	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+		write!(f, "{}", self.inner().to_string())
+	}
+}
+
+impl<'a, 'r> FromRequest<'a, 'r> for ClientIp {
+	type Error = String;
+
+	fn from_request(request: &'a Request<'r>) -> Outcome<Self, Self::Error> {
+		let ip = request.client_ip().unwrap();
+		Outcome::Success(ClientIp(ip))
+	}
+}
+
+#[get("/update?<token>&<domain>&<ip>")]
+fn update(
+	db: State<Database>,
+	clientip: ClientIp,
+	token: String,
+	domain: String,
+	ip: Option<String>,
+) -> String {
+	let new_ip: IpAddr = {
+		if let Some(iip) = ip {
+			iip.parse::<IpAddr>().unwrap()
+		} else {
+			clientip.into_inner()
+		}
+	};
+
+	let d = db.get_domain(&domain).unwrap();
+
+	if d.token != token {
+		return "not a valid token".to_string();
+	}
+
+	match new_ip {
+		IpAddr::V4(addr) => db.update_ipv4(&domain, addr),
+		IpAddr::V6(addr) => db.update_ipv6(&domain, addr),
+	}
+
+	db.update_lastupdate(&domain, Utc::now());
+
+	format!("{} updated to {:?}", domain, new_ip)
+}
+
+#[get("/create?<domain>")]
+fn create(db: State<Database>, domain: String) -> String {
+	let token = db::generate_token();
+	let d = Domain {
+		domainname: domain.clone(),
+		token: token.clone(),
+		lastupdate: None,
+		ipv4: None,
+		ipv6: None,
+	};
+
+	db.insert_new_domain(&d);
+
+	format!("your token for {}: {}", domain, token)
+}
+
+#[get("/status?<domain>")]
+fn status(db: State<Database>, domain: String) -> String {
+	let domaininfo = match db.get_domain(&domain) {
+		None => return "domain not found".to_string(),
+		Some(r) => r,
+	};
+
+	format!("{:#?}", domaininfo)
+}
+
+#[get("/")]
+fn index(state: State<'_, AppState>) -> Html<String> {
+	let html = state
+		.templates
+		.render("index", &tera::Context::from_serialize(&json!({})).unwrap())
+		.unwrap();
+
+	Html(html)
+}
+
+#[get("/newdomain?<domainname>")]
+fn newdomain(state: State<'_, AppState>, domainname: Option<String>) -> Html<String> {
+	let db = &state.db;
+	let mut template_data: json::Value = json!({});
+
+	match &domainname {
+		Some(name) if db.get_domain(&name).is_some() => {
+			template_data = json!({
+				"form_request": true,
+				"created": false,
+				"error": true,
+				"error_msg": "Domain already exists"
+			})
+		}
+		Some(name) if db.get_domain(&name).is_none() => {
+			let domain = db::Domain::new(name.clone());
+			db.insert_new_domain(&domain);
+
+			template_data = json!({
+				"form_request": true,
+				"created": true,
+				"error": false,
+				"token": domain.token
+			});
+		}
+		None | _ => {
+			template_data = json!({
+				"form_request": false
+			})
+		}
+	}
+
+
+
+	let html = state
+		.templates
+		.render(
+			"newdomain",
+			&tera::Context::from_serialize(&template_data).unwrap()
+		).unwrap();
+
+	info!("{:#?}", domainname);
+
+	Html(html)
+}
+
+pub fn start_web(db: Database) {
+	let appstate = AppState {
+		db: db,
+		templates: load_templates(),
+	};
+
+	rocket::custom(rocket::config::ConfigBuilder::new(rocket::config::Environment::Development)
+		.port(8053)
+		.finalize()
+		.unwrap()
+	)
+		.mount("/", routes![index, update, create, status, newdomain])
+		.mount("/dns", routes![dns::lookup])
+		.manage(appstate)
+		.launch();
+}
+
+fn load_templates() -> tera::Tera {
+	let mut t = tera::Tera::default();
+
+	for (name, template) in TEMPLATES {
+		if let Err(e) = t.add_raw_template(name, template) {
+			error!("failed to load template: {}", name);
+			match &e.kind {
+				tera::ErrorKind::Msg(m) => error!("{}", m),
+				_ => error!("unknown error"),
+			}
+			panic!("loading templates failed: {:#?}", e);
+		}
+	}
+
+	t
+}
diff --git a/ffddns-web/templates/head.html b/ffddns-web/templates/head.html
new file mode 100644
index 0000000..0ddf38e
--- /dev/null
+++ b/ffddns-web/templates/head.html
@@ -0,0 +1,9 @@
+<!doctype html>
+<head>
+	<Title>{{ site.title | default (value="FFdynDNS") }} </Title>
+	<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/css/bootstrap.min.css" rel="stylesheet"
+		integrity="sha384-BmbxuPwQa2lc/FVzBcNJ7UAyJxM6wuqIj61tLrc4wSX0szH/Ev+nYRRuWlolflfl" crossorigin="anonymous">
+	<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.0.0-beta2/dist/js/bootstrap.bundle.min.js"
+		integrity="sha384-b5kHyXgcpbZJO/tY9Ul7kGkf1S0CWuKcCD38l8YkeH8z8QjE0GmW1gYU5S9FOnJ0"
+		crossorigin="anonymous"></script>
+</head>
diff --git a/ffddns-web/templates/index.html b/ffddns-web/templates/index.html
new file mode 100644
index 0000000..d89caef
--- /dev/null
+++ b/ffddns-web/templates/index.html
@@ -0,0 +1,7 @@
+{% include "head" %}
+<body>
+	{% include "navbar" %}
+	<div class="container">
+		<p>Hello Freifunk!</p>
+	</div>
+</body>
diff --git a/ffddns-web/templates/navbar.html b/ffddns-web/templates/navbar.html
new file mode 100644
index 0000000..342ff82
--- /dev/null
+++ b/ffddns-web/templates/navbar.html
@@ -0,0 +1,40 @@
+<nav class="navbar navbar-expand-lg navbar-light bg-light">
+	<div class="container-fluid">
+		<a class="navbar-brand" href="/">FFdynDNS</a>
+		<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
+			aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
+			<span class="navbar-toggler-icon"></span>
+		</button>
+		<div class="collapse navbar-collapse" id="navbarSupportedContent">
+			<ul class="navbar-nav me-auto mb-2 mb-lg-0">
+				<li class="nav-item">
+					<a class="nav-link active" aria-current="page" href="/newdomain">New Domain</a>
+				</li>
+				<li class="nav-item">
+					<a class="nav-link" href="#">Link</a>
+				</li>
+				<li class="nav-item dropdown">
+					<a class="nav-link dropdown-toggle" href="#" id="navbarDropdown" role="button"
+						data-bs-toggle="dropdown" aria-expanded="false">
+						Dropdown
+					</a>
+					<ul class="dropdown-menu" aria-labelledby="navbarDropdown">
+						<li><a class="dropdown-item" href="#">Action</a></li>
+						<li><a class="dropdown-item" href="#">Another action</a></li>
+						<li>
+							<hr class="dropdown-divider">
+						</li>
+						<li><a class="dropdown-item" href="#">Something else here</a></li>
+					</ul>
+				</li>
+				<li class="nav-item">
+					<a class="nav-link disabled" href="#" tabindex="-1" aria-disabled="true">Disabled</a>
+				</li>
+			</ul>
+			<form class="d-flex">
+				<input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">
+				<button class="btn btn-outline-success" type="submit">Search</button>
+			</form>
+		</div>
+	</div>
+</nav>
diff --git a/ffddns-web/templates/newdomain.html b/ffddns-web/templates/newdomain.html
new file mode 100644
index 0000000..2e49990
--- /dev/null
+++ b/ffddns-web/templates/newdomain.html
@@ -0,0 +1,33 @@
+{% include "head" %}
+
+<body>
+	{% include "navbar" %}
+	<div class="container">
+		<h1>Create a new Domain</h1>
+		{% if error %}
+			<div class="alert alert-danger" role="alert">
+				{{ error_msg }}
+			</div>
+		{% endif %}
+		{% if not form_request %}
+			<form action="/newdomain" method="GET">
+				<div class="mb-3">
+					<label for="domainname" class="form-label">Domain name</label>
+					<input type="text" class="form-control" id="domainname" name="domainname" required aria-describedby="domainnameHelp">
+					<div id="domainnameHelp" class="form-text">Your domain name</div>
+				</div>
+				<div class="mb-3 form-check">
+					<input type="checkbox" class="form-check-input" id="exampleCheck1">
+					<label class="form-check-label" for="exampleCheck1">Check me out</label>
+				</div>
+				<button type="submit" class="btn btn-primary">Submit</button>
+			</form>
+		{% endif %}
+		{% if form_request and created %}
+			<div class="alert alert-success" role="alert">
+				Your domain was successfully created! <br>
+				Your token is: <span class="badge bg-light text-dark">{{ token }}</span>
+			</div>
+		{% endif %}
+	</div>
+</body>
diff --git a/ffddns-web/templates/node.html b/ffddns-web/templates/node.html
new file mode 100644
index 0000000..eb68a43
--- /dev/null
+++ b/ffddns-web/templates/node.html
@@ -0,0 +1,15 @@
+{% include "head" %}
+
+<body>
+	{% include "navbar" %}
+	<div class="container">
+
+	<h1>{{ node.last_response.nodeinfo.hostname }}</h1>
+	node is {{ node.status }} <br>
+	last response {{ last_response_secs }} ago <br>
+
+	<textarea readonly style="width: 90%; height: 1200px; font-family: monospace;">
+{{ last_response }}
+	</textarea>
+	</div>
+</body>
diff --git a/ffddns-web/templates/nodelist.html b/ffddns-web/templates/nodelist.html
new file mode 100644
index 0000000..a8b83e5
--- /dev/null
+++ b/ffddns-web/templates/nodelist.html
@@ -0,0 +1,32 @@
+{% include "head" %}
+<body>
+	{% include "navbar" %}
+	<div class="container">
+
+		<h1 id="content" class="bd-title">Nodelist</h1>
+		<table class="table table-striped">
+			<thead>
+				<tr>
+					<th scope="col">Hostname</th>
+					<th scope="col">Status</th>
+					<th scope="col">last response</th>
+					<th scope="col">Address</th>
+				</tr>
+			</thead>
+			{% for node in nodes %}
+			<tr>
+				<th><a href="/node/{{ node.nodeid }}">{{ node.last_response.nodeinfo.hostname }}</a></th>
+				<th>
+					{% if node.status == "Up" %}
+					<span class="badge bg-success">Up</span>
+					{% else %}
+					<span class="badge bg-danger">Down</span>
+					{% endif %}
+				</th>
+				<th>{{ node.last_seen_secs }}s ago</th>
+				<th><a href="http://[{{ node.last_address }}]">{{ node.last_address }}</a></th>
+			</tr>
+			{% endfor %}
+		</table>
+	</div>
+</body>
-- 
GitLab