summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Cargo.lock669
-rw-r--r--Cargo.toml7
-rw-r--r--src/api/liveops.rs4
-rw-r--r--src/api/ops.rs20
-rw-r--r--src/api/users.rs24
-rw-r--r--src/main.rs19
-rw-r--r--src/resources/languages.rs67
-rw-r--r--src/resources/mod.rs4
-rw-r--r--src/resources/scripts.rs37
-rw-r--r--src/resources/style.rs53
-rw-r--r--src/resources/templates.rs59
-rw-r--r--static/languages/en.ini6
-rw-r--r--static/scripts/main.js0
-rw-r--r--static/style/style.scss0
-rw-r--r--static/templates/base.html16
-rw-r--r--static/templates/login.html11
16 files changed, 968 insertions, 28 deletions
diff --git a/Cargo.lock b/Cargo.lock
index e7c39aa..ffe67bd 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -237,6 +237,24 @@ dependencies = [
]
[[package]]
+name = "android_system_properties"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "ansi_term"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d52a9bb7ec0cf484c551830a7ce27bd20d67eac647e1befb56b0be4ee39a55d2"
+dependencies = [
+ "winapi",
+]
+
+[[package]]
name = "arrayref"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -258,6 +276,17 @@ dependencies = [
]
[[package]]
+name = "atty"
+version = "0.2.14"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d9b39be18770d11421cdb1b9947a45dd3f37e93092cbf377614828a319d5fee8"
+dependencies = [
+ "hermit-abi 0.1.19",
+ "libc",
+ "winapi",
+]
+
+[[package]]
name = "autocfg"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -329,6 +358,16 @@ dependencies = [
]
[[package]]
+name = "bstr"
+version = "1.4.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c3d4260bcc2e8fc9df1eac4919a720effeb63a3f0952f5bf4944adfa18897f09"
+dependencies = [
+ "memchr",
+ "serde",
+]
+
+[[package]]
name = "bumpalo"
version = "3.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -371,6 +410,61 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
+name = "chrono"
+version = "0.4.24"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4e3c5919066adf22df73762e50cffcde3a758f2a848b113b586d1f86728b673b"
+dependencies = [
+ "iana-time-zone",
+ "num-integer",
+ "num-traits",
+ "winapi",
+]
+
+[[package]]
+name = "chrono-tz"
+version = "0.6.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58549f1842da3080ce63002102d5bc954c7bc843d4f47818e642abdc36253552"
+dependencies = [
+ "chrono",
+ "chrono-tz-build",
+ "phf",
+]
+
+[[package]]
+name = "chrono-tz-build"
+version = "0.0.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "db058d493fb2f65f41861bfed7e3fe6335264a9f0f92710cab5bdf01fef09069"
+dependencies = [
+ "parse-zoneinfo",
+ "phf",
+ "phf_codegen",
+]
+
+[[package]]
+name = "clap"
+version = "2.34.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a0610544180c38b88101fecf2dd634b174a62eef6946f84dfc6a7127512b381c"
+dependencies = [
+ "ansi_term",
+ "atty",
+ "bitflags",
+ "strsim",
+ "textwrap",
+ "unicode-width",
+ "vec_map",
+]
+
+[[package]]
+name = "codemap"
+version = "0.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b9e769b5c8c8283982a987c6e948e540254f1058d5a74b8794914d4ef5fc2a24"
+
+[[package]]
name = "const-oid"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -406,6 +500,12 @@ dependencies = [
]
[[package]]
+name = "core-foundation-sys"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
+
+[[package]]
name = "cpufeatures"
version = "0.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -502,6 +602,12 @@ dependencies = [
]
[[package]]
+name = "deunicode"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "850878694b7933ca4c9569d30a34b55031b9b139ee1fc7b94a527c4ef960d690"
+
+[[package]]
name = "digest"
version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -512,6 +618,23 @@ dependencies = [
]
[[package]]
+name = "displaydoc"
+version = "0.2.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "487585f4d0c6655fe74905e2504d8ad6908e4db67f744eb140876906c2f3175d"
+dependencies = [
+ "proc-macro2",
+ "quote",
+ "syn 2.0.2",
+]
+
+[[package]]
+name = "dlv-list"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257"
+
+[[package]]
name = "dotenv"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -652,6 +775,54 @@ dependencies = [
]
[[package]]
+name = "globset"
+version = "0.4.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc"
+dependencies = [
+ "aho-corasick",
+ "bstr",
+ "fnv",
+ "log",
+ "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 = "grass"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "85cc4b64880a2264a41f9eab431780e72a68a6c88b9bddef361ba638812d572e"
+dependencies = [
+ "clap",
+ "grass_compiler",
+]
+
+[[package]]
+name = "grass_compiler"
+version = "0.12.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8e4feeef87d958eebd4d55431040768b93a5b088202198e0b203adc3c1d468c6"
+dependencies = [
+ "codemap",
+ "indexmap",
+ "lasso",
+ "once_cell",
+ "phf",
+ "rand",
+]
+
+[[package]]
name = "h2"
version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -672,6 +843,15 @@ dependencies = [
[[package]]
name = "hashbrown"
+version = "0.11.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e"
+dependencies = [
+ "ahash 0.7.6",
+]
+
+[[package]]
+name = "hashbrown"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
@@ -685,7 +865,7 @@ version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "69fe1fcf8b4278d860ad0548329f892a3631fb63f82574df68275f34cdbe0ffa"
dependencies = [
- "hashbrown",
+ "hashbrown 0.12.3",
]
[[package]]
@@ -699,6 +879,15 @@ dependencies = [
[[package]]
name = "hermit-abi"
+version = "0.1.19"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
+dependencies = [
+ "libc",
+]
+
+[[package]]
+name = "hermit-abi"
version = "0.2.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ee512640fe35acbfb4bb779db6f0d80704c2cacfa2e39b601ef3e3f47d1ae4c7"
@@ -736,6 +925,38 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4a1e36c821dbe04574f602848a19f742f4fb3c98d40449f11bcad18d6b17421"
[[package]]
+name = "humansize"
+version = "2.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
+dependencies = [
+ "libm",
+]
+
+[[package]]
+name = "iana-time-zone"
+version = "0.1.56"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0722cd7114b7de04316e7ea5456a0bbb20e4adb46fd27a3697adb812cff0f37c"
+dependencies = [
+ "android_system_properties",
+ "core-foundation-sys",
+ "iana-time-zone-haiku",
+ "js-sys",
+ "wasm-bindgen",
+ "windows",
+]
+
+[[package]]
+name = "iana-time-zone-haiku"
+version = "0.1.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
+dependencies = [
+ "cc",
+]
+
+[[package]]
name = "idna"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -746,13 +967,30 @@ dependencies = [
]
[[package]]
+name = "ignore"
+version = "0.4.20"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492"
+dependencies = [
+ "globset",
+ "lazy_static",
+ "log",
+ "memchr",
+ "regex",
+ "same-file",
+ "thread_local",
+ "walkdir",
+ "winapi-util",
+]
+
+[[package]]
name = "indexmap"
version = "1.9.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1885e79c1fc4b10f0e172c475f458b7f7b93061064d98c3293e98c5ba0c8b399"
dependencies = [
"autocfg",
- "hashbrown",
+ "hashbrown 0.12.3",
]
[[package]]
@@ -804,6 +1042,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4345964bb142484797b161f473a503a434de77149dd8c7427788c6e13379388"
[[package]]
+name = "lasso"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "aeb7b21a526375c5ca55f1a6dfd4e1fad9fa4edd750f530252a718a44b2608f0"
+dependencies = [
+ "hashbrown 0.11.2",
+]
+
+[[package]]
name = "lazy_static"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -975,7 +1222,7 @@ version = "1.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0fac9e2da13b5eb447a6ce3d392f23a29d8694bff781bf03a16cd9ac8697593b"
dependencies = [
- "hermit-abi",
+ "hermit-abi 0.2.6",
"libc",
]
@@ -986,6 +1233,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3"
[[package]]
+name = "ordered-multimap"
+version = "0.4.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a"
+dependencies = [
+ "dlv-list",
+ "hashbrown 0.12.3",
+]
+
+[[package]]
name = "parking_lot"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1034,6 +1291,15 @@ dependencies = [
]
[[package]]
+name = "parse-zoneinfo"
+version = "0.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c705f256449c60da65e11ff6626e0c16a0a0b96aaa348de61376b249bc340f41"
+dependencies = [
+ "regex",
+]
+
+[[package]]
name = "paste"
version = "1.0.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1055,6 +1321,105 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
[[package]]
+name = "pest"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e68e84bfb01f0507134eac1e9b410a12ba379d064eab48c50ba4ce329a527b70"
+dependencies = [
+ "thiserror",
+ "ucd-trie",
+]
+
+[[package]]
+name = "pest_derive"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6b79d4c71c865a25a4322296122e3924d30bc8ee0834c8bfc8b95f7f054afbfb"
+dependencies = [
+ "pest",
+ "pest_generator",
+]
+
+[[package]]
+name = "pest_generator"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6c435bf1076437b851ebc8edc3a18442796b30f1728ffea6262d59bbe28b077e"
+dependencies = [
+ "pest",
+ "pest_meta",
+ "proc-macro2",
+ "quote",
+ "syn 2.0.2",
+]
+
+[[package]]
+name = "pest_meta"
+version = "2.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "745a452f8eb71e39ffd8ee32b3c5f51d03845f99786fa9b68db6ff509c505411"
+dependencies = [
+ "once_cell",
+ "pest",
+ "sha2",
+]
+
+[[package]]
+name = "phf"
+version = "0.10.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "fabbf1ead8a5bcbc20f5f8b939ee3f5b0f6f281b6ad3468b84656b658b455259"
+dependencies = [
+ "phf_macros",
+ "phf_shared",
+ "proc-macro-hack",
+]
+
+[[package]]
+name = "phf_codegen"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4fb1c3a8bc4dd4e5cfce29b44ffc14bedd2ee294559a294e2a4d4c9e9a6a13cd"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+]
+
+[[package]]
+name = "phf_generator"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5d5285893bb5eb82e6aaf5d59ee909a06a16737a8970984dd7746ba9283498d6"
+dependencies = [
+ "phf_shared",
+ "rand",
+]
+
+[[package]]
+name = "phf_macros"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "58fdf3184dd560f160dd73922bea2d5cd6e8f064bf4b13110abd81b03697b4e0"
+dependencies = [
+ "phf_generator",
+ "phf_shared",
+ "proc-macro-hack",
+ "proc-macro2",
+ "quote",
+ "syn 1.0.109",
+]
+
+[[package]]
+name = "phf_shared"
+version = "0.10.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b6796ad771acdc0123d2a88dc428b5e38ef24456743ddb1744ed628f9815c096"
+dependencies = [
+ "siphasher",
+ "uncased",
+]
+
+[[package]]
name = "pin-project-lite"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1101,6 +1466,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
+name = "proc-macro-hack"
+version = "0.5.20+deprecated"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "dc375e1527247fe1a97d8b7156678dfe7c1af2fc075c9a4db3690ecd2a148068"
+
+[[package]]
name = "proc-macro2"
version = "1.0.52"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1228,20 +1599,35 @@ dependencies = [
]
[[package]]
+name = "rust-ini"
+version = "0.18.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df"
+dependencies = [
+ "cfg-if",
+ "ordered-multimap",
+]
+
+[[package]]
name = "rust-pw-server"
version = "0.1.0"
dependencies = [
"actix-web",
"dotenv",
"exun",
+ "grass",
"hex",
"log",
+ "parking_lot 0.12.1",
"raise",
"rand",
"rust-argon2",
+ "rust-ini",
"serde",
"sqlx",
+ "tera",
"thiserror",
+ "unic-langid",
"uuid",
]
@@ -1282,6 +1668,15 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f91339c0467de62360649f8d3e185ca8de4224ff281f66000de5eb2a77a79041"
[[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 = "scopeguard"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1378,6 +1773,12 @@ dependencies = [
]
[[package]]
+name = "siphasher"
+version = "0.3.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7bd3e3206899af3f8b12af284fafc038cc1dc2b41d1b89dd17297221c5d225de"
+
+[[package]]
name = "slab"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1387,6 +1788,15 @@ dependencies = [
]
[[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.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1534,6 +1944,12 @@ dependencies = [
]
[[package]]
+name = "strsim"
+version = "0.8.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "8ea5119cdb4c55b55d432abb513a0429384878c15dde60cc77b1c99de1a95a6a"
+
+[[package]]
name = "subtle"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1562,6 +1978,38 @@ dependencies = [
]
[[package]]
+name = "tera"
+version = "1.18.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "95a665751302f22a03c56721e23094e4dc22b04a80f381e6737a07bf7a7c70c0"
+dependencies = [
+ "chrono",
+ "chrono-tz",
+ "globwalk",
+ "humansize",
+ "lazy_static",
+ "percent-encoding",
+ "pest",
+ "pest_derive",
+ "rand",
+ "regex",
+ "serde",
+ "serde_json",
+ "slug",
+ "thread_local",
+ "unic-segment",
+]
+
+[[package]]
+name = "textwrap"
+version = "0.11.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d326610f408c7a4eb6f51c37c330e496b08506c9457c9d34287ecc38809fb060"
+dependencies = [
+ "unicode-width",
+]
+
+[[package]]
name = "thiserror"
version = "1.0.40"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1582,6 +2030,15 @@ dependencies = [
]
[[package]]
+name = "thread_local"
+version = "1.1.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180"
+dependencies = [
+ "once_cell",
+]
+
+[[package]]
name = "time"
version = "0.3.20"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1609,6 +2066,15 @@ dependencies = [
]
[[package]]
+name = "tinystr"
+version = "0.7.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7ac3f5b6856e931e15e07b478e98c8045239829a65f9156d4fa7e7788197a5ef"
+dependencies = [
+ "displaydoc",
+]
+
+[[package]]
name = "tinyvec"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1706,6 +2172,90 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba"
[[package]]
+name = "ucd-trie"
+version = "0.1.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81"
+
+[[package]]
+name = "uncased"
+version = "0.9.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "9b9bc53168a4be7402ab86c3aad243a84dd7381d09be0eddc81280c1da95ca68"
+dependencies = [
+ "version_check",
+]
+
+[[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-langid"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "398f9ad7239db44fd0f80fe068d12ff22d78354080332a5077dc6f52f14dcf2f"
+dependencies = [
+ "unic-langid-impl",
+]
+
+[[package]]
+name = "unic-langid-impl"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e35bfd2f2b8796545b55d7d3fd3e89a0613f68a0d1c8bc28cb7ff96b411a35ff"
+dependencies = [
+ "serde",
+ "tinystr",
+]
+
+[[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 = "unicode-bidi"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1733,6 +2283,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
[[package]]
+name = "unicode-width"
+version = "0.1.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
+
+[[package]]
name = "unicode_categories"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1767,12 +2323,28 @@ dependencies = [
]
[[package]]
+name = "vec_map"
+version = "0.8.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
+
+[[package]]
name = "version_check"
version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
+name = "walkdir"
+version = "2.3.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698"
+dependencies = [
+ "same-file",
+ "winapi-util",
+]
+
+[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1878,18 +2450,36 @@ 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",
+]
+
+[[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 = "windows"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e686886bc078bc1b0b600cac0147aadb815089b6e4da64016cbd754b6342700f"
+dependencies = [
+ "windows-targets 0.48.0",
+]
+
+[[package]]
name = "windows-sys"
version = "0.45.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0"
dependencies = [
- "windows-targets",
+ "windows-targets 0.42.2",
]
[[package]]
@@ -1898,13 +2488,28 @@ version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071"
dependencies = [
- "windows_aarch64_gnullvm",
- "windows_aarch64_msvc",
- "windows_i686_gnu",
- "windows_i686_msvc",
- "windows_x86_64_gnu",
- "windows_x86_64_gnullvm",
- "windows_x86_64_msvc",
+ "windows_aarch64_gnullvm 0.42.2",
+ "windows_aarch64_msvc 0.42.2",
+ "windows_i686_gnu 0.42.2",
+ "windows_i686_msvc 0.42.2",
+ "windows_x86_64_gnu 0.42.2",
+ "windows_x86_64_gnullvm 0.42.2",
+ "windows_x86_64_msvc 0.42.2",
+]
+
+[[package]]
+name = "windows-targets"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7b1eb6f0cd7c80c79759c929114ef071b87354ce476d9d94271031c0497adfd5"
+dependencies = [
+ "windows_aarch64_gnullvm 0.48.0",
+ "windows_aarch64_msvc 0.48.0",
+ "windows_i686_gnu 0.48.0",
+ "windows_i686_msvc 0.48.0",
+ "windows_x86_64_gnu 0.48.0",
+ "windows_x86_64_gnullvm 0.48.0",
+ "windows_x86_64_msvc 0.48.0",
]
[[package]]
@@ -1914,42 +2519,84 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8"
[[package]]
+name = "windows_aarch64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "91ae572e1b79dba883e0d315474df7305d12f569b400fcf90581b06062f7e1bc"
+
+[[package]]
name = "windows_aarch64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43"
[[package]]
+name = "windows_aarch64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b2ef27e0d7bdfcfc7b868b317c1d32c641a6fe4629c171b8928c7b08d98d7cf3"
+
+[[package]]
name = "windows_i686_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f"
[[package]]
+name = "windows_i686_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "622a1962a7db830d6fd0a69683c80a18fda201879f0f447f065a3b7467daa241"
+
+[[package]]
name = "windows_i686_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060"
[[package]]
+name = "windows_i686_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "4542c6e364ce21bf45d69fdd2a8e455fa38d316158cfd43b3ac1c5b1b19f8e00"
+
+[[package]]
name = "windows_x86_64_gnu"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36"
[[package]]
+name = "windows_x86_64_gnu"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ca2b8a661f7628cbd23440e50b05d705db3686f894fc9580820623656af974b1"
+
+[[package]]
name = "windows_x86_64_gnullvm"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3"
[[package]]
+name = "windows_x86_64_gnullvm"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7896dbc1f41e08872e9d5e8f8baa8fdd2677f29468c4e156210174edc7f7b953"
+
+[[package]]
name = "windows_x86_64_msvc"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0"
[[package]]
+name = "windows_x86_64_msvc"
+version = "0.48.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "1a515f5799fe4961cb532f983ce2b23082366b898e52ffbce459c86f67c8378a"
+
+[[package]]
name = "zeroize"
version = "1.5.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
diff --git a/Cargo.toml b/Cargo.toml
index 92eff53..f56f5fb 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,14 +7,19 @@ edition = "2021"
[dependencies]
actix-web = "4"
+tera = "1"
serde = "1"
thiserror = "1"
rust-argon2 = "1"
uuid = { version = "1", features = [ "v4", "fast-rng", "serde" ] }
raise = "2"
+exun = "0.1"
+rust-ini = "0.18"
dotenv = "0.15"
+parking_lot = "0.12"
+grass = "0.12"
+unic-langid = { version = "0.9", features = ["serde"] }
rand = "0.8"
sqlx = { version = "0.6", features = [ "runtime-actix-rustls", "mysql", "uuid", "offline" ] }
log = "0.4"
hex = "0.4"
-exun = "0.1"
diff --git a/src/api/liveops.rs b/src/api/liveops.rs
index 2dda41d..d4bf129 100644
--- a/src/api/liveops.rs
+++ b/src/api/liveops.rs
@@ -1,11 +1,11 @@
use actix_web::{get, web, HttpResponse, Scope};
/// Simple ping
-#[get("ping")]
+#[get("/ping")]
async fn ping() -> HttpResponse {
HttpResponse::Ok().finish()
}
pub fn service() -> Scope {
- web::scope("liveops").service(ping)
+ web::scope("/liveops").service(ping)
}
diff --git a/src/api/ops.rs b/src/api/ops.rs
index 018743c..d947e64 100644
--- a/src/api/ops.rs
+++ b/src/api/ops.rs
@@ -1,9 +1,14 @@
-use actix_web::{http::StatusCode, post, web, HttpResponse, ResponseError, Scope};
+use std::str::FromStr;
+
+use actix_web::{get, http::StatusCode, post, web, HttpResponse, ResponseError, Scope};
use raise::yeet;
use serde::Deserialize;
use sqlx::MySqlPool;
+use tera::Tera;
use thiserror::Error;
+use unic_langid::subtags::Language;
+use crate::resources::{languages, templates};
use crate::services::db;
/// A request to login
@@ -60,6 +65,17 @@ async fn login(
Ok(response)
}
+#[get("/login")]
+async fn login_page(
+ tera: web::Data<Tera>,
+ translations: web::Data<languages::Translations>,
+) -> HttpResponse {
+ // TODO find a better way of doing this
+ let language = Language::from_str("en").unwrap();
+ let page = templates::login_page(&tera, language, translations.get_ref().clone()).unwrap();
+ HttpResponse::Ok().content_type("text/html").body(page)
+}
+
pub fn service() -> Scope {
- web::scope("").service(login)
+ web::scope("").service(login).service(login_page)
}
diff --git a/src/api/users.rs b/src/api/users.rs
index 863d99e..353f8ff 100644
--- a/src/api/users.rs
+++ b/src/api/users.rs
@@ -26,19 +26,21 @@ impl From<User> for UserResponse {
}
}
-#[get("/")]
-async fn search_users(
- web::Query(username): web::Query<Option<Box<str>>>,
- web::Query(limit): web::Query<Option<u32>>,
- web::Query(offset): web::Query<Option<u32>>,
- conn: web::Data<MySqlPool>,
-) -> HttpResponse {
+#[derive(Debug, Clone, Deserialize)]
+struct SearchUsers {
+ username: Option<Box<str>>,
+ limit: Option<u32>,
+ offset: Option<u32>,
+}
+
+#[get("")]
+async fn search_users(params: web::Query<SearchUsers>, conn: web::Data<MySqlPool>) -> HttpResponse {
let conn = conn.get_ref();
- let username = username.unwrap_or_default();
- let offset = offset.unwrap_or_default();
+ let username = params.username.clone().unwrap_or_default();
+ let offset = params.offset.unwrap_or_default();
- let results: Box<[UserResponse]> = if let Some(limit) = limit {
+ let results: Box<[UserResponse]> = if let Some(limit) = params.limit {
db::search_users_limit(conn, &username, offset, limit)
.await
.unwrap()
@@ -129,7 +131,7 @@ impl ResponseError for UsernameTakenError {
}
}
-#[post("/")]
+#[post("")]
async fn create_user(
body: web::Json<UserRequest>,
conn: web::Data<MySqlPool>,
diff --git a/src/main.rs b/src/main.rs
index 7d2b643..7b25dd1 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -1,5 +1,5 @@
use actix_web::http::header::{self, HeaderValue};
-use actix_web::middleware::{DefaultHeaders, ErrorHandlerResponse, ErrorHandlers, Logger};
+use actix_web::middleware::{ErrorHandlerResponse, ErrorHandlers, Logger, NormalizePath};
use actix_web::web::Data;
use actix_web::{dev, App, HttpServer};
@@ -7,8 +7,10 @@ use exun::*;
mod api;
mod models;
+mod resources;
mod services;
+use resources::*;
use services::*;
fn error_content_language<B>(
@@ -31,12 +33,27 @@ async fn main() -> Result<(), RawUnexpected> {
let db_url = secrets::database_url()?;
let sql_pool = db::initialize(&db_url).await?;
+ let tera = templates::initialize()?;
+
+ let translations = languages::initialize()?;
+
// start the server
HttpServer::new(move || {
App::new()
+ // middleware
.wrap(ErrorHandlers::new().default_handler(error_content_language))
+ .wrap(NormalizePath::trim())
.wrap(Logger::new("\"%r\" %s %Dms"))
+ // app shared state
.app_data(Data::new(sql_pool.clone()))
+ .app_data(Data::new(tera.clone()))
+ .app_data(Data::new(translations.clone()))
+ // frontend services
+ // has to be first so they don't get overwritten by the "" scope
+ .service(style::get_css)
+ .service(scripts::get_js)
+ .service(languages::languages())
+ // api services
.service(api::liveops())
.service(api::users())
.service(api::ops())
diff --git a/src/resources/languages.rs b/src/resources/languages.rs
new file mode 100644
index 0000000..8ef7553
--- /dev/null
+++ b/src/resources/languages.rs
@@ -0,0 +1,67 @@
+use std::collections::HashMap;
+use std::path::PathBuf;
+
+use actix_web::{get, web, HttpResponse, Scope};
+use exun::RawUnexpected;
+use ini::{Ini, Properties};
+use raise::yeet;
+use unic_langid::subtags::Language;
+
+#[derive(Debug, Clone, PartialEq)]
+pub struct Translations {
+ languages: HashMap<Language, Properties>,
+}
+
+pub fn initialize() -> Result<Translations, RawUnexpected> {
+ let mut translations = Translations {
+ languages: HashMap::new(),
+ };
+ translations.refresh()?;
+ Ok(translations)
+}
+
+impl Translations {
+ pub fn languages(&self) -> Box<[Language]> {
+ self.languages.keys().cloned().collect()
+ }
+
+ pub fn get_message(&self, language: Language, key: &str) -> Option<String> {
+ Some(self.languages.get(&language)?.get(key)?.to_owned())
+ }
+
+ pub fn refresh(&mut self) -> Result<(), RawUnexpected> {
+ let mut languages = HashMap::with_capacity(1);
+ for entry in PathBuf::from("static/languages").read_dir()? {
+ let entry = entry?;
+ if entry.file_type()?.is_dir() {
+ continue;
+ }
+
+ let path = entry.path();
+ let path = path.to_string_lossy();
+ let Some(language) = path.as_bytes().get(0..2) else { yeet!(RawUnexpected::msg(format!("{} not long enough to be a language name", path))) };
+ let language = Language::from_bytes(language)?;
+ let messages = Ini::load_from_file(entry.path())?.general_section().clone();
+
+ languages.insert(language, messages);
+ }
+
+ self.languages = languages;
+ Ok(())
+ }
+}
+
+#[get("")]
+pub async fn all_languages(translations: web::Data<Translations>) -> HttpResponse {
+ HttpResponse::Ok().json(
+ translations
+ .languages()
+ .into_iter()
+ .map(|l| l.as_str())
+ .collect::<Box<[&str]>>(),
+ )
+}
+
+pub fn languages() -> Scope {
+ web::scope("/languages").service(all_languages)
+}
diff --git a/src/resources/mod.rs b/src/resources/mod.rs
new file mode 100644
index 0000000..9251d2c
--- /dev/null
+++ b/src/resources/mod.rs
@@ -0,0 +1,4 @@
+pub mod languages;
+pub mod scripts;
+pub mod style;
+pub mod templates;
diff --git a/src/resources/scripts.rs b/src/resources/scripts.rs
new file mode 100644
index 0000000..3e2d869
--- /dev/null
+++ b/src/resources/scripts.rs
@@ -0,0 +1,37 @@
+use std::path::{Path, PathBuf};
+
+use actix_web::{get, http::StatusCode, web, HttpResponse, ResponseError};
+use exun::{Expect, ResultErrorExt};
+use raise::yeet;
+use serde::Serialize;
+use thiserror::Error;
+
+#[derive(Debug, Clone, Error, Serialize)]
+pub enum LoadScriptError {
+ #[error("The requested script does not exist")]
+ FileNotFound(Box<Path>),
+}
+
+impl ResponseError for LoadScriptError {
+ fn status_code(&self) -> StatusCode {
+ match self {
+ Self::FileNotFound(..) => StatusCode::NOT_FOUND,
+ }
+ }
+}
+
+fn load(script: &str) -> Result<String, Expect<LoadScriptError>> {
+ let path = PathBuf::from(format!("static/scripts/{}.js", script));
+ if !path.exists() {
+ yeet!(LoadScriptError::FileNotFound(path.into()).into());
+ }
+ let js = std::fs::read_to_string(format!("static/scripts/{}.js", script)).unexpect()?;
+ Ok(js)
+}
+
+#[get("/{script}.js")]
+pub async fn get_js(script: web::Path<Box<str>>) -> Result<HttpResponse, LoadScriptError> {
+ let js = load(&script).map_err(|e| e.unwrap())?;
+ let response = HttpResponse::Ok().content_type("text/javascript").body(js);
+ Ok(response)
+}
diff --git a/src/resources/style.rs b/src/resources/style.rs
new file mode 100644
index 0000000..2777a82
--- /dev/null
+++ b/src/resources/style.rs
@@ -0,0 +1,53 @@
+use std::path::{Path, PathBuf};
+
+use actix_web::{get, http::StatusCode, web, HttpResponse, ResponseError};
+use exun::{Expect, ResultErrorExt};
+use grass::OutputStyle;
+use raise::yeet;
+use serde::Serialize;
+use thiserror::Error;
+
+fn output_style() -> OutputStyle {
+ if cfg!(debug_assertions) {
+ OutputStyle::Expanded
+ } else {
+ OutputStyle::Compressed
+ }
+}
+
+fn options() -> grass::Options<'static> {
+ grass::Options::default()
+ .load_path("static/style")
+ .style(output_style())
+}
+
+#[derive(Debug, Clone, Error, Serialize)]
+pub enum LoadStyleError {
+ #[error("The requested stylesheet was not found")]
+ FileNotFound(Box<Path>),
+}
+
+impl ResponseError for LoadStyleError {
+ fn status_code(&self) -> StatusCode {
+ match self {
+ Self::FileNotFound(..) => StatusCode::NOT_FOUND,
+ }
+ }
+}
+
+pub fn load(stylesheet: &str) -> Result<String, Expect<LoadStyleError>> {
+ let options = options();
+ let path = PathBuf::from(format!("static/style/{}.scss", stylesheet));
+ if !path.exists() {
+ yeet!(LoadStyleError::FileNotFound(path.into()).into());
+ }
+ let css = grass::from_path(format!("static/style/{}.scss", stylesheet), &options).unexpect()?;
+ Ok(css)
+}
+
+#[get("/{stylesheet}.css")]
+pub async fn get_css(stylesheet: web::Path<Box<str>>) -> Result<HttpResponse, LoadStyleError> {
+ let css = load(&stylesheet).map_err(|e| e.unwrap())?;
+ let response = HttpResponse::Ok().content_type("text/css").body(css);
+ Ok(response)
+}
diff --git a/src/resources/templates.rs b/src/resources/templates.rs
new file mode 100644
index 0000000..43d6b67
--- /dev/null
+++ b/src/resources/templates.rs
@@ -0,0 +1,59 @@
+use std::collections::HashMap;
+
+use exun::{RawUnexpected, ResultErrorExt};
+use raise::yeet;
+use tera::{Function, Tera, Value};
+use unic_langid::subtags::Language;
+
+use super::languages;
+
+fn make_lang(language: Language) -> impl Function {
+ Box::new(move |_: &HashMap<String, Value>| -> tera::Result<Value> {
+ Ok(Value::String(language.to_string()))
+ })
+}
+
+fn make_msg(language: Language, translations: languages::Translations) -> impl Function {
+ Box::new(
+ move |args: &HashMap<String, Value>| -> tera::Result<Value> {
+ let Some(key) = args.get("key") else { yeet!("No parameter 'key' provided".into()) };
+ let Some(key) = key.as_str() else { yeet!(format!("{} is not a string", key).into()) };
+ let Some(value) = translations.get_message(language, key) else { yeet!(format!("{} does not exist", key).into()) };
+ Ok(Value::String(value))
+ },
+ )
+}
+
+fn make_base_url() -> impl Function {
+ Box::new(|_: &HashMap<String, Value>| Ok(Value::String("foo".to_string())))
+}
+
+fn extend_tera(
+ tera: &Tera,
+ language: Language,
+ translations: languages::Translations,
+) -> Result<Tera, RawUnexpected> {
+ let mut new_tera = initialize()?;
+ new_tera.extend(tera)?;
+ new_tera.register_function("lang", make_lang(language));
+ new_tera.register_function("msg", make_msg(language, translations));
+ new_tera.register_function("baseUrl", make_base_url());
+ Ok(new_tera)
+}
+
+pub fn initialize() -> tera::Result<Tera> {
+ let tera = Tera::new("static/templates/*")?;
+ Ok(tera)
+}
+
+pub fn login_page(
+ tera: &Tera,
+ language: Language,
+ mut translations: languages::Translations,
+) -> Result<String, RawUnexpected> {
+ translations.refresh()?;
+ let mut tera = extend_tera(tera, language, translations)?;
+ tera.full_reload()?;
+ let context = tera::Context::new();
+ tera.render("login.html", &context).unexpect()
+}
diff --git a/static/languages/en.ini b/static/languages/en.ini
new file mode 100644
index 0000000..1848a7d
--- /dev/null
+++ b/static/languages/en.ini
@@ -0,0 +1,6 @@
+loginTitle = Log In
+usernameLabel = Username
+usernamePlaceholder = Enter your username
+passwordLabel = Password
+passwordPlaceholder = Enter your password
+loginSubmitButton = Log In
diff --git a/static/scripts/main.js b/static/scripts/main.js
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/static/scripts/main.js
diff --git a/static/style/style.scss b/static/style/style.scss
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/static/style/style.scss
diff --git a/static/templates/base.html b/static/templates/base.html
new file mode 100644
index 0000000..2d9f8fd
--- /dev/null
+++ b/static/templates/base.html
@@ -0,0 +1,16 @@
+<!DOCTYPE html>
+<html lang="{{lang()}}">
+ <head>
+ <title>{% block title %}{% endblock title %}</title>
+ <meta charset="utf-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
+ <link rel="stylesheet" href="/style.css" />
+ {% block head %}{% endblock head %}
+ </head>
+ <body>
+ {% block content %}{% endblock content %}
+ <footer>
+ <div id="copyright">&copy; 2023 Amplify Education Inc.</div>
+ </footer>
+ </body>
+</html>
diff --git a/static/templates/login.html b/static/templates/login.html
new file mode 100644
index 0000000..fd3d136
--- /dev/null
+++ b/static/templates/login.html
@@ -0,0 +1,11 @@
+{% extends "base.html" %}
+{% block title %}Log In{% endblock title %}
+{% block content %}
+<form method="post" action="{{ baseUrl() }}/authenticate">
+ <label for="username">Username</label>
+ <input id="username" type="text" name="username" tabindex="0" placeholder="Enter your username" autofocus autocomplete="off" />
+ <label for="password">Password</label>
+ <input id="password" type="password" name="password" tabindex="0" placeholder="Enter your password" autofocus autocomplete="off" />
+ <input type="submit" tabindex="0" name="login" value="Log In" />
+</form>
+{% endblock content %} \ No newline at end of file