1
0
Fork 0

We have a web UI now, I guess

This commit is contained in:
Honbra 2024-06-09 18:29:07 +02:00
parent 75b87e7bac
commit 331423d3f6
Signed by: honbra
GPG key ID: B61CC9ADABE2D952
28 changed files with 1471 additions and 494 deletions

3
.prettierrc Normal file
View file

@ -0,0 +1,3 @@
{
"tabWidth": 4
}

View file

@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO file_key (id, file_id) VALUES ($1, $2)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Uuid"
]
},
"nullable": []
},
"hash": "0687afdf61aef5edee2b530e67d81bf7eef0678276d1e9674398d99684f818ca"
}

View file

@ -1,6 +1,6 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id, hash, mime FROM file WHERE id = $1",
"query": "SELECT id, title FROM paste",
"describe": {
"columns": [
{
@ -10,25 +10,17 @@
},
{
"ordinal": 1,
"name": "hash",
"type_info": "Bytea"
},
{
"ordinal": 2,
"name": "mime",
"name": "title",
"type_info": "Text"
}
],
"parameters": {
"Left": [
"Uuid"
]
"Left": []
},
"nullable": [
false,
false,
false
]
},
"hash": "3a21749fe8df1fd7eb2e77ac89b19217f0c99925b33b928f223c33f0d8cfd551"
"hash": "218cab8328020896b78a81be345a2074ca5461affae6b7f4daa07ba6618f96aa"
}

View file

@ -1,16 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO file (id, hash, mime) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Bytea",
"Text"
]
},
"nullable": []
},
"hash": "25c1819af558e744e238802f3e30651897914b080ec395db706782f4fbe3042b"
}

View file

@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "SELECT id FROM file_key WHERE file_id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "id",
"type_info": "Uuid"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false
]
},
"hash": "44d061458ca74c102b74d132c8f1dd8ce5ad8839398e382af103962f206317fc"
}

View file

@ -1,16 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "SELECT hash, mime FROM file_key JOIN file ON file_id = file.id WHERE file_key.id = $1",
"query": "SELECT title, content FROM paste WHERE id = $1",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "hash",
"type_info": "Bytea"
"name": "title",
"type_info": "Text"
},
{
"ordinal": 1,
"name": "mime",
"name": "content",
"type_info": "Text"
}
],
@ -24,5 +24,5 @@
false
]
},
"hash": "7a41221f1c34f6ac44691f3e29bb48a7bf59a667f99462c823137a04b5d80ea3"
"hash": "b5a5561c8b6458f14c64e5df09ce6e798eb6d3f63f248a950db817600f272715"
}

View file

@ -1,15 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM file_key WHERE id = $1 AND file_id = $2",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Uuid"
]
},
"nullable": []
},
"hash": "bdb8d776b5cbf82b35b2d24a3fdb4ca049d36d8370861ba4be25372e542a0ba1"
}

View file

@ -0,0 +1,16 @@
{
"db_name": "PostgreSQL",
"query": "INSERT INTO paste (id, title, content) VALUES ($1, $2, $3)",
"describe": {
"columns": [],
"parameters": {
"Left": [
"Uuid",
"Text",
"Text"
]
},
"nullable": []
},
"hash": "c9694938fa65a37d27cbbe251e7bc7ae38772b677e4d99c60c2f6d616078f94e"
}

View file

@ -1,22 +0,0 @@
{
"db_name": "PostgreSQL",
"query": "DELETE FROM file WHERE id = $1 RETURNING hash",
"describe": {
"columns": [
{
"ordinal": 0,
"name": "hash",
"type_info": "Bytea"
}
],
"parameters": {
"Left": [
"Uuid"
]
},
"nullable": [
false
]
},
"hash": "ed6ee326516d37d078ce80b39d769747a88683fab15feb466c848c2f2ba65c50"
}

303
Cargo.lock generated
View file

@ -30,12 +30,42 @@ dependencies = [
"zerocopy",
]
[[package]]
name = "alloc-no-stdlib"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
[[package]]
name = "alloc-stdlib"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
dependencies = [
"alloc-no-stdlib",
]
[[package]]
name = "allocator-api2"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f"
[[package]]
name = "android-tzdata"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0"
[[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 = "async-trait"
version = "0.1.80"
@ -137,7 +167,6 @@ dependencies = [
"axum-core",
"bytes",
"futures-util",
"headers",
"http",
"http-body",
"http-body-util",
@ -215,6 +244,27 @@ dependencies = [
"generic-array",
]
[[package]]
name = "brotli"
version = "5.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19483b140a7ac7174d34b5a581b406c64f84da5409d3e09cf4fff604f9270e67"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
"brotli-decompressor",
]
[[package]]
name = "brotli-decompressor"
version = "4.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
]
[[package]]
name = "bumpalo"
version = "3.16.0"
@ -251,12 +301,30 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chrono"
version = "0.4.38"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
dependencies = [
"android-tzdata",
"iana-time-zone",
"num-traits",
"windows-targets 0.52.5",
]
[[package]]
name = "const-oid"
version = "0.9.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
[[package]]
name = "core-foundation-sys"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f"
[[package]]
name = "cpufeatures"
version = "0.2.12"
@ -281,6 +349,15 @@ version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5"
[[package]]
name = "crc32fast"
version = "1.4.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3"
dependencies = [
"cfg-if",
]
[[package]]
name = "crossbeam-queue"
version = "0.3.11"
@ -395,9 +472,9 @@ checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
[[package]]
name = "figment"
version = "0.10.18"
version = "0.10.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d032832d74006f99547004d49410a4b4218e4c33382d56ca3ff89df74f86b953"
checksum = "8cb01cd46b0cf372153850f4c6c272d9cbea2da513e07538405148f95bd789f3"
dependencies = [
"atomic",
"pear",
@ -413,6 +490,16 @@ version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fcfdc7a0362c9f4444381a9e697c79d435fe65b52a37466fc2c1184cee9edc6"
[[package]]
name = "flate2"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f54427cfd1c7829e2a139fcefea601bf088ebca651d2bf53ebc600eac295dae"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]]
name = "flume"
version = "0.11.0"
@ -559,30 +646,6 @@ dependencies = [
"hashbrown",
]
[[package]]
name = "headers"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9"
dependencies = [
"base64",
"bytes",
"headers-core",
"http",
"httpdate",
"mime",
"sha1",
]
[[package]]
name = "headers-core"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4"
dependencies = [
"http",
]
[[package]]
name = "heck"
version = "0.4.1"
@ -718,6 +781,29 @@ dependencies = [
"tokio",
]
[[package]]
name = "iana-time-zone"
version = "0.1.60"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e7ffbb5a1b541ea2561f8c41c087286cc091e21e556a4f09a8f6cbf17b69b141"
dependencies = [
"android_system_properties",
"core-foundation-sys",
"iana-time-zone-haiku",
"js-sys",
"wasm-bindgen",
"windows-core",
]
[[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.5.0"
@ -834,6 +920,30 @@ version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94"
[[package]]
name = "maud"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df518b75016b4289cdddffa1b01f2122f4a49802c93191f3133f6dc2472ebcaa"
dependencies = [
"axum-core",
"http",
"itoa",
"maud_macros",
]
[[package]]
name = "maud_macros"
version = "0.26.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa453238ec218da0af6b11fc5978d3b5c3a45ed97b722391a2a11f3306274e18"
dependencies = [
"proc-macro-error",
"proc-macro2",
"quote",
"syn 2.0.63",
]
[[package]]
name = "md-5"
version = "0.10.6"
@ -850,6 +960,37 @@ version = "2.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c8640c5d730cb13ebd907d8d04b52f55ac9a2eec55b440c8892f40d56c76c1d"
[[package]]
name = "memory-serve"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d1f8b697424035812d76b87ce97cc06bf144eb569838ebc459f0edd6b473699"
dependencies = [
"axum",
"brotli",
"flate2",
"memory-serve-macros",
"sha256",
"tracing",
]
[[package]]
name = "memory-serve-macros"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9049d464b282ee8d87ed38a9144ad263a791e4451c1eb96456da2c00845337f8"
dependencies = [
"brotli",
"mime_guess",
"proc-macro2",
"quote",
"sha256",
"syn 2.0.63",
"tracing",
"tracing-subscriber",
"walkdir",
]
[[package]]
name = "mime"
version = "0.3.17"
@ -898,21 +1039,16 @@ version = "0.1.0"
dependencies = [
"axum",
"axum-extra",
"bytes",
"chrono",
"eyre",
"figment",
"futures-util",
"headers",
"hex",
"http",
"http-body-util",
"mime",
"maud",
"memory-serve",
"serde",
"sha2",
"sqlx",
"thiserror",
"tokio",
"tokio-util",
"tower-http",
"tracing",
"tracing-subscriber",
@ -1151,6 +1287,29 @@ version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de"
[[package]]
name = "proc-macro-error"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c"
dependencies = [
"proc-macro-error-attr",
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro-error-attr"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869"
dependencies = [
"proc-macro2",
"quote",
"version_check",
]
[[package]]
name = "proc-macro2"
version = "1.0.82"
@ -1281,6 +1440,15 @@ version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
[[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.2.0"
@ -1289,18 +1457,18 @@ checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
[[package]]
name = "serde"
version = "1.0.202"
version = "1.0.203"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395"
checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.202"
version = "1.0.203"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838"
checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba"
dependencies = [
"proc-macro2",
"quote",
@ -1371,6 +1539,19 @@ dependencies = [
"digest",
]
[[package]]
name = "sha256"
version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18278f6a914fa3070aa316493f7d2ddfb9ac86ebc06fa3b83bffda487e9065b0"
dependencies = [
"async-trait",
"bytes",
"hex",
"sha2",
"tokio",
]
[[package]]
name = "sharded-slab"
version = "0.1.7"
@ -1714,18 +1895,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.60"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18"
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.60"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524"
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
dependencies = [
"proc-macro2",
"quote",
@ -1759,9 +1940,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.37.0"
version = "1.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
dependencies = [
"backtrace",
"bytes",
@ -1776,9 +1957,9 @@ dependencies = [
[[package]]
name = "tokio-macros"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
dependencies = [
"proc-macro2",
"quote",
@ -2066,6 +2247,16 @@ version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "walkdir"
version = "2.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b"
dependencies = [
"same-file",
"winapi-util",
]
[[package]]
name = "wasi"
version = "0.11.0+wasi-snapshot-preview1"
@ -2168,12 +2359,30 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6"
[[package]]
name = "winapi-util"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4d4cc384e1e73b93bafa6fb4f1df8c41695c8a91cf9c4c64358067d15a7b6c6b"
dependencies = [
"windows-sys 0.52.0",
]
[[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-core"
version = "0.52.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9"
dependencies = [
"windows-targets 0.52.5",
]
[[package]]
name = "windows-sys"
version = "0.48.0"

View file

@ -4,29 +4,24 @@ version = "0.1.0"
edition = "2021"
[dependencies]
axum = { version = "0.7.5", default-features = false, features = ["http1", "json", "macros", "matched-path", "tokio", "tower-log", "tracing", "query"] }
axum-extra = { version = "0.9.3", features = ["async-read-body", "typed-header"] }
bytes = "1.6.0"
axum = { version = "0.7.5", default-features = false, features = ["http1", "json", "macros", "matched-path", "tokio", "tower-log", "tracing", "query", "form"] }
axum-extra = { version = "0.9.3", features = ["async-read-body"] }
chrono = { version = "0.4.38", default-features = false, features = ["alloc", "clock"] }
eyre = "0.6.12"
figment = { version = "0.10.15", features = ["env", "toml"] }
futures-util = { version = "0.3.30", default-features = false }
headers = "0.4.0"
hex = "0.4.3"
figment = { version = "0.10.19", features = ["env", "toml"] }
http = "1.1.0"
http-body-util = "0.1.1"
mime = "0.3.17"
serde = { version = "1.0.197", features = ["derive"] }
sha2 = "0.10.8"
maud = { version = "0.26.0", features = ["axum"] }
memory-serve = "0.4.5"
serde = { version = "1.0.203", features = ["derive"] }
sqlx = { version = "0.7.4", features = ["runtime-tokio", "postgres", "uuid"] }
thiserror = "1.0.58"
tokio = { version = "1.37.0", features = ["rt-multi-thread", "macros", "fs", "io-std"] }
tokio-util = { version = "0.7.10", features = ["io"] }
thiserror = "1.0.61"
tokio = { version = "1.38.0", features = ["rt-multi-thread", "macros", "fs", "io-std"] }
tower-http = { version = "0.5.2", features = ["trace", "fs"] }
tracing = "0.1.40"
tracing-subscriber = "0.3.18"
ulid = { version = "1.1.2", features = ["uuid", "serde"] }
url = { version = "2.5.0", features = ["serde"] }
uuid = "1.7.0"
uuid = "1.8.0"
[profile.release]
strip = true

View file

@ -8,11 +8,11 @@
"rust-analyzer-src": "rust-analyzer-src"
},
"locked": {
"lastModified": 1712384501,
"narHash": "sha256-AZmYmEnc1ZkSlxUJVUtGh9VFAqWPr+xtNIiBqD2eKfc=",
"lastModified": 1717741716,
"narHash": "sha256-v71DDu2gb02iBIAhUwu5mQZNtYD45QcbEqahqsOCCyU=",
"owner": "nix-community",
"repo": "fenix",
"rev": "99c6241db5ca5363c05c8f4acbdf3a4e8fc42844",
"rev": "05f2a8ae62c45637b20d162fa2dd450b79e71c27",
"type": "github"
},
"original": {
@ -41,11 +41,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1712439257,
"narHash": "sha256-aSpiNepFOMk9932HOax0XwNxbA38GOUVOiXfUVPOrck=",
"lastModified": 1717602782,
"narHash": "sha256-pL9jeus5QpX5R+9rsp3hhZ+uplVHscNJh8n8VpqscM0=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "ff0dbd94265ac470dda06a657d5fe49de93b4599",
"rev": "e8057b67ebf307f01bdcc8fba94d94f75039d1f6",
"type": "github"
},
"original": {
@ -65,11 +65,11 @@
"rust-analyzer-src": {
"flake": false,
"locked": {
"lastModified": 1712156296,
"narHash": "sha256-St7ZQrkrr5lmQX9wC1ZJAFxL8W7alswnyZk9d1se3Us=",
"lastModified": 1717583671,
"narHash": "sha256-+lRAmz92CNUxorqWusgJbL9VE1eKCnQQojglRemzwkw=",
"owner": "rust-lang",
"repo": "rust-analyzer",
"rev": "8e581ac348e223488622f4d3003cb2bd412bf27e",
"rev": "48bbdd6a74f3176987d5c809894ac33957000d19",
"type": "github"
},
"original": {

View file

@ -44,6 +44,7 @@
postgresql
sqlfluff
sqlx-cli
tailwindcss
];
};
}

View file

@ -1,3 +1,2 @@
DROP TABLE IF EXISTS file_key;
DROP TABLE IF EXISTS file;
DROP TABLE IF EXISTS paste;
DROP TABLE IF EXISTS link;

View file

@ -4,13 +4,8 @@ CREATE TABLE IF NOT EXISTS link (
destination TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS file (
CREATE TABLE IF NOT EXISTS paste (
id UUID PRIMARY KEY,
hash BYTEA UNIQUE NOT NULL,
mime TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS file_key (
id UUID PRIMARY KEY,
file_id UUID REFERENCES file (id) ON DELETE CASCADE NOT NULL
title TEXT NOT NULL,
content TEXT NOT NULL
);

View file

@ -1,252 +0,0 @@
use std::path::PathBuf;
use axum::{
body::Body,
extract::{Path, Query, State},
routing::{delete, get, post},
Json, Router,
};
use axum_extra::TypedHeader;
use futures_util::TryStreamExt;
use headers::ContentType;
use http::StatusCode;
use mime::Mime;
use serde::{Deserialize, Serialize};
use sha2::{Digest, Sha256};
use sqlx::query;
use tokio::{fs, io};
use tokio_util::io::StreamReader;
use tracing::{error, field, info, instrument};
use ulid::Ulid;
use uuid::Uuid;
use crate::{app::SharedState, error::AppError};
pub fn resource() -> Router<SharedState> {
Router::new()
.route("/files", post(upload_file))
.route("/files/:file_id", get(get_file_info))
.route("/files/:file_id", delete(delete_file))
.route("/files/:file_id/keys/", post(create_file_key))
.route("/files/:file_id/keys/:key_id", delete(delete_file_key))
}
#[derive(Serialize)]
struct File {
id: Ulid,
hash: String,
mime: String,
keys: Vec<Ulid>,
}
#[derive(Serialize)]
struct NewFile {
id: Ulid,
hash: String,
mime: String,
key: Option<Ulid>,
}
#[derive(Deserialize)]
struct UploadFileOptions {
#[serde(default)]
create_key: bool,
}
#[instrument(skip_all)]
async fn upload_file(
State(SharedState { db, config }): State<SharedState>,
Query(UploadFileOptions { create_key }): Query<UploadFileOptions>,
TypedHeader(content_type): TypedHeader<ContentType>,
body: Body,
) -> Result<Json<NewFile>, AppError> {
let id = Ulid::new();
let path_temp = config.file_temp_dir.join(id.to_string());
let mut hasher = Sha256::new();
{
let mut file_temp = fs::File::create(&path_temp).await?;
let better_body = body
.into_data_stream()
.inspect_ok(|b| hasher.update(b))
.map_err(|err| io::Error::new(io::ErrorKind::Other, err));
let mut reader = StreamReader::new(better_body);
if let Err(err) = io::copy(&mut reader, &mut file_temp).await {
error!(
err = field::display(&err),
file_path = field::debug(&path_temp),
"failed to copy file, removing",
);
drop(file_temp);
if let Err(err) = fs::remove_file(path_temp).await {
error!(
err = field::display(err),
"failed to remove failed upload file",
);
}
return Err(err.into());
}
}
let hash = hasher.finalize();
let hash_hex = hex::encode(hash);
let path_hash = PathBuf::from("files").join(&hash_hex);
if fs::try_exists(&path_hash).await? {
info!(hash = hash_hex, "file already exists");
if let Err(err) = fs::remove_file(&path_temp).await {
error!(err = field::display(&err), "failed to remove temp file");
}
} else if let Err(err) = fs::rename(&path_temp, &path_hash).await {
error!(err = field::display(&err), "failed to move finished file");
if let Err(err) = fs::remove_file(&path_temp).await {
error!(
err = field::display(&err),
"failed to remove file after failed move",
);
}
return Err(err.into());
}
let mime = Into::<Mime>::into(content_type);
let mime_str = mime.to_string();
let mut tx = db.begin().await?;
match query!(
"INSERT INTO file (id, hash, mime) VALUES ($1, $2, $3) ON CONFLICT DO NOTHING",
Uuid::from(id),
&hash[..],
mime_str,
)
.execute(&mut *tx)
.await?
.rows_affected()
{
0 | 1 => {}
rows => return Err(AppError::ImpossibleAffectedRows(rows)),
}
// `ON CONFLICT DO NOTHING RETURNING id` only works when there *isn't* a
// conflict
let id = query!("SELECT id FROM file WHERE hash = $1", &hash[..])
.fetch_one(&mut *tx)
.await?
.id
.into();
let mut key_opt = None;
if create_key {
let key = Ulid::new();
key_opt = Some(key);
match query!(
"INSERT INTO file_key (id, file_id) VALUES ($1, $2)",
Uuid::from(key),
Uuid::from(id),
)
.execute(&mut *tx)
.await?
.rows_affected()
{
1 => {}
0 => return Err(AppError::UlidConflict(key)),
rows => return Err(AppError::ImpossibleAffectedRows(rows)),
}
}
tx.commit().await?;
Ok(Json(NewFile {
id,
hash: hash_hex,
mime: mime_str,
key: key_opt,
}))
}
async fn get_file_info(
State(SharedState { db, .. }): State<SharedState>,
Path(id): Path<Ulid>,
) -> Result<Json<File>, AppError> {
let (file, keys) = tokio::try_join!(
query!(
"SELECT id, hash, mime FROM file WHERE id = $1",
Uuid::from(id),
)
.fetch_optional(&db),
query!("SELECT id FROM file_key WHERE file_id = $1", Uuid::from(id)).fetch_all(&db),
)?;
match file {
Some(r) => Ok(Json(File {
id,
hash: hex::encode(r.hash),
mime: r.mime,
keys: keys.into_iter().map(|r| r.id.into()).collect(),
})),
None => Err(AppError::FileNotFound(id)),
}
}
#[instrument(skip_all)]
async fn delete_file(
State(SharedState { db, config }): State<SharedState>,
Path(file_id): Path<Ulid>,
) -> Result<StatusCode, AppError> {
let file_hash = query!(
"DELETE FROM file WHERE id = $1 RETURNING hash",
Uuid::from(file_id)
)
.fetch_optional(&db)
.await?
.ok_or(AppError::FileNotFound(file_id))?
.hash;
let file_path = config.file_store_dir.join(hex::encode(file_hash));
if let Err(err) = fs::remove_file(file_path).await {
error!(err = field::display(err), "failed to remove file");
}
Ok(StatusCode::NO_CONTENT)
}
async fn create_file_key(
State(SharedState { db, .. }): State<SharedState>,
Path(file_id): Path<Ulid>,
) -> Result<(StatusCode, Json<Ulid>), AppError> {
let key_id = Ulid::new();
match query!(
"INSERT INTO file_key (id, file_id) VALUES ($1, $2)",
Uuid::from(key_id),
Uuid::from(file_id),
)
.execute(&db)
.await?
.rows_affected()
{
1 => Ok((StatusCode::CREATED, Json(key_id))),
rows => Err(AppError::ImpossibleAffectedRows(rows)),
}
}
async fn delete_file_key(
State(SharedState { db, .. }): State<SharedState>,
Path((file_id, key_id)): Path<(Ulid, Ulid)>,
) -> Result<StatusCode, AppError> {
match query!(
"DELETE FROM file_key WHERE id = $1 AND file_id = $2",
Uuid::from(key_id),
Uuid::from(file_id),
)
.execute(&db)
.await?
.rows_affected()
{
1 => Ok(StatusCode::NO_CONTENT),
0 => Err(AppError::FileKeyNotFound(key_id)),
rows => Err(AppError::ImpossibleAffectedRows(rows)),
}
}

View file

@ -1,4 +1,3 @@
mod files;
mod links;
use axum::Router;
@ -6,7 +5,5 @@ use axum::Router;
use super::SharedState;
pub fn router() -> Router<SharedState> {
Router::new()
.merge(files::resource())
.merge(links::resource())
Router::new().merge(links::resource())
}

View file

@ -1,10 +1,12 @@
mod api;
mod pages;
mod root;
use std::sync::Arc;
use axum::{body::Body, Router};
use http::Request;
use memory_serve::{load_assets, MemoryServe};
use sqlx::{postgres::PgConnectOptions, PgPool};
use tower_http::trace::{DefaultOnRequest, DefaultOnResponse, TraceLayer};
use tracing::{field, span, Level};
@ -14,7 +16,7 @@ use crate::config::Config;
#[derive(Clone)]
struct SharedState {
db: PgPool,
config: Arc<Config>,
_config: Arc<Config>,
}
pub async fn build_app(config: Config) -> eyre::Result<Router> {
@ -27,11 +29,16 @@ pub async fn build_app(config: Config) -> eyre::Result<Router> {
)
.await?;
Ok(root::router()
let memory_router = MemoryServe::new(load_assets!("./static/")).into_router();
Ok(Router::new()
.merge(memory_router)
.merge(pages::router())
.nest("/api", api::router())
.merge(root::router())
.with_state(SharedState {
db,
config: Arc::new(config),
_config: Arc::new(config),
})
.layer(
TraceLayer::new_for_http()

View file

@ -0,0 +1,27 @@
mod pastes;
use axum::{routing::get, Router};
use maud::{html, Markup};
use super::{page, Breadcrumb, BC_INDEX};
use crate::app::SharedState;
pub(super) fn router() -> Router<SharedState> {
Router::new()
.route("/", get(show_admin_page()))
.nest("/pastes", pastes::router())
}
const BC_ADMIN: Breadcrumb = Breadcrumb::new_static("Admin", "/admin");
fn show_admin_page() -> Markup {
page(
"Admin",
&[BC_INDEX],
html! {
"meow :3"
p { a.tl href="/admin/pastes" { "Pastes" } }
p { a.tl href="/admin/pastes/new" { "Create Paste" } }
},
)
}

View file

@ -0,0 +1,113 @@
use axum::{
extract::{Query, State},
response::Redirect,
routing::get,
Form, Router,
};
use maud::{html, Markup};
use serde::Deserialize;
use sqlx::query;
use ulid::Ulid;
use uuid::Uuid;
use super::BC_ADMIN;
use crate::{
app::{
pages::{dt_iso, page, Breadcrumb, BC_INDEX},
SharedState,
},
error::AppError,
};
pub(super) fn router() -> Router<SharedState> {
Router::new()
.route("/", get(show_pastes).post(create_paste))
.route("/new", get(show_create_paste))
}
const BC_PASTES: Breadcrumb = Breadcrumb::new_static("Pastes", "/admin/pastes");
async fn show_pastes(
State(SharedState { db, .. }): State<SharedState>,
) -> Result<Markup, AppError> {
Ok(page(
"Pastes",
&[BC_INDEX, BC_ADMIN],
html! {
table class="w-full" {
thead {
tr {
th { "Name" }
th { "Created at" }
th { "Actions" }
}
}
tbody {
@for p in query!("SELECT id, title FROM paste").fetch_all(&db).await? {
tr {
td { a.tl href=(format!("/p/{}", Ulid::from(p.id))) { (p.title)} }
td { (dt_iso(Ulid::from(p.id).datetime())) }
td class="w-min-content" {
div class="flex gap-2" {
a.tl href="#" { "Edit" }
a.tl href="#" { "Delete" }
}
}
}
}
}
}
},
))
}
#[derive(Deserialize)]
struct CreatePasteFieldsOptional {
title: Option<String>,
content: Option<String>,
}
async fn show_create_paste(
Query(CreatePasteFieldsOptional { title, content }): Query<CreatePasteFieldsOptional>,
) -> Markup {
page(
"Create Paste",
&[BC_INDEX, BC_ADMIN, BC_PASTES],
html! {
form method="post" action="/admin/pastes" {
fieldset {
legend { "Paste" }
input class="w-full" type="text" name="title" placeholder="Paste title" required value=(title.unwrap_or_default());
textarea class="w-full min-h-32" name="content" placeholder="Paste content" required { (content.unwrap_or_default()) }
input type="submit" value="Create";
}
}
},
)
}
#[derive(Deserialize)]
struct CreatePasteFields {
title: String,
content: String,
}
async fn create_paste(
State(SharedState { db, .. }): State<SharedState>,
Form(CreatePasteFields { title, content }): Form<CreatePasteFields>,
) -> Result<Redirect, AppError> {
let id = Ulid::new();
match query!(
"INSERT INTO paste (id, title, content) VALUES ($1, $2, $3)",
Uuid::from(id),
title,
content,
)
.execute(&db)
.await?
.rows_affected()
{
1 => Ok(Redirect::temporary(&format!("/p/{id}"))),
r => Err(AppError::ImpossibleAffectedRows(r)),
}
}

78
src/app/pages/mod.rs Normal file
View file

@ -0,0 +1,78 @@
mod admin;
mod pastes_public;
use std::{borrow::Cow, time::SystemTime};
use axum::Router;
use chrono::{DateTime, Utc};
use maud::{html, Markup, DOCTYPE};
use super::SharedState;
pub fn router() -> Router<SharedState> {
Router::new()
.nest("/p", pastes_public::router())
.nest("/admin", admin::router())
}
const BC_INDEX: Breadcrumb = Breadcrumb::new_static("NCPN", "/");
struct Breadcrumb<'a> {
title: Cow<'a, str>,
href: Cow<'a, str>,
}
impl<'a> Breadcrumb<'a> {
fn new(title: impl Into<Cow<'a, str>>, href: impl Into<Cow<'a, str>>) -> Self {
Breadcrumb {
title: title.into(),
href: href.into(),
}
}
}
impl Breadcrumb<'static> {
const fn new_static(title: &'static str, href: &'static str) -> Self {
Breadcrumb {
title: Cow::Borrowed(title),
href: Cow::Borrowed(href),
}
}
}
fn page(title: &str, breadcrumbs: &[Breadcrumb], content: Markup) -> Markup {
html! {
(DOCTYPE)
html {
head {
meta charset="utf-8";
title { (format!("{title} | NCPN")) }
link rel="stylesheet" href="/style.css";
script src="/script.js" defer {}
}
body {
header {
div {
@for b in breadcrumbs {
a.tl href=(b.href) { (b.title) }
span { "/" }
}
}
h1 { (title) }
}
(content)
footer {
"© 2024 mrrp meow :3"
}
}
}
}
}
fn dt_iso(t: SystemTime) -> Markup {
let dt: DateTime<Utc> = t.into();
let iso = dt.to_rfc3339();
html! {
time datetime=(iso) { (iso) }
}
}

View file

@ -0,0 +1,42 @@
// 01HZT59RTH4R6P1TYE6NMAFYEP 018FF454-E351-260D-60EB-CE3568A7F9D6
use axum::{
extract::{Path, State},
routing::get,
Router,
};
use maud::{html, Markup};
use sqlx::query;
use ulid::Ulid;
use uuid::Uuid;
use super::{page, Breadcrumb, BC_INDEX};
use crate::{app::SharedState, error::AppError};
pub(super) fn router() -> Router<SharedState> {
Router::new().route("/:id", get(show_paste))
}
const BC_PASTES_PUBLIC: Breadcrumb = Breadcrumb::new_static("Pastes", "/p");
async fn show_paste(
Path(id): Path<Ulid>,
State(SharedState { db, .. }): State<SharedState>,
) -> Result<Markup, AppError> {
match query!(
"SELECT title, content FROM paste WHERE id = $1",
Uuid::from(id)
)
.fetch_optional(&db)
.await?
{
Some(r) => Ok(page(
&r.title,
&[BC_INDEX, BC_PASTES_PUBLIC],
html! {
pre class="p-4 border border-bd-base rounded-xl" { (r.content) }
},
)),
None => Err(AppError::PasteNotFound(id)),
}
}

View file

@ -1,26 +1,16 @@
use axum::{
body::Body,
extract::{Path, State},
response::Redirect,
routing::get,
BoxError, Router,
Router,
};
use bytes::Bytes;
use http::{Request, Response};
use http_body_util::{combinators::UnsyncBoxBody, BodyExt};
use mime::Mime;
use sqlx::query;
use tower_http::services::ServeFile;
use ulid::Ulid;
use uuid::Uuid;
use super::SharedState;
use crate::error::AppError;
pub fn router() -> Router<SharedState> {
Router::new()
.route("/:slug", get(redirect_link))
.route("/f/:key", get(download_file))
Router::new().route("/:slug", get(redirect_link))
}
async fn redirect_link(
@ -35,27 +25,3 @@ async fn redirect_link(
None => Err(AppError::LinkNotFoundSlug(slug)),
}
}
async fn download_file(
State(SharedState { db, config }): State<SharedState>,
Path(key): Path<Ulid>,
request: Request<Body>,
) -> Result<Response<UnsyncBoxBody<Bytes, BoxError>>, AppError> {
let file = query!(
"SELECT hash, mime FROM file_key JOIN file ON file_id = file.id WHERE file_key.id = $1",
Uuid::from(key),
)
.fetch_optional(&db)
.await?
.ok_or(AppError::FileKeyNotFound(key))?;
let mime: Option<Mime> = file.mime.parse().ok();
let path = config.file_store_dir.join(hex::encode(file.hash));
let mut sf = match mime {
Some(mime) => ServeFile::new_with_mime(path, &mime),
None => ServeFile::new(path),
};
match sf.try_call(request).await {
Ok(response) => Ok(response.map(|body| body.map_err(Into::into).boxed_unsync())),
Err(err) => Err(AppError::Io(err)),
}
}

View file

@ -1,5 +1,3 @@
use std::path::PathBuf;
use axum::{body::Body, response::IntoResponse};
use http::StatusCode;
use tracing::{error, field};
@ -13,12 +11,8 @@ pub enum AppError {
LinkNotFoundId(Ulid),
#[error("link not found ({0})")]
LinkNotFoundSlug(String),
#[error("file not found ({0})")]
FileNotFound(Ulid),
#[error("file key not found ({0})")]
FileKeyNotFound(Ulid),
#[error("file is missing ({0})")]
FileMissing(PathBuf),
#[error("paste not found ({0})")]
PasteNotFound(Ulid),
#[error("database returned an impossible number of affected rows ({0})")]
ImpossibleAffectedRows(u64),
#[error("ulid conflict")]
@ -39,9 +33,7 @@ impl IntoResponse for AppError {
Self::LinkNotFoundId(_) | Self::LinkNotFoundSlug(_) => {
(StatusCode::NOT_FOUND, "Link not found")
}
Self::FileNotFound(_) => (StatusCode::NOT_FOUND, "File not found"),
Self::FileKeyNotFound(_) => (StatusCode::NOT_FOUND, "File key not found"),
Self::FileMissing(_) => (StatusCode::INTERNAL_SERVER_ERROR, "File is missing"),
Self::PasteNotFound(_) => (StatusCode::NOT_FOUND, "Paste not found"),
Self::ImpossibleAffectedRows(_) => (
StatusCode::INTERNAL_SERVER_ERROR,
"Database returned an impossible number of affected rows",

50
src/style.scss Normal file
View file

@ -0,0 +1,50 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
body {
@apply flex flex-col gap-8 px-4 py-16 mx-auto max-w-4xl bg-slate-900 text-fg-base max-lg:py-8;
}
header {
div {
@apply flex gap-2 mb-1 text-lg text-fg-deemphasized;
}
h1 {
@apply text-5xl font-extrabold text-fg-headings;
}
}
footer {
@apply text-center text-fg-deemphasized;
}
a.tl {
@apply underline hover:no-underline hover:text-white focus-visible:no-underline focus-visible:text-white;
}
table {
@apply table-auto border-collapse border border-bd-base;
thead tr th {
@apply border border-bd-base px-2 py-1 text-left;
}
tbody tr td {
@apply border border-bd-base px-2 py-1;
}
}
form fieldset {
@apply flex flex-col p-4 pt-2 gap-4 border border-bd-base rounded-xl;
input[type="text"],
textarea {
@apply px-3 py-2 rounded-lg bg-transparent border border-bd-base hover:border-bd-highlighted focus-visible:border-bd-highlighted;
}
input[type="submit"] {
@apply ml-auto px-3 py-2 rounded-lg cursor-pointer border border-bd-base hover:border-bd-highlighted focus-visible:border-bd-highlighted;
}
}

8
static/script.js Normal file
View file

@ -0,0 +1,8 @@
document
.querySelectorAll("time[datetime]")
.forEach(
(e) =>
(e.innerText = new Date(
e.getAttribute("datetime")
).toLocaleString())
);

795
static/style.css Normal file
View file

@ -0,0 +1,795 @@
/*
! tailwindcss v3.4.3 | MIT License | https://tailwindcss.com
*/
/*
1. Prevent padding and border from affecting element width. (https://github.com/mozdevs/cssremedy/issues/4)
2. Allow adding a border to an element by just adding a border-width. (https://github.com/tailwindcss/tailwindcss/pull/116)
*/
*,
::before,
::after {
box-sizing: border-box;
/* 1 */
border-width: 0;
/* 2 */
border-style: solid;
/* 2 */
border-color: #e5e7eb;
/* 2 */
}
::before,
::after {
--tw-content: '';
}
/*
1. Use a consistent sensible line-height in all browsers.
2. Prevent adjustments of font size after orientation changes in iOS.
3. Use a more readable tab size.
4. Use the user's configured `sans` font-family by default.
5. Use the user's configured `sans` font-feature-settings by default.
6. Use the user's configured `sans` font-variation-settings by default.
7. Disable tap highlights on iOS
*/
html,
:host {
line-height: 1.5;
/* 1 */
-webkit-text-size-adjust: 100%;
/* 2 */
-moz-tab-size: 4;
/* 3 */
-o-tab-size: 4;
tab-size: 4;
/* 3 */
font-family: sans-serif;
/* 4 */
font-feature-settings: normal;
/* 5 */
font-variation-settings: normal;
/* 6 */
-webkit-tap-highlight-color: transparent;
/* 7 */
}
/*
1. Remove the margin in all browsers.
2. Inherit line-height from `html` so users can set them as a class directly on the `html` element.
*/
body {
margin: 0;
/* 1 */
line-height: inherit;
/* 2 */
}
/*
1. Add the correct height in Firefox.
2. Correct the inheritance of border color in Firefox. (https://bugzilla.mozilla.org/show_bug.cgi?id=190655)
3. Ensure horizontal rules are visible by default.
*/
hr {
height: 0;
/* 1 */
color: inherit;
/* 2 */
border-top-width: 1px;
/* 3 */
}
/*
Add the correct text decoration in Chrome, Edge, and Safari.
*/
abbr:where([title]) {
-webkit-text-decoration: underline dotted;
text-decoration: underline dotted;
}
/*
Remove the default font size and weight for headings.
*/
h1,
h2,
h3,
h4,
h5,
h6 {
font-size: inherit;
font-weight: inherit;
}
/*
Reset links to optimize for opt-in styling instead of opt-out.
*/
a {
color: inherit;
text-decoration: inherit;
}
/*
Add the correct font weight in Edge and Safari.
*/
b,
strong {
font-weight: bolder;
}
/*
1. Use the user's configured `mono` font-family by default.
2. Use the user's configured `mono` font-feature-settings by default.
3. Use the user's configured `mono` font-variation-settings by default.
4. Correct the odd `em` font sizing in all browsers.
*/
code,
kbd,
samp,
pre {
font-family: monospace;
/* 1 */
font-feature-settings: normal;
/* 2 */
font-variation-settings: normal;
/* 3 */
font-size: 1em;
/* 4 */
}
/*
Add the correct font size in all browsers.
*/
small {
font-size: 80%;
}
/*
Prevent `sub` and `sup` elements from affecting the line height in all browsers.
*/
sub,
sup {
font-size: 75%;
line-height: 0;
position: relative;
vertical-align: baseline;
}
sub {
bottom: -0.25em;
}
sup {
top: -0.5em;
}
/*
1. Remove text indentation from table contents in Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=999088, https://bugs.webkit.org/show_bug.cgi?id=201297)
2. Correct table border color inheritance in all Chrome and Safari. (https://bugs.chromium.org/p/chromium/issues/detail?id=935729, https://bugs.webkit.org/show_bug.cgi?id=195016)
3. Remove gaps between table borders by default.
*/
table {
text-indent: 0;
/* 1 */
border-color: inherit;
/* 2 */
border-collapse: collapse;
/* 3 */
}
/*
1. Change the font styles in all browsers.
2. Remove the margin in Firefox and Safari.
3. Remove default padding in all browsers.
*/
button,
input,
optgroup,
select,
textarea {
font-family: inherit;
/* 1 */
font-feature-settings: inherit;
/* 1 */
font-variation-settings: inherit;
/* 1 */
font-size: 100%;
/* 1 */
font-weight: inherit;
/* 1 */
line-height: inherit;
/* 1 */
letter-spacing: inherit;
/* 1 */
color: inherit;
/* 1 */
margin: 0;
/* 2 */
padding: 0;
/* 3 */
}
/*
Remove the inheritance of text transform in Edge and Firefox.
*/
button,
select {
text-transform: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Remove default button styles.
*/
button,
input:where([type='button']),
input:where([type='reset']),
input:where([type='submit']) {
-webkit-appearance: button;
/* 1 */
background-color: transparent;
/* 2 */
background-image: none;
/* 2 */
}
/*
Use the modern Firefox focus style for all focusable elements.
*/
:-moz-focusring {
outline: auto;
}
/*
Remove the additional `:invalid` styles in Firefox. (https://github.com/mozilla/gecko-dev/blob/2f9eacd9d3d995c937b4251a5557d95d494c9be1/layout/style/res/forms.css#L728-L737)
*/
:-moz-ui-invalid {
box-shadow: none;
}
/*
Add the correct vertical alignment in Chrome and Firefox.
*/
progress {
vertical-align: baseline;
}
/*
Correct the cursor style of increment and decrement buttons in Safari.
*/
::-webkit-inner-spin-button,
::-webkit-outer-spin-button {
height: auto;
}
/*
1. Correct the odd appearance in Chrome and Safari.
2. Correct the outline style in Safari.
*/
[type='search'] {
-webkit-appearance: textfield;
/* 1 */
outline-offset: -2px;
/* 2 */
}
/*
Remove the inner padding in Chrome and Safari on macOS.
*/
::-webkit-search-decoration {
-webkit-appearance: none;
}
/*
1. Correct the inability to style clickable types in iOS and Safari.
2. Change font properties to `inherit` in Safari.
*/
::-webkit-file-upload-button {
-webkit-appearance: button;
/* 1 */
font: inherit;
/* 2 */
}
/*
Add the correct display in Chrome and Safari.
*/
summary {
display: list-item;
}
/*
Removes the default spacing and border for appropriate elements.
*/
blockquote,
dl,
dd,
h1,
h2,
h3,
h4,
h5,
h6,
hr,
figure,
p,
pre {
margin: 0;
}
fieldset {
margin: 0;
padding: 0;
}
legend {
padding: 0;
}
ol,
ul,
menu {
list-style: none;
margin: 0;
padding: 0;
}
/*
Reset default styling for dialogs.
*/
dialog {
padding: 0;
}
/*
Prevent resizing textareas horizontally by default.
*/
textarea {
resize: vertical;
}
/*
1. Reset the default placeholder opacity in Firefox. (https://github.com/tailwindlabs/tailwindcss/issues/3300)
2. Set the default placeholder color to the user's configured gray 400 color.
*/
input::-moz-placeholder, textarea::-moz-placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
input::placeholder,
textarea::placeholder {
opacity: 1;
/* 1 */
color: #9ca3af;
/* 2 */
}
/*
Set the default cursor for buttons.
*/
button,
[role="button"] {
cursor: pointer;
}
/*
Make sure disabled buttons don't get the pointer cursor.
*/
:disabled {
cursor: default;
}
/*
1. Make replaced elements `display: block` by default. (https://github.com/mozdevs/cssremedy/issues/14)
2. Add `vertical-align: middle` to align replaced elements more sensibly by default. (https://github.com/jensimmons/cssremedy/issues/14#issuecomment-634934210)
This can trigger a poorly considered lint error in some tools but is included by design.
*/
img,
svg,
video,
canvas,
audio,
iframe,
embed,
object {
display: block;
/* 1 */
vertical-align: middle;
/* 2 */
}
/*
Constrain images and videos to the parent width and preserve their intrinsic aspect ratio. (https://github.com/mozdevs/cssremedy/issues/14)
*/
img,
video {
max-width: 100%;
height: auto;
}
/* Make elements with the HTML hidden attribute stay hidden by default */
[hidden] {
display: none;
}
*, ::before, ::after {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
--tw-contain-size: ;
--tw-contain-layout: ;
--tw-contain-paint: ;
--tw-contain-style: ;
}
::backdrop {
--tw-border-spacing-x: 0;
--tw-border-spacing-y: 0;
--tw-translate-x: 0;
--tw-translate-y: 0;
--tw-rotate: 0;
--tw-skew-x: 0;
--tw-skew-y: 0;
--tw-scale-x: 1;
--tw-scale-y: 1;
--tw-pan-x: ;
--tw-pan-y: ;
--tw-pinch-zoom: ;
--tw-scroll-snap-strictness: proximity;
--tw-gradient-from-position: ;
--tw-gradient-via-position: ;
--tw-gradient-to-position: ;
--tw-ordinal: ;
--tw-slashed-zero: ;
--tw-numeric-figure: ;
--tw-numeric-spacing: ;
--tw-numeric-fraction: ;
--tw-ring-inset: ;
--tw-ring-offset-width: 0px;
--tw-ring-offset-color: #fff;
--tw-ring-color: rgb(59 130 246 / 0.5);
--tw-ring-offset-shadow: 0 0 #0000;
--tw-ring-shadow: 0 0 #0000;
--tw-shadow: 0 0 #0000;
--tw-shadow-colored: 0 0 #0000;
--tw-blur: ;
--tw-brightness: ;
--tw-contrast: ;
--tw-grayscale: ;
--tw-hue-rotate: ;
--tw-invert: ;
--tw-saturate: ;
--tw-sepia: ;
--tw-drop-shadow: ;
--tw-backdrop-blur: ;
--tw-backdrop-brightness: ;
--tw-backdrop-contrast: ;
--tw-backdrop-grayscale: ;
--tw-backdrop-hue-rotate: ;
--tw-backdrop-invert: ;
--tw-backdrop-opacity: ;
--tw-backdrop-saturate: ;
--tw-backdrop-sepia: ;
--tw-contain-size: ;
--tw-contain-layout: ;
--tw-contain-paint: ;
--tw-contain-style: ;
}
.static {
position: static;
}
.flex {
display: flex;
}
.table {
display: table;
}
.min-h-32 {
min-height: 8rem;
}
.w-full {
width: 100%;
}
.gap-2 {
gap: 0.5rem;
}
.rounded-xl {
border-radius: 0.75rem;
}
.border {
border-width: 1px;
}
.border-bd-base {
--tw-border-opacity: 1;
border-color: rgb(51 65 85 / var(--tw-border-opacity));
}
.p-4 {
padding: 1rem;
}
body {
margin-left: auto;
margin-right: auto;
display: flex;
max-width: 56rem;
flex-direction: column;
gap: 2rem;
--tw-bg-opacity: 1;
background-color: rgb(15 23 42 / var(--tw-bg-opacity));
padding-left: 1rem;
padding-right: 1rem;
padding-top: 4rem;
padding-bottom: 4rem;
--tw-text-opacity: 1;
color: rgb(203 213 225 / var(--tw-text-opacity));
}
@media not all and (min-width: 1024px) {
body {
padding-top: 2rem;
padding-bottom: 2rem;
}
}
header {
div {
margin-bottom: 0.25rem;
}
div {
display: flex;
}
div {
gap: 0.5rem;
}
div {
font-size: 1.125rem;
line-height: 1.75rem;
}
div {
--tw-text-opacity: 1;
color: rgb(148 163 184 / var(--tw-text-opacity));
}
h1 {
font-size: 3rem;
line-height: 1;
}
h1 {
font-weight: 800;
}
h1 {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
}
}
footer {
text-align: center;
--tw-text-opacity: 1;
color: rgb(148 163 184 / var(--tw-text-opacity));
}
a.tl {
text-decoration-line: underline;
}
a.tl:hover {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
text-decoration-line: none;
}
a.tl:focus-visible {
--tw-text-opacity: 1;
color: rgb(255 255 255 / var(--tw-text-opacity));
text-decoration-line: none;
}
table {
table-layout: auto;
border-collapse: collapse;
border-width: 1px;
--tw-border-opacity: 1;
border-color: rgb(51 65 85 / var(--tw-border-opacity));
thead tr th {
border-width: 1px;
}
thead tr th {
--tw-border-opacity: 1;
border-color: rgb(51 65 85 / var(--tw-border-opacity));
}
thead tr th {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
thead tr th {
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
thead tr th {
text-align: left;
}
tbody tr td {
border-width: 1px;
}
tbody tr td {
--tw-border-opacity: 1;
border-color: rgb(51 65 85 / var(--tw-border-opacity));
}
tbody tr td {
padding-left: 0.5rem;
padding-right: 0.5rem;
}
tbody tr td {
padding-top: 0.25rem;
padding-bottom: 0.25rem;
}
}
form fieldset {
display: flex;
flex-direction: column;
gap: 1rem;
border-radius: 0.75rem;
border-width: 1px;
--tw-border-opacity: 1;
border-color: rgb(51 65 85 / var(--tw-border-opacity));
padding: 1rem;
padding-top: 0.5rem;
input[type="text"],
textarea {
border-radius: 0.5rem;
}
input[type="text"],
textarea {
border-width: 1px;
}
input[type="text"],
textarea {
--tw-border-opacity: 1;
border-color: rgb(51 65 85 / var(--tw-border-opacity));
}
input[type="text"],
textarea {
background-color: transparent;
}
input[type="text"],
textarea {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
input[type="text"],
textarea {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
input[type="text"]:hover,
textarea:hover {
--tw-border-opacity: 1;
border-color: rgb(100 116 139 / var(--tw-border-opacity));
}
input[type="text"]:focus-visible,
textarea:focus-visible {
--tw-border-opacity: 1;
border-color: rgb(100 116 139 / var(--tw-border-opacity));
}
input[type="submit"] {
margin-left: auto;
}
input[type="submit"] {
cursor: pointer;
}
input[type="submit"] {
border-radius: 0.5rem;
}
input[type="submit"] {
border-width: 1px;
}
input[type="submit"] {
--tw-border-opacity: 1;
border-color: rgb(51 65 85 / var(--tw-border-opacity));
}
input[type="submit"] {
padding-left: 0.75rem;
padding-right: 0.75rem;
}
input[type="submit"] {
padding-top: 0.5rem;
padding-bottom: 0.5rem;
}
input[type="submit"]:hover {
--tw-border-opacity: 1;
border-color: rgb(100 116 139 / var(--tw-border-opacity));
}
input[type="submit"]:focus-visible {
--tw-border-opacity: 1;
border-color: rgb(100 116 139 / var(--tw-border-opacity));
}
}

34
tailwind.config.js Normal file
View file

@ -0,0 +1,34 @@
/** @type {import("tailwindcss/colors")} */
const colors = require("tailwindcss/colors");
/** @type {import("tailwindcss").Config} */
module.exports = {
content: ["./src/app/pages/**/*.rs"],
theme: {
fontFamily: {
sans: ["sans-serif"],
mono: ["monospace"],
},
extend: {
colors: {
fg: {
base: colors.slate[300],
deemphasized: colors.slate[400],
headings: colors.white,
},
bg: {
base: colors.slate[900],
raised: {
// backgrounds
1: colors.slate[800],
2: colors.slate[700],
},
},
bd: {
base: colors.slate[700],
highlighted: colors.slate[500],
},
},
},
},
};