We have a web UI now, I guess
This commit is contained in:
parent
75b87e7bac
commit
331423d3f6
3
.prettierrc
Normal file
3
.prettierrc
Normal file
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"tabWidth": 4
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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"
|
||||
}
|
|
@ -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
303
Cargo.lock
generated
|
@ -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"
|
||||
|
|
25
Cargo.toml
25
Cargo.toml
|
@ -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
|
||||
|
|
18
flake.lock
18
flake.lock
|
@ -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": {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
);
|
||||
|
|
|
@ -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)),
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
|
|
|
@ -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()
|
||||
|
|
27
src/app/pages/admin/mod.rs
Normal file
27
src/app/pages/admin/mod.rs
Normal 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" } }
|
||||
},
|
||||
)
|
||||
}
|
113
src/app/pages/admin/pastes.rs
Normal file
113
src/app/pages/admin/pastes.rs
Normal 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
78
src/app/pages/mod.rs
Normal 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) }
|
||||
}
|
||||
}
|
42
src/app/pages/pastes_public.rs
Normal file
42
src/app/pages/pastes_public.rs
Normal 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)),
|
||||
}
|
||||
}
|
|
@ -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)),
|
||||
}
|
||||
}
|
||||
|
|
14
src/error.rs
14
src/error.rs
|
@ -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
50
src/style.scss
Normal 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
8
static/script.js
Normal 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
795
static/style.css
Normal 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
34
tailwind.config.js
Normal 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],
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
Loading…
Reference in a new issue