Compare commits
	
		
			5 Commits 
		
	
	
		
			5112a6ce35
			...
			82ccad196c
		
	
	| Author | SHA1 | Date | 
|---|---|---|
| 
							
							
								
									
								
								 | 
						82ccad196c | |
| 
							
							
								
									
								
								 | 
						3071685950 | |
| 
							
							
								
									
								
								 | 
						b3e49be299 | |
| 
							
							
								
									
								
								 | 
						ad015b47e4 | |
| 
							
							
								
									
								
								 | 
						2c44f788d9 | 
| 
						 | 
					@ -213,6 +213,21 @@ dependencies = [
 | 
				
			||||||
 "syn",
 | 
					 "syn",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "axum-range"
 | 
				
			||||||
 | 
					version = "0.5.0"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "e8b09d24c2cfcf6596afc4b9d139ad62c53637c7e0f791ef8a25ce1cc431f73a"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "axum",
 | 
				
			||||||
 | 
					 "axum-extra",
 | 
				
			||||||
 | 
					 "bytes",
 | 
				
			||||||
 | 
					 "futures",
 | 
				
			||||||
 | 
					 "http-body",
 | 
				
			||||||
 | 
					 "pin-project",
 | 
				
			||||||
 | 
					 "tokio",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "backtrace"
 | 
					name = "backtrace"
 | 
				
			||||||
version = "0.3.74"
 | 
					version = "0.3.74"
 | 
				
			||||||
| 
						 | 
					@ -264,6 +279,16 @@ dependencies = [
 | 
				
			||||||
 "generic-array",
 | 
					 "generic-array",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "bstr"
 | 
				
			||||||
 | 
					version = "1.11.3"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "531a9155a481e2ee699d4f98f43c0ca4ff8ee1bfd55c31e9e98fb29d2b176fe0"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "memchr",
 | 
				
			||||||
 | 
					 "serde",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "bumpalo"
 | 
					name = "bumpalo"
 | 
				
			||||||
version = "3.17.0"
 | 
					version = "3.17.0"
 | 
				
			||||||
| 
						 | 
					@ -324,6 +349,28 @@ dependencies = [
 | 
				
			||||||
 "windows-targets",
 | 
					 "windows-targets",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "chrono-tz"
 | 
				
			||||||
 | 
					version = "0.9.0"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "93698b29de5e97ad0ae26447b344c482a7284c737d9ddc5f9e52b74a336671bb"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "chrono",
 | 
				
			||||||
 | 
					 "chrono-tz-build",
 | 
				
			||||||
 | 
					 "phf",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "chrono-tz-build"
 | 
				
			||||||
 | 
					version = "0.3.0"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "0c088aee841df9c3041febbb73934cfc39708749bf96dc827e3359cd39ef11b1"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "parse-zoneinfo",
 | 
				
			||||||
 | 
					 "phf",
 | 
				
			||||||
 | 
					 "phf_codegen",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "ciborium"
 | 
					name = "ciborium"
 | 
				
			||||||
version = "0.2.2"
 | 
					version = "0.2.2"
 | 
				
			||||||
| 
						 | 
					@ -544,6 +591,12 @@ dependencies = [
 | 
				
			||||||
 "powerfmt",
 | 
					 "powerfmt",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "deunicode"
 | 
				
			||||||
 | 
					version = "1.6.1"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "dc55fe0d1f6c107595572ec8b107c0999bb1a2e0b75e37429a4fb0d6474a0e7d"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "diesel"
 | 
					name = "diesel"
 | 
				
			||||||
version = "2.2.7"
 | 
					version = "2.2.7"
 | 
				
			||||||
| 
						 | 
					@ -655,6 +708,20 @@ dependencies = [
 | 
				
			||||||
 "percent-encoding",
 | 
					 "percent-encoding",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "futures"
 | 
				
			||||||
 | 
					version = "0.3.31"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "futures-channel",
 | 
				
			||||||
 | 
					 "futures-core",
 | 
				
			||||||
 | 
					 "futures-io",
 | 
				
			||||||
 | 
					 "futures-sink",
 | 
				
			||||||
 | 
					 "futures-task",
 | 
				
			||||||
 | 
					 "futures-util",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "futures-channel"
 | 
					name = "futures-channel"
 | 
				
			||||||
version = "0.3.31"
 | 
					version = "0.3.31"
 | 
				
			||||||
| 
						 | 
					@ -662,6 +729,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
 | 
					checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "futures-core",
 | 
					 "futures-core",
 | 
				
			||||||
 | 
					 "futures-sink",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
| 
						 | 
					@ -670,6 +738,18 @@ version = "0.3.31"
 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
 | 
					checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "futures-io"
 | 
				
			||||||
 | 
					version = "0.3.31"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "futures-sink"
 | 
				
			||||||
 | 
					version = "0.3.31"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "futures-task"
 | 
					name = "futures-task"
 | 
				
			||||||
version = "0.3.31"
 | 
					version = "0.3.31"
 | 
				
			||||||
| 
						 | 
					@ -682,10 +762,15 @@ version = "0.3.31"
 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
 | 
					checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "futures-channel",
 | 
				
			||||||
 "futures-core",
 | 
					 "futures-core",
 | 
				
			||||||
 | 
					 "futures-io",
 | 
				
			||||||
 | 
					 "futures-sink",
 | 
				
			||||||
 "futures-task",
 | 
					 "futures-task",
 | 
				
			||||||
 | 
					 "memchr",
 | 
				
			||||||
 "pin-project-lite",
 | 
					 "pin-project-lite",
 | 
				
			||||||
 "pin-utils",
 | 
					 "pin-utils",
 | 
				
			||||||
 | 
					 "slab",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
| 
						 | 
					@ -715,6 +800,30 @@ version = "0.31.1"
 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
 | 
					checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "globset"
 | 
				
			||||||
 | 
					version = "0.4.16"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "54a1028dfc5f5df5da8a56a73e6c153c9a9708ec57232470703592a3f18e49f5"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "aho-corasick",
 | 
				
			||||||
 | 
					 "bstr",
 | 
				
			||||||
 | 
					 "log",
 | 
				
			||||||
 | 
					 "regex-automata",
 | 
				
			||||||
 | 
					 "regex-syntax",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "globwalk"
 | 
				
			||||||
 | 
					version = "0.9.1"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "bitflags",
 | 
				
			||||||
 | 
					 "ignore",
 | 
				
			||||||
 | 
					 "walkdir",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "gpodder"
 | 
					name = "gpodder"
 | 
				
			||||||
version = "0.1.0"
 | 
					version = "0.1.0"
 | 
				
			||||||
| 
						 | 
					@ -836,6 +945,15 @@ version = "1.0.3"
 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
 | 
					checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "humansize"
 | 
				
			||||||
 | 
					version = "2.1.3"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "6cb51c9a029ddc91b07a787f1d86b53ccfa49b0e86688c946ebe8d3555685dd7"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "libm",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "hyper"
 | 
					name = "hyper"
 | 
				
			||||||
version = "1.6.0"
 | 
					version = "1.6.0"
 | 
				
			||||||
| 
						 | 
					@ -900,6 +1018,22 @@ version = "1.0.1"
 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
 | 
					checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "ignore"
 | 
				
			||||||
 | 
					version = "0.4.23"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "6d89fd380afde86567dfba715db065673989d6253f42b88179abd3eae47bda4b"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "crossbeam-deque",
 | 
				
			||||||
 | 
					 "globset",
 | 
				
			||||||
 | 
					 "log",
 | 
				
			||||||
 | 
					 "memchr",
 | 
				
			||||||
 | 
					 "regex-automata",
 | 
				
			||||||
 | 
					 "same-file",
 | 
				
			||||||
 | 
					 "walkdir",
 | 
				
			||||||
 | 
					 "winapi-util",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "indexmap"
 | 
					name = "indexmap"
 | 
				
			||||||
version = "2.7.1"
 | 
					version = "2.7.1"
 | 
				
			||||||
| 
						 | 
					@ -970,6 +1104,12 @@ version = "0.2.170"
 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828"
 | 
					checksum = "875b3680cb2f8f71bdcf9a30f38d48282f5d3c95cbf9b3fa57269bb5d5c06828"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "libm"
 | 
				
			||||||
 | 
					version = "0.2.11"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "8355be11b20d696c8f18f6cc018c4e372165b1fa8126cef092399c9951984ffa"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "libsqlite3-sys"
 | 
					name = "libsqlite3-sys"
 | 
				
			||||||
version = "0.31.0"
 | 
					version = "0.31.0"
 | 
				
			||||||
| 
						 | 
					@ -1108,6 +1248,7 @@ version = "0.1.0"
 | 
				
			||||||
dependencies = [
 | 
					dependencies = [
 | 
				
			||||||
 "axum",
 | 
					 "axum",
 | 
				
			||||||
 "axum-extra",
 | 
					 "axum-extra",
 | 
				
			||||||
 | 
					 "axum-range",
 | 
				
			||||||
 "chrono",
 | 
					 "chrono",
 | 
				
			||||||
 "clap",
 | 
					 "clap",
 | 
				
			||||||
 "cookie",
 | 
					 "cookie",
 | 
				
			||||||
| 
						 | 
					@ -1117,6 +1258,7 @@ dependencies = [
 | 
				
			||||||
 "http-body-util",
 | 
					 "http-body-util",
 | 
				
			||||||
 "rand",
 | 
					 "rand",
 | 
				
			||||||
 "serde",
 | 
					 "serde",
 | 
				
			||||||
 | 
					 "tera",
 | 
				
			||||||
 "tokio",
 | 
					 "tokio",
 | 
				
			||||||
 "tower-http",
 | 
					 "tower-http",
 | 
				
			||||||
 "tracing",
 | 
					 "tracing",
 | 
				
			||||||
| 
						 | 
					@ -1152,6 +1294,15 @@ dependencies = [
 | 
				
			||||||
 "windows-targets",
 | 
					 "windows-targets",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "parse-zoneinfo"
 | 
				
			||||||
 | 
					version = "0.3.1"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "1f2a05b18d44e2957b88f96ba460715e295bc1d7510468a2f3d3b44535d26c24"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "regex",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "password-hash"
 | 
					name = "password-hash"
 | 
				
			||||||
version = "0.5.0"
 | 
					version = "0.5.0"
 | 
				
			||||||
| 
						 | 
					@ -1192,6 +1343,109 @@ version = "2.3.1"
 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
 | 
					checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "pest"
 | 
				
			||||||
 | 
					version = "2.8.0"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "198db74531d58c70a361c42201efde7e2591e976d518caf7662a47dc5720e7b6"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "memchr",
 | 
				
			||||||
 | 
					 "thiserror",
 | 
				
			||||||
 | 
					 "ucd-trie",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "pest_derive"
 | 
				
			||||||
 | 
					version = "2.8.0"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "d725d9cfd79e87dccc9341a2ef39d1b6f6353d68c4b33c177febbe1a402c97c5"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "pest",
 | 
				
			||||||
 | 
					 "pest_generator",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "pest_generator"
 | 
				
			||||||
 | 
					version = "2.8.0"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "db7d01726be8ab66ab32f9df467ae8b1148906685bbe75c82d1e65d7f5b3f841"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "pest",
 | 
				
			||||||
 | 
					 "pest_meta",
 | 
				
			||||||
 | 
					 "proc-macro2",
 | 
				
			||||||
 | 
					 "quote",
 | 
				
			||||||
 | 
					 "syn",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "pest_meta"
 | 
				
			||||||
 | 
					version = "2.8.0"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "7f9f832470494906d1fca5329f8ab5791cc60beb230c74815dff541cbd2b5ca0"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "once_cell",
 | 
				
			||||||
 | 
					 "pest",
 | 
				
			||||||
 | 
					 "sha2",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "phf"
 | 
				
			||||||
 | 
					version = "0.11.3"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "1fd6780a80ae0c52cc120a26a1a42c1ae51b247a253e4e06113d23d2c2edd078"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "phf_shared",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "phf_codegen"
 | 
				
			||||||
 | 
					version = "0.11.3"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "aef8048c789fa5e851558d709946d6d79a8ff88c0440c587967f8e94bfb1216a"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "phf_generator",
 | 
				
			||||||
 | 
					 "phf_shared",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "phf_generator"
 | 
				
			||||||
 | 
					version = "0.11.3"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "3c80231409c20246a13fddb31776fb942c38553c51e871f8cbd687a4cfb5843d"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "phf_shared",
 | 
				
			||||||
 | 
					 "rand",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "phf_shared"
 | 
				
			||||||
 | 
					version = "0.11.3"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "67eabc2ef2a60eb7faa00097bd1ffdb5bd28e62bf39990626a582201b7a754e5"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "siphasher",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "pin-project"
 | 
				
			||||||
 | 
					version = "1.1.10"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "pin-project-internal",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "pin-project-internal"
 | 
				
			||||||
 | 
					version = "1.1.10"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "proc-macro2",
 | 
				
			||||||
 | 
					 "quote",
 | 
				
			||||||
 | 
					 "syn",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "pin-project-lite"
 | 
					name = "pin-project-lite"
 | 
				
			||||||
version = "0.2.16"
 | 
					version = "0.2.16"
 | 
				
			||||||
| 
						 | 
					@ -1499,6 +1753,17 @@ dependencies = [
 | 
				
			||||||
 "digest",
 | 
					 "digest",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "sha2"
 | 
				
			||||||
 | 
					version = "0.10.8"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "cfg-if",
 | 
				
			||||||
 | 
					 "cpufeatures",
 | 
				
			||||||
 | 
					 "digest",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "sharded-slab"
 | 
					name = "sharded-slab"
 | 
				
			||||||
version = "0.1.7"
 | 
					version = "0.1.7"
 | 
				
			||||||
| 
						 | 
					@ -1523,6 +1788,31 @@ dependencies = [
 | 
				
			||||||
 "libc",
 | 
					 "libc",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "siphasher"
 | 
				
			||||||
 | 
					version = "1.0.1"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "56199f7ddabf13fe5074ce809e7d3f42b42ae711800501b5b16ea82ad029c39d"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "slab"
 | 
				
			||||||
 | 
					version = "0.4.9"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "autocfg",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "slug"
 | 
				
			||||||
 | 
					version = "0.1.6"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "882a80f72ee45de3cc9a5afeb2da0331d58df69e4e7d8eeb5d3c7784ae67e724"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "deunicode",
 | 
				
			||||||
 | 
					 "wasm-bindgen",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "smallvec"
 | 
					name = "smallvec"
 | 
				
			||||||
version = "1.14.0"
 | 
					version = "1.14.0"
 | 
				
			||||||
| 
						 | 
					@ -1568,6 +1858,48 @@ version = "1.0.2"
 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
 | 
					checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "tera"
 | 
				
			||||||
 | 
					version = "1.20.0"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "ab9d851b45e865f178319da0abdbfe6acbc4328759ff18dafc3a41c16b4cd2ee"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "chrono",
 | 
				
			||||||
 | 
					 "chrono-tz",
 | 
				
			||||||
 | 
					 "globwalk",
 | 
				
			||||||
 | 
					 "humansize",
 | 
				
			||||||
 | 
					 "lazy_static",
 | 
				
			||||||
 | 
					 "percent-encoding",
 | 
				
			||||||
 | 
					 "pest",
 | 
				
			||||||
 | 
					 "pest_derive",
 | 
				
			||||||
 | 
					 "rand",
 | 
				
			||||||
 | 
					 "regex",
 | 
				
			||||||
 | 
					 "serde",
 | 
				
			||||||
 | 
					 "serde_json",
 | 
				
			||||||
 | 
					 "slug",
 | 
				
			||||||
 | 
					 "unic-segment",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "thiserror"
 | 
				
			||||||
 | 
					version = "2.0.12"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "thiserror-impl",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "thiserror-impl"
 | 
				
			||||||
 | 
					version = "2.0.12"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "proc-macro2",
 | 
				
			||||||
 | 
					 "quote",
 | 
				
			||||||
 | 
					 "syn",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "thread_local"
 | 
					name = "thread_local"
 | 
				
			||||||
version = "1.1.8"
 | 
					version = "1.1.8"
 | 
				
			||||||
| 
						 | 
					@ -1790,6 +2122,12 @@ version = "1.18.0"
 | 
				
			||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
 | 
					checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "ucd-trie"
 | 
				
			||||||
 | 
					version = "0.1.7"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "2896d95c02a80c6d6a5d6e953d479f5ddf2dfdb6a244441010e373ac0fb88971"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "uncased"
 | 
					name = "uncased"
 | 
				
			||||||
version = "0.9.10"
 | 
					version = "0.9.10"
 | 
				
			||||||
| 
						 | 
					@ -1799,6 +2137,56 @@ dependencies = [
 | 
				
			||||||
 "version_check",
 | 
					 "version_check",
 | 
				
			||||||
]
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "unic-char-property"
 | 
				
			||||||
 | 
					version = "0.9.0"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "a8c57a407d9b6fa02b4795eb81c5b6652060a15a7903ea981f3d723e6c0be221"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "unic-char-range",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "unic-char-range"
 | 
				
			||||||
 | 
					version = "0.9.0"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "0398022d5f700414f6b899e10b8348231abf9173fa93144cbc1a43b9793c1fbc"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "unic-common"
 | 
				
			||||||
 | 
					version = "0.9.0"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "80d7ff825a6a654ee85a63e80f92f054f904f21e7d12da4e22f9834a4aaa35bc"
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "unic-segment"
 | 
				
			||||||
 | 
					version = "0.9.0"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "e4ed5d26be57f84f176157270c112ef57b86debac9cd21daaabbe56db0f88f23"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "unic-ucd-segment",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "unic-ucd-segment"
 | 
				
			||||||
 | 
					version = "0.9.0"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "2079c122a62205b421f499da10f3ee0f7697f012f55b675e002483c73ea34700"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "unic-char-property",
 | 
				
			||||||
 | 
					 "unic-char-range",
 | 
				
			||||||
 | 
					 "unic-ucd-version",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					[[package]]
 | 
				
			||||||
 | 
					name = "unic-ucd-version"
 | 
				
			||||||
 | 
					version = "0.9.0"
 | 
				
			||||||
 | 
					source = "registry+https://github.com/rust-lang/crates.io-index"
 | 
				
			||||||
 | 
					checksum = "96bd2f2237fe450fcd0a1d2f5f4e91711124f7857ba2e964247776ebeeb7b0c4"
 | 
				
			||||||
 | 
					dependencies = [
 | 
				
			||||||
 | 
					 "unic-common",
 | 
				
			||||||
 | 
					]
 | 
				
			||||||
 | 
					
 | 
				
			||||||
[[package]]
 | 
					[[package]]
 | 
				
			||||||
name = "unicode-ident"
 | 
					name = "unicode-ident"
 | 
				
			||||||
version = "1.0.17"
 | 
					version = "1.0.17"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -26,3 +26,5 @@ tokio = { version = "1.43.0", features = ["full"] }
 | 
				
			||||||
tower-http = { version = "0.6.2", features = ["set-header", "trace"] }
 | 
					tower-http = { version = "0.6.2", features = ["set-header", "trace"] }
 | 
				
			||||||
tracing = "0.1.41"
 | 
					tracing = "0.1.41"
 | 
				
			||||||
tracing-subscriber = "0.3.19"
 | 
					tracing-subscriber = "0.3.19"
 | 
				
			||||||
 | 
					tera = "1.20.0"
 | 
				
			||||||
 | 
					axum-range = "0.5.0"
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -56,6 +56,7 @@ pub struct EpisodeAction {
 | 
				
			||||||
pub struct Session {
 | 
					pub struct Session {
 | 
				
			||||||
    pub id: i64,
 | 
					    pub id: i64,
 | 
				
			||||||
    pub last_seen: DateTime<Utc>,
 | 
					    pub last_seen: DateTime<Utc>,
 | 
				
			||||||
 | 
					    pub user_agent: Option<String>,
 | 
				
			||||||
    pub user: User,
 | 
					    pub user: User,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -71,11 +71,16 @@ impl GpodderRepository {
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    pub fn create_session(&self, user: &models::User) -> Result<models::Session, AuthErr> {
 | 
					    pub fn create_session(
 | 
				
			||||||
 | 
					        &self,
 | 
				
			||||||
 | 
					        user: &models::User,
 | 
				
			||||||
 | 
					        user_agent: Option<String>,
 | 
				
			||||||
 | 
					    ) -> Result<models::Session, AuthErr> {
 | 
				
			||||||
        let session = models::Session {
 | 
					        let session = models::Session {
 | 
				
			||||||
            id: rand::thread_rng().gen(),
 | 
					            id: rand::thread_rng().gen(),
 | 
				
			||||||
            last_seen: Utc::now(),
 | 
					            last_seen: Utc::now(),
 | 
				
			||||||
            user: user.clone(),
 | 
					            user: user.clone(),
 | 
				
			||||||
 | 
					            user_agent,
 | 
				
			||||||
        };
 | 
					        };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        self.store.insert_session(&session)?;
 | 
					        self.store.insert_session(&session)?;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,2 @@
 | 
				
			||||||
 | 
					alter table sessions
 | 
				
			||||||
 | 
					    drop column user_agent;
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,2 @@
 | 
				
			||||||
 | 
					alter table sessions
 | 
				
			||||||
 | 
					    add column user_agent text;
 | 
				
			||||||
| 
						 | 
					@ -10,6 +10,7 @@ pub struct Session {
 | 
				
			||||||
    pub id: i64,
 | 
					    pub id: i64,
 | 
				
			||||||
    pub user_id: i64,
 | 
					    pub user_id: i64,
 | 
				
			||||||
    pub last_seen: i64,
 | 
					    pub last_seen: i64,
 | 
				
			||||||
 | 
					    pub user_agent: Option<String>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl Session {
 | 
					impl Session {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -59,6 +59,7 @@ impl gpodder::AuthStore for SqliteRepository {
 | 
				
			||||||
                id: session.id,
 | 
					                id: session.id,
 | 
				
			||||||
                last_seen: DateTime::from_timestamp(session.last_seen, 0).unwrap(),
 | 
					                last_seen: DateTime::from_timestamp(session.last_seen, 0).unwrap(),
 | 
				
			||||||
                user: user.into(),
 | 
					                user: user.into(),
 | 
				
			||||||
 | 
					                user_agent: session.user_agent.clone(),
 | 
				
			||||||
            })),
 | 
					            })),
 | 
				
			||||||
            Ok(None) => Ok(None),
 | 
					            Ok(None) => Ok(None),
 | 
				
			||||||
            Err(err) => Err(DbError::from(err).into()),
 | 
					            Err(err) => Err(DbError::from(err).into()),
 | 
				
			||||||
| 
						 | 
					@ -79,6 +80,7 @@ impl gpodder::AuthStore for SqliteRepository {
 | 
				
			||||||
            id: session.id,
 | 
					            id: session.id,
 | 
				
			||||||
            user_id: session.user.id,
 | 
					            user_id: session.user.id,
 | 
				
			||||||
            last_seen: session.last_seen.timestamp(),
 | 
					            last_seen: session.last_seen.timestamp(),
 | 
				
			||||||
 | 
					            user_agent: session.user_agent.clone(),
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        .insert_into(sessions::table)
 | 
					        .insert_into(sessions::table)
 | 
				
			||||||
        .execute(&mut self.pool.get().map_err(DbError::from)?)
 | 
					        .execute(&mut self.pool.get().map_err(DbError::from)?)
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -43,6 +43,7 @@ diesel::table! {
 | 
				
			||||||
        id -> BigInt,
 | 
					        id -> BigInt,
 | 
				
			||||||
        user_id -> BigInt,
 | 
					        user_id -> BigInt,
 | 
				
			||||||
        last_seen -> BigInt,
 | 
					        last_seen -> BigInt,
 | 
				
			||||||
 | 
					        user_agent -> Nullable<Text>,
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,4 @@
 | 
				
			||||||
use std::time::Duration;
 | 
					use std::{sync::Arc, time::Duration};
 | 
				
			||||||
 | 
					 | 
				
			||||||
use tracing_subscriber::util::SubscriberInitExt;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::server;
 | 
					use crate::server;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -11,12 +9,17 @@ pub fn serve(config: &crate::config::Config) -> u8 {
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    tracing::info!("Initializing database and running migrations");
 | 
					    tracing::info!("Initializing database and running migrations");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // TODO remove unwraps
 | 
				
			||||||
    let store =
 | 
					    let store =
 | 
				
			||||||
        gpodder_sqlite::SqliteRepository::from_path(config.data_dir.join(crate::DB_FILENAME))
 | 
					        gpodder_sqlite::SqliteRepository::from_path(config.data_dir.join(crate::DB_FILENAME))
 | 
				
			||||||
            .unwrap();
 | 
					            .unwrap();
 | 
				
			||||||
 | 
					    let tera = crate::web::initialize_tera().unwrap();
 | 
				
			||||||
    let store = gpodder::GpodderRepository::new(store);
 | 
					    let store = gpodder::GpodderRepository::new(store);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let ctx = server::Context { store };
 | 
					    let ctx = server::Context {
 | 
				
			||||||
 | 
					        store,
 | 
				
			||||||
 | 
					        tera: Arc::new(tera),
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
    let app = server::app(ctx.clone());
 | 
					    let app = server::app(ctx.clone());
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let rt = tokio::runtime::Builder::new_multi_thread()
 | 
					    let rt = tokio::runtime::Builder::new_multi_thread()
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,6 +1,7 @@
 | 
				
			||||||
mod cli;
 | 
					mod cli;
 | 
				
			||||||
mod config;
 | 
					mod config;
 | 
				
			||||||
mod server;
 | 
					mod server;
 | 
				
			||||||
 | 
					mod web;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use clap::Parser;
 | 
					use clap::Parser;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,6 +10,7 @@ pub type AppResult<T> = Result<T, AppError>;
 | 
				
			||||||
pub enum AppError {
 | 
					pub enum AppError {
 | 
				
			||||||
    // Db(db::DbError),
 | 
					    // Db(db::DbError),
 | 
				
			||||||
    IO(std::io::Error),
 | 
					    IO(std::io::Error),
 | 
				
			||||||
 | 
					    Tera(tera::Error),
 | 
				
			||||||
    Other(Box<dyn std::error::Error + 'static + Send + Sync>),
 | 
					    Other(Box<dyn std::error::Error + 'static + Send + Sync>),
 | 
				
			||||||
    BadRequest,
 | 
					    BadRequest,
 | 
				
			||||||
    Unauthorized,
 | 
					    Unauthorized,
 | 
				
			||||||
| 
						 | 
					@ -21,6 +22,7 @@ impl fmt::Display for AppError {
 | 
				
			||||||
        match self {
 | 
					        match self {
 | 
				
			||||||
            // Self::Db(_) => write!(f, "database error"),
 | 
					            // Self::Db(_) => write!(f, "database error"),
 | 
				
			||||||
            Self::IO(_) => write!(f, "io error"),
 | 
					            Self::IO(_) => write!(f, "io error"),
 | 
				
			||||||
 | 
					            Self::Tera(_) => write!(f, "tera error"),
 | 
				
			||||||
            Self::Other(_) => write!(f, "other error"),
 | 
					            Self::Other(_) => write!(f, "other error"),
 | 
				
			||||||
            Self::BadRequest => write!(f, "bad request"),
 | 
					            Self::BadRequest => write!(f, "bad request"),
 | 
				
			||||||
            Self::Unauthorized => write!(f, "unauthorized"),
 | 
					            Self::Unauthorized => write!(f, "unauthorized"),
 | 
				
			||||||
| 
						 | 
					@ -33,6 +35,7 @@ impl std::error::Error for AppError {
 | 
				
			||||||
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
 | 
					    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
 | 
				
			||||||
        match self {
 | 
					        match self {
 | 
				
			||||||
            // Self::Db(err) => Some(err),
 | 
					            // Self::Db(err) => Some(err),
 | 
				
			||||||
 | 
					            Self::Tera(err) => Some(err),
 | 
				
			||||||
            Self::IO(err) => Some(err),
 | 
					            Self::IO(err) => Some(err),
 | 
				
			||||||
            Self::Other(err) => Some(err.as_ref()),
 | 
					            Self::Other(err) => Some(err.as_ref()),
 | 
				
			||||||
            Self::NotFound | Self::Unauthorized | Self::BadRequest => None,
 | 
					            Self::NotFound | Self::Unauthorized | Self::BadRequest => None,
 | 
				
			||||||
| 
						 | 
					@ -46,6 +49,12 @@ impl From<std::io::Error> for AppError {
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl From<tera::Error> for AppError {
 | 
				
			||||||
 | 
					    fn from(value: tera::Error) -> Self {
 | 
				
			||||||
 | 
					        Self::Tera(value)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
impl IntoResponse for AppError {
 | 
					impl IntoResponse for AppError {
 | 
				
			||||||
    fn into_response(self) -> axum::response::Response {
 | 
					    fn into_response(self) -> axum::response::Response {
 | 
				
			||||||
        match self {
 | 
					        match self {
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -5,10 +5,11 @@ use axum::{
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use axum_extra::{
 | 
					use axum_extra::{
 | 
				
			||||||
    extract::{cookie::Cookie, CookieJar},
 | 
					    extract::{cookie::Cookie, CookieJar},
 | 
				
			||||||
    headers::{authorization::Basic, Authorization},
 | 
					    headers::{authorization::Basic, Authorization, UserAgent},
 | 
				
			||||||
    TypedHeader,
 | 
					    TypedHeader,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use cookie::time::Duration;
 | 
					use cookie::time::Duration;
 | 
				
			||||||
 | 
					use gpodder::AuthErr;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::server::{
 | 
					use crate::server::{
 | 
				
			||||||
    error::{AppError, AppResult},
 | 
					    error::{AppError, AppResult},
 | 
				
			||||||
| 
						 | 
					@ -27,6 +28,7 @@ async fn post_login(
 | 
				
			||||||
    Path(username): Path<String>,
 | 
					    Path(username): Path<String>,
 | 
				
			||||||
    jar: CookieJar,
 | 
					    jar: CookieJar,
 | 
				
			||||||
    TypedHeader(auth): TypedHeader<Authorization<Basic>>,
 | 
					    TypedHeader(auth): TypedHeader<Authorization<Basic>>,
 | 
				
			||||||
 | 
					    user_agent: Option<TypedHeader<UserAgent>>,
 | 
				
			||||||
) -> AppResult<CookieJar> {
 | 
					) -> AppResult<CookieJar> {
 | 
				
			||||||
    // These should be the same according to the spec
 | 
					    // These should be the same according to the spec
 | 
				
			||||||
    if username != auth.username() {
 | 
					    if username != auth.username() {
 | 
				
			||||||
| 
						 | 
					@ -62,7 +64,11 @@ async fn post_login(
 | 
				
			||||||
        let user = ctx
 | 
					        let user = ctx
 | 
				
			||||||
            .store
 | 
					            .store
 | 
				
			||||||
            .validate_credentials(auth.username(), auth.password())?;
 | 
					            .validate_credentials(auth.username(), auth.password())?;
 | 
				
			||||||
        ctx.store.create_session(&user)
 | 
					
 | 
				
			||||||
 | 
					        let user_agent = user_agent.map(|header| header.to_string());
 | 
				
			||||||
 | 
					        let session = ctx.store.create_session(&user, user_agent)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok::<_, AuthErr>(session)
 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
    .await
 | 
					    .await
 | 
				
			||||||
    .unwrap()?;
 | 
					    .unwrap()?;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,7 +8,7 @@ use axum::{
 | 
				
			||||||
use crate::server::{
 | 
					use crate::server::{
 | 
				
			||||||
    error::{AppError, AppResult},
 | 
					    error::{AppError, AppResult},
 | 
				
			||||||
    gpodder::{
 | 
					    gpodder::{
 | 
				
			||||||
        auth_middleware,
 | 
					        auth_api_middleware,
 | 
				
			||||||
        format::{Format, StringWithFormat},
 | 
					        format::{Format, StringWithFormat},
 | 
				
			||||||
        models,
 | 
					        models,
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					@ -19,7 +19,7 @@ pub fn router(ctx: Context) -> Router<Context> {
 | 
				
			||||||
    Router::new()
 | 
					    Router::new()
 | 
				
			||||||
        .route("/{username}", get(get_devices))
 | 
					        .route("/{username}", get(get_devices))
 | 
				
			||||||
        .route("/{username}/{id}", post(post_device))
 | 
					        .route("/{username}/{id}", post(post_device))
 | 
				
			||||||
        .layer(middleware::from_fn_with_state(ctx.clone(), auth_middleware))
 | 
					        .layer(middleware::from_fn_with_state(ctx.clone(), auth_api_middleware))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async fn get_devices(
 | 
					async fn get_devices(
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -10,7 +10,7 @@ use serde::{Deserialize, Serialize};
 | 
				
			||||||
use crate::server::{
 | 
					use crate::server::{
 | 
				
			||||||
    error::{AppError, AppResult},
 | 
					    error::{AppError, AppResult},
 | 
				
			||||||
    gpodder::{
 | 
					    gpodder::{
 | 
				
			||||||
        auth_middleware,
 | 
					        auth_api_middleware,
 | 
				
			||||||
        format::{Format, StringWithFormat},
 | 
					        format::{Format, StringWithFormat},
 | 
				
			||||||
        models,
 | 
					        models,
 | 
				
			||||||
        models::UpdatedUrlsResponse,
 | 
					        models::UpdatedUrlsResponse,
 | 
				
			||||||
| 
						 | 
					@ -24,7 +24,7 @@ pub fn router(ctx: Context) -> Router<Context> {
 | 
				
			||||||
            "/{username}",
 | 
					            "/{username}",
 | 
				
			||||||
            post(post_episode_actions).get(get_episode_actions),
 | 
					            post(post_episode_actions).get(get_episode_actions),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        .layer(middleware::from_fn_with_state(ctx.clone(), auth_middleware))
 | 
					        .layer(middleware::from_fn_with_state(ctx.clone(), auth_api_middleware))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
async fn post_episode_actions(
 | 
					async fn post_episode_actions(
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -9,7 +9,7 @@ use serde::Deserialize;
 | 
				
			||||||
use crate::server::{
 | 
					use crate::server::{
 | 
				
			||||||
    error::{AppError, AppResult},
 | 
					    error::{AppError, AppResult},
 | 
				
			||||||
    gpodder::{
 | 
					    gpodder::{
 | 
				
			||||||
        auth_middleware,
 | 
					        auth_api_middleware,
 | 
				
			||||||
        format::{Format, StringWithFormat},
 | 
					        format::{Format, StringWithFormat},
 | 
				
			||||||
        models::{SubscriptionDelta, SubscriptionDeltaResponse, UpdatedUrlsResponse},
 | 
					        models::{SubscriptionDelta, SubscriptionDeltaResponse, UpdatedUrlsResponse},
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					@ -22,7 +22,7 @@ pub fn router(ctx: Context) -> Router<Context> {
 | 
				
			||||||
            "/{username}/{id}",
 | 
					            "/{username}/{id}",
 | 
				
			||||||
            post(post_subscription_changes).get(get_subscription_changes),
 | 
					            post(post_subscription_changes).get(get_subscription_changes),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        .layer(middleware::from_fn_with_state(ctx.clone(), auth_middleware))
 | 
					        .layer(middleware::from_fn_with_state(ctx.clone(), auth_api_middleware))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn post_subscription_changes(
 | 
					pub async fn post_subscription_changes(
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -8,7 +8,7 @@ use axum::{
 | 
				
			||||||
use crate::server::{
 | 
					use crate::server::{
 | 
				
			||||||
    error::{AppError, AppResult},
 | 
					    error::{AppError, AppResult},
 | 
				
			||||||
    gpodder::{
 | 
					    gpodder::{
 | 
				
			||||||
        auth_middleware,
 | 
					        auth_api_middleware,
 | 
				
			||||||
        format::{Format, StringWithFormat},
 | 
					        format::{Format, StringWithFormat},
 | 
				
			||||||
        models::{SyncStatus, SyncStatusDelta},
 | 
					        models::{SyncStatus, SyncStatusDelta},
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
| 
						 | 
					@ -21,7 +21,7 @@ pub fn router(ctx: Context) -> Router<Context> {
 | 
				
			||||||
            "/{username}",
 | 
					            "/{username}",
 | 
				
			||||||
            get(get_sync_status).post(post_sync_status_changes),
 | 
					            get(get_sync_status).post(post_sync_status_changes),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        .layer(middleware::from_fn_with_state(ctx.clone(), auth_middleware))
 | 
					        .layer(middleware::from_fn_with_state(ctx.clone(), auth_api_middleware))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn get_sync_status(
 | 
					pub async fn get_sync_status(
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -36,8 +36,13 @@ pub fn router(ctx: Context) -> Router<Context> {
 | 
				
			||||||
        ))
 | 
					        ))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/// This middleware accepts
 | 
					/// Middleware that can authenticate both with session cookies and basic auth. If basic auth is
 | 
				
			||||||
pub async fn auth_middleware(State(ctx): State<Context>, mut req: Request, next: Next) -> Response {
 | 
					/// used, no session is created. If authentication fails, the server returns a 401.
 | 
				
			||||||
 | 
					pub async fn auth_api_middleware(
 | 
				
			||||||
 | 
					    State(ctx): State<Context>,
 | 
				
			||||||
 | 
					    mut req: Request,
 | 
				
			||||||
 | 
					    next: Next,
 | 
				
			||||||
 | 
					) -> Response {
 | 
				
			||||||
    // SAFETY: this extractor's error type is Infallible
 | 
					    // SAFETY: this extractor's error type is Infallible
 | 
				
			||||||
    let mut jar: CookieJar = req.extract_parts().await.unwrap();
 | 
					    let mut jar: CookieJar = req.extract_parts().await.unwrap();
 | 
				
			||||||
    let mut auth_user = None;
 | 
					    let mut auth_user = None;
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -7,7 +7,7 @@ use axum::{
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use crate::server::{
 | 
					use crate::server::{
 | 
				
			||||||
    error::{AppError, AppResult},
 | 
					    error::{AppError, AppResult},
 | 
				
			||||||
    gpodder::{auth_middleware, format::StringWithFormat},
 | 
					    gpodder::{auth_api_middleware, format::StringWithFormat},
 | 
				
			||||||
    Context,
 | 
					    Context,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
| 
						 | 
					@ -18,7 +18,7 @@ pub fn router(ctx: Context) -> Router<Context> {
 | 
				
			||||||
            get(get_device_subscriptions).put(put_device_subscriptions),
 | 
					            get(get_device_subscriptions).put(put_device_subscriptions),
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
        .route("/{username}", get(get_user_subscriptions))
 | 
					        .route("/{username}", get(get_user_subscriptions))
 | 
				
			||||||
        .layer(middleware::from_fn_with_state(ctx.clone(), auth_middleware))
 | 
					        .layer(middleware::from_fn_with_state(ctx.clone(), auth_api_middleware))
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub async fn get_device_subscriptions(
 | 
					pub async fn get_device_subscriptions(
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
| 
						 | 
					@ -1,12 +1,17 @@
 | 
				
			||||||
mod error;
 | 
					mod error;
 | 
				
			||||||
mod gpodder;
 | 
					mod gpodder;
 | 
				
			||||||
 | 
					mod r#static;
 | 
				
			||||||
 | 
					mod web;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::sync::Arc;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
use axum::{
 | 
					use axum::{
 | 
				
			||||||
    body::Body,
 | 
					    body::Body,
 | 
				
			||||||
    extract::Request,
 | 
					    extract::Request,
 | 
				
			||||||
    http::StatusCode,
 | 
					    http::StatusCode,
 | 
				
			||||||
    middleware::Next,
 | 
					    middleware::Next,
 | 
				
			||||||
    response::{IntoResponse, Response},
 | 
					    response::{IntoResponse, Redirect, Response},
 | 
				
			||||||
 | 
					    routing::get,
 | 
				
			||||||
    Router,
 | 
					    Router,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
use http_body_util::BodyExt;
 | 
					use http_body_util::BodyExt;
 | 
				
			||||||
| 
						 | 
					@ -15,11 +20,15 @@ use tower_http::trace::TraceLayer;
 | 
				
			||||||
#[derive(Clone)]
 | 
					#[derive(Clone)]
 | 
				
			||||||
pub struct Context {
 | 
					pub struct Context {
 | 
				
			||||||
    pub store: ::gpodder::GpodderRepository,
 | 
					    pub store: ::gpodder::GpodderRepository,
 | 
				
			||||||
 | 
					    pub tera: Arc<tera::Tera>,
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
pub fn app(ctx: Context) -> Router {
 | 
					pub fn app(ctx: Context) -> Router {
 | 
				
			||||||
    Router::new()
 | 
					    Router::new()
 | 
				
			||||||
        .merge(gpodder::router(ctx.clone()))
 | 
					        .merge(gpodder::router(ctx.clone()))
 | 
				
			||||||
 | 
					        .nest("/static", r#static::router())
 | 
				
			||||||
 | 
					        .nest("/_", web::router(ctx.clone()))
 | 
				
			||||||
 | 
					        .route("/", get(|| async { Redirect::to("/_") }))
 | 
				
			||||||
        .layer(axum::middleware::from_fn(header_logger))
 | 
					        .layer(axum::middleware::from_fn(header_logger))
 | 
				
			||||||
        .layer(axum::middleware::from_fn(body_logger))
 | 
					        .layer(axum::middleware::from_fn(body_logger))
 | 
				
			||||||
        .layer(TraceLayer::new_for_http())
 | 
					        .layer(TraceLayer::new_for_http())
 | 
				
			||||||
| 
						 | 
					
 | 
				
			||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| 
						 | 
					@ -0,0 +1,34 @@
 | 
				
			||||||
 | 
					use std::io::Cursor;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use axum::{routing::get, Router};
 | 
				
			||||||
 | 
					use axum_extra::{headers::Range, TypedHeader};
 | 
				
			||||||
 | 
					use axum_range::{KnownSize, Ranged};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use super::Context;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const HTMX: &str = include_str!("./htmx_2.0.4.min.js");
 | 
				
			||||||
 | 
					const PICOCSS: &str = include_str!("./pico_2.1.1.classless.jade.min.css");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type RangedResponse = Ranged<KnownSize<Cursor<&'static str>>>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn router() -> Router<Context> {
 | 
				
			||||||
 | 
					    Router::new()
 | 
				
			||||||
 | 
					        .route("/htmx_2.0.4.min.js", get(get_htmx))
 | 
				
			||||||
 | 
					        .route("/pico_2.1.1.classless.jade.min.css", get(get_picocss))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[inline(always)]
 | 
				
			||||||
 | 
					fn serve_static(data: &'static str, range: Option<Range>) -> RangedResponse {
 | 
				
			||||||
 | 
					    let cursor = Cursor::new(data);
 | 
				
			||||||
 | 
					    let body = KnownSize::sized(cursor, data.len() as u64);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ranged::new(range, body)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn get_htmx(range: Option<TypedHeader<Range>>) -> RangedResponse {
 | 
				
			||||||
 | 
					    serve_static(HTMX, range.map(|TypedHeader(range)| range))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn get_picocss(range: Option<TypedHeader<Range>>) -> RangedResponse {
 | 
				
			||||||
 | 
					    serve_static(PICOCSS, range.map(|TypedHeader(range)| range))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| 
						 | 
					@ -0,0 +1,143 @@
 | 
				
			||||||
 | 
					use axum::{
 | 
				
			||||||
 | 
					    extract::{Request, State},
 | 
				
			||||||
 | 
					    http::HeaderMap,
 | 
				
			||||||
 | 
					    middleware::{self, Next},
 | 
				
			||||||
 | 
					    response::{IntoResponse, Redirect, Response},
 | 
				
			||||||
 | 
					    routing::get,
 | 
				
			||||||
 | 
					    Form, RequestExt, Router,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					use axum_extra::{extract::CookieJar, headers::UserAgent, TypedHeader};
 | 
				
			||||||
 | 
					use cookie::{time::Duration, Cookie};
 | 
				
			||||||
 | 
					use gpodder::{AuthErr, Session};
 | 
				
			||||||
 | 
					use serde::Deserialize;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use crate::web::{Page, TemplateExt, TemplateResponse, View};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use super::{
 | 
				
			||||||
 | 
					    error::{AppError, AppResult},
 | 
				
			||||||
 | 
					    Context,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const SESSION_ID_COOKIE: &str = "sessionid";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn router(ctx: Context) -> Router<Context> {
 | 
				
			||||||
 | 
					    Router::new()
 | 
				
			||||||
 | 
					        .route("/", get(get_index))
 | 
				
			||||||
 | 
					        .layer(middleware::from_fn_with_state(
 | 
				
			||||||
 | 
					            ctx.clone(),
 | 
				
			||||||
 | 
					            auth_web_middleware,
 | 
				
			||||||
 | 
					        ))
 | 
				
			||||||
 | 
					        // Login route needs to be handled differently, as the middleware turns it into a redirect
 | 
				
			||||||
 | 
					        // loop
 | 
				
			||||||
 | 
					        .route("/login", get(get_login).post(post_login))
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn get_index(State(ctx): State<Context>, headers: HeaderMap) -> TemplateResponse<Page<View>> {
 | 
				
			||||||
 | 
					    View::Index.page(&headers).response(&ctx.tera)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn get_login(State(ctx): State<Context>, headers: HeaderMap, jar: CookieJar) -> Response {
 | 
				
			||||||
 | 
					    if extract_session(ctx.clone(), &jar)
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .ok()
 | 
				
			||||||
 | 
					        .flatten()
 | 
				
			||||||
 | 
					        .is_some()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Redirect::to("/_").into_response()
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        View::Login
 | 
				
			||||||
 | 
					            .page(&headers)
 | 
				
			||||||
 | 
					            .response(&ctx.tera)
 | 
				
			||||||
 | 
					            .into_response()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#[derive(Deserialize)]
 | 
				
			||||||
 | 
					struct LoginForm {
 | 
				
			||||||
 | 
					    username: String,
 | 
				
			||||||
 | 
					    password: String,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn post_login(
 | 
				
			||||||
 | 
					    State(ctx): State<Context>,
 | 
				
			||||||
 | 
					    user_agent: Option<TypedHeader<UserAgent>>,
 | 
				
			||||||
 | 
					    _headers: HeaderMap,
 | 
				
			||||||
 | 
					    jar: CookieJar,
 | 
				
			||||||
 | 
					    Form(login): Form<LoginForm>,
 | 
				
			||||||
 | 
					) -> AppResult<Response> {
 | 
				
			||||||
 | 
					    match tokio::task::spawn_blocking(move || {
 | 
				
			||||||
 | 
					        let user = ctx
 | 
				
			||||||
 | 
					            .store
 | 
				
			||||||
 | 
					            .validate_credentials(&login.username, &login.password)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        let user_agent = user_agent.map(|header| header.to_string());
 | 
				
			||||||
 | 
					        let session = ctx.store.create_session(&user, user_agent)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        Ok::<_, AuthErr>(session)
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .await
 | 
				
			||||||
 | 
					    .unwrap()
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        Ok(session) => Ok((
 | 
				
			||||||
 | 
					            jar.add(
 | 
				
			||||||
 | 
					                Cookie::build((SESSION_ID_COOKIE, session.id.to_string()))
 | 
				
			||||||
 | 
					                    .secure(true)
 | 
				
			||||||
 | 
					                    .same_site(cookie::SameSite::Lax)
 | 
				
			||||||
 | 
					                    .http_only(true)
 | 
				
			||||||
 | 
					                    .path("/")
 | 
				
			||||||
 | 
					                    .max_age(Duration::days(365)),
 | 
				
			||||||
 | 
					            ),
 | 
				
			||||||
 | 
					            Redirect::to("/_"),
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					            .into_response()),
 | 
				
			||||||
 | 
					        Err(AuthErr::UnknownUser | AuthErr::InvalidPassword) => {
 | 
				
			||||||
 | 
					            todo!("serve login form with error messages")
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        Err(err) => Err(AppError::from(err)),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					async fn extract_session(ctx: Context, jar: &CookieJar) -> AppResult<Option<Session>> {
 | 
				
			||||||
 | 
					    if let Some(session_id) = jar
 | 
				
			||||||
 | 
					        .get(SESSION_ID_COOKIE)
 | 
				
			||||||
 | 
					        .and_then(|c| c.value().parse::<i64>().ok())
 | 
				
			||||||
 | 
					    {
 | 
				
			||||||
 | 
					        match tokio::task::spawn_blocking(move || {
 | 
				
			||||||
 | 
					            let session = ctx.store.get_session(session_id)?;
 | 
				
			||||||
 | 
					            ctx.store.refresh_session(&session)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            Ok(session)
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .await
 | 
				
			||||||
 | 
					        .unwrap()
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					            Ok(session) => Ok(Some(session)),
 | 
				
			||||||
 | 
					            Err(gpodder::AuthErr::UnknownSession) => Ok(None),
 | 
				
			||||||
 | 
					            Err(err) => Err(AppError::from(err)),
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					        Ok(None)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Middleware that authenticates the current user via the session token. If the credentials are
 | 
				
			||||||
 | 
					/// invalid, the user is redirected to the login page.
 | 
				
			||||||
 | 
					pub async fn auth_web_middleware(
 | 
				
			||||||
 | 
					    State(ctx): State<Context>,
 | 
				
			||||||
 | 
					    mut req: Request,
 | 
				
			||||||
 | 
					    next: Next,
 | 
				
			||||||
 | 
					) -> Response {
 | 
				
			||||||
 | 
					    // SAFETY: this extractor's error type is Infallible
 | 
				
			||||||
 | 
					    let jar: CookieJar = req.extract_parts().await.unwrap();
 | 
				
			||||||
 | 
					    let redirect = Redirect::to("/_/login");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    match extract_session(ctx, &jar).await {
 | 
				
			||||||
 | 
					        Ok(Some(session)) => {
 | 
				
			||||||
 | 
					            req.extensions_mut().insert(session.user);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            next.run(req).await
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        Ok(None) => redirect.into_response(),
 | 
				
			||||||
 | 
					        Err(err) => err.into_response(),
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,85 @@
 | 
				
			||||||
 | 
					mod page;
 | 
				
			||||||
 | 
					mod view;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use std::sync::Arc;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use axum::{
 | 
				
			||||||
 | 
					    body::Body,
 | 
				
			||||||
 | 
					    http::{HeaderMap, Response, StatusCode},
 | 
				
			||||||
 | 
					    response::{Html, IntoResponse},
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub use page::Page;
 | 
				
			||||||
 | 
					pub use view::View;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const BASE_TEMPLATE: &str = "base.html";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Trait defining shared methods for working with typed Tera templates
 | 
				
			||||||
 | 
					pub trait Template {
 | 
				
			||||||
 | 
					    /// Returns the name or path used to identify the template in the Tera struct
 | 
				
			||||||
 | 
					    fn template(&self) -> &'static str;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    /// Render the template using the given Tera instance.
 | 
				
			||||||
 | 
					    ///
 | 
				
			||||||
 | 
					    /// Templates are expected to manage their own context requirements if needed.
 | 
				
			||||||
 | 
					    fn render(&self, tera: &tera::Tera) -> tera::Result<String>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Useful additional functions on sized Template implementors
 | 
				
			||||||
 | 
					pub trait TemplateExt: Sized + Template {
 | 
				
			||||||
 | 
					    fn response(self, tera: &Arc<tera::Tera>) -> TemplateResponse<Self> {
 | 
				
			||||||
 | 
					        TemplateResponse::new(tera, self)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn page(self, headers: &HeaderMap) -> Page<Self> {
 | 
				
			||||||
 | 
					        Page::new(self).headers(headers)
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<T: Sized + Template> TemplateExt for T {}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// A specific instance of a template. This type can be used as a return type from Axum handlers.
 | 
				
			||||||
 | 
					pub struct TemplateResponse<T> {
 | 
				
			||||||
 | 
					    tera: Arc<tera::Tera>,
 | 
				
			||||||
 | 
					    template: T,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<T> TemplateResponse<T> {
 | 
				
			||||||
 | 
					    pub fn new(tera: &Arc<tera::Tera>, template: T) -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            tera: Arc::clone(&tera),
 | 
				
			||||||
 | 
					            template,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<T: Template> IntoResponse for TemplateResponse<T> {
 | 
				
			||||||
 | 
					    fn into_response(self) -> Response<Body> {
 | 
				
			||||||
 | 
					        match self.template.render(&self.tera) {
 | 
				
			||||||
 | 
					            Ok(s) => Html(s).into_response(),
 | 
				
			||||||
 | 
					            Err(err) => {
 | 
				
			||||||
 | 
					                tracing::error!("tera template failed: {err}");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                StatusCode::INTERNAL_SERVER_ERROR.into_response()
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub fn initialize_tera() -> tera::Result<tera::Tera> {
 | 
				
			||||||
 | 
					    let mut tera = tera::Tera::default();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    tera.add_raw_templates([
 | 
				
			||||||
 | 
					        (BASE_TEMPLATE, include_str!("templates/base.html")),
 | 
				
			||||||
 | 
					        (
 | 
				
			||||||
 | 
					            View::Index.template(),
 | 
				
			||||||
 | 
					            include_str!("templates/views/index.html"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					        (
 | 
				
			||||||
 | 
					            View::Login.template(),
 | 
				
			||||||
 | 
					            include_str!("templates/views/login.html"),
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					    ])?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    Ok(tera)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,53 @@
 | 
				
			||||||
 | 
					use axum::http::{HeaderMap, HeaderValue};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					use super::Template;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const HX_REQUEST_HEADER: &str = "HX-Request";
 | 
				
			||||||
 | 
					const HX_HISTORY_RESTORE_HEADER: &str = "HX-History-Restore-Request";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/// Overarching template type that conditionally wraps its inner template with the base template if
 | 
				
			||||||
 | 
					/// required, as derived from the request headers
 | 
				
			||||||
 | 
					pub struct Page<T> {
 | 
				
			||||||
 | 
					    template: T,
 | 
				
			||||||
 | 
					    wrap_with_base: bool,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<T: Template> Template for Page<T> {
 | 
				
			||||||
 | 
					    fn template(&self) -> &'static str {
 | 
				
			||||||
 | 
					        self.template.template()
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn render(&self, tera: &tera::Tera) -> tera::Result<String> {
 | 
				
			||||||
 | 
					        let inner = self.template.render(tera)?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if self.wrap_with_base {
 | 
				
			||||||
 | 
					            let mut ctx = tera::Context::new();
 | 
				
			||||||
 | 
					            ctx.insert("inner", &inner);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            tera.render(super::BASE_TEMPLATE, &ctx)
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					            Ok(inner)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl<T> Page<T> {
 | 
				
			||||||
 | 
					    pub fn new(template: T) -> Self {
 | 
				
			||||||
 | 
					        Self {
 | 
				
			||||||
 | 
					            template,
 | 
				
			||||||
 | 
					            wrap_with_base: false,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    pub fn headers(mut self, headers: &HeaderMap) -> Self {
 | 
				
			||||||
 | 
					        let is_htmx_req = headers.get(HX_REQUEST_HEADER).is_some();
 | 
				
			||||||
 | 
					        let is_hist_restore_req = headers
 | 
				
			||||||
 | 
					            .get(HX_HISTORY_RESTORE_HEADER)
 | 
				
			||||||
 | 
					            .map(|val| val == HeaderValue::from_static("true"))
 | 
				
			||||||
 | 
					            .unwrap_or(false);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self.wrap_with_base = !is_htmx_req || is_hist_restore_req;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        self
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,24 @@
 | 
				
			||||||
 | 
					<!DOCTYPE html>
 | 
				
			||||||
 | 
					<html lang="en">
 | 
				
			||||||
 | 
					    <head>
 | 
				
			||||||
 | 
					        <script src="/static/htmx_2.0.4.min.js" integrity="sha384-HGfztofotfshcF7+8n44JQL2oJmowVChPTg48S+jvZoztPfvwD79OC/LTtG6dMp+"></script>
 | 
				
			||||||
 | 
					        <link rel="stylesheet" href="/static/pico_2.1.1.classless.jade.min.css" />
 | 
				
			||||||
 | 
					        <meta charset="utf-8" />
 | 
				
			||||||
 | 
					        <meta name="viewport" content="width=device-width, initial-scale=1" />
 | 
				
			||||||
 | 
					        <meta name="color-scheme" content="light dark" />
 | 
				
			||||||
 | 
					        <style type="text/css">
 | 
				
			||||||
 | 
					a:hover {
 | 
				
			||||||
 | 
					    cursor:pointer;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					        </style>
 | 
				
			||||||
 | 
					    </head>
 | 
				
			||||||
 | 
					    <body>
 | 
				
			||||||
 | 
					        <main>
 | 
				
			||||||
 | 
					            <nav>
 | 
				
			||||||
 | 
					            </nav>
 | 
				
			||||||
 | 
					            <article id="inner">
 | 
				
			||||||
 | 
					                {{ inner | safe }}
 | 
				
			||||||
 | 
					            </article>
 | 
				
			||||||
 | 
					        </main>
 | 
				
			||||||
 | 
					    </body>
 | 
				
			||||||
 | 
					</html>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,5 @@
 | 
				
			||||||
 | 
					<h1>Otter</h1>
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					Otter is a self-hostable Gpodder implementation.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					If you're seeing this, you're logged in.
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,9 @@
 | 
				
			||||||
 | 
					<article>
 | 
				
			||||||
 | 
					    <form hx-post="/_/login" hx-target="#inner">
 | 
				
			||||||
 | 
					        <label for="username">Username:</label>
 | 
				
			||||||
 | 
					        <input type="text" id="username" name="username">
 | 
				
			||||||
 | 
					        <label for="password">Password:</label>
 | 
				
			||||||
 | 
					        <input type="password" id="password" name="password">
 | 
				
			||||||
 | 
					        <input type="submit" value="Login">
 | 
				
			||||||
 | 
					    </form>
 | 
				
			||||||
 | 
					</article>
 | 
				
			||||||
| 
						 | 
					@ -0,0 +1,19 @@
 | 
				
			||||||
 | 
					use super::Template;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					pub enum View {
 | 
				
			||||||
 | 
					    Index,
 | 
				
			||||||
 | 
					    Login,
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					impl Template for View {
 | 
				
			||||||
 | 
					    fn template(&self) -> &'static str {
 | 
				
			||||||
 | 
					        match self {
 | 
				
			||||||
 | 
					            Self::Index => "views/index.html",
 | 
				
			||||||
 | 
					            Self::Login => "views/login.html",
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fn render(&self, tera: &tera::Tera) -> tera::Result<String> {
 | 
				
			||||||
 | 
					        tera.render(self.template(), &tera::Context::new())
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
		Loading…
	
		Reference in New Issue