+666 -23 +/-13 browse
1 | diff --git a/Cargo.lock b/Cargo.lock |
2 | index 422f381..8ceffe9 100644 |
3 | --- a/Cargo.lock |
4 | +++ b/Cargo.lock |
5 | @@ -23,6 +23,21 @@ dependencies = [ |
6 | ] |
7 | |
8 | [[package]] |
9 | + name = "alloc-no-stdlib" |
10 | + version = "2.0.4" |
11 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
12 | + checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" |
13 | + |
14 | + [[package]] |
15 | + name = "alloc-stdlib" |
16 | + version = "0.2.2" |
17 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
18 | + checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" |
19 | + dependencies = [ |
20 | + "alloc-no-stdlib", |
21 | + ] |
22 | + |
23 | + [[package]] |
24 | name = "android_system_properties" |
25 | version = "0.1.5" |
26 | source = "registry+https://github.com/rust-lang/crates.io-index" |
27 | @@ -56,6 +71,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" |
28 | checksum = "23b62fc65de8e4e7f52534fb52b0f3ed04746ae267519eef2a83941e8085068b" |
29 | |
30 | [[package]] |
31 | + name = "assert-json-diff" |
32 | + version = "2.0.2" |
33 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
34 | + checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" |
35 | + dependencies = [ |
36 | + "serde", |
37 | + "serde_json", |
38 | + ] |
39 | + |
40 | + [[package]] |
41 | name = "assert_cmd" |
42 | version = "2.0.11" |
43 | source = "registry+https://github.com/rust-lang/crates.io-index" |
44 | @@ -82,6 +107,19 @@ dependencies = [ |
45 | ] |
46 | |
47 | [[package]] |
48 | + name = "async-compression" |
49 | + version = "0.3.15" |
50 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
51 | + checksum = "942c7cd7ae39e91bde4820d74132e9862e62c2f386c3aa90ccf55949f5bad63a" |
52 | + dependencies = [ |
53 | + "brotli", |
54 | + "futures-core", |
55 | + "memchr", |
56 | + "pin-project-lite", |
57 | + "tokio", |
58 | + ] |
59 | + |
60 | + [[package]] |
61 | name = "async-executor" |
62 | version = "1.5.1" |
63 | source = "registry+https://github.com/rust-lang/crates.io-index" |
64 | @@ -399,6 +437,19 @@ dependencies = [ |
65 | ] |
66 | |
67 | [[package]] |
68 | + name = "bcrypt" |
69 | + version = "0.14.0" |
70 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
71 | + checksum = "9df288bec72232f78c1ec5fe4e8f1d108aa0265476e93097593c803c8c02062a" |
72 | + dependencies = [ |
73 | + "base64 0.21.0", |
74 | + "blowfish", |
75 | + "getrandom", |
76 | + "subtle", |
77 | + "zeroize", |
78 | + ] |
79 | + |
80 | + [[package]] |
81 | name = "bincode" |
82 | version = "1.3.3" |
83 | source = "registry+https://github.com/rust-lang/crates.io-index" |
84 | @@ -462,6 +513,37 @@ dependencies = [ |
85 | ] |
86 | |
87 | [[package]] |
88 | + name = "blowfish" |
89 | + version = "0.9.1" |
90 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
91 | + checksum = "e412e2cd0f2b2d93e02543ceae7917b3c70331573df19ee046bcbc35e45e87d7" |
92 | + dependencies = [ |
93 | + "byteorder", |
94 | + "cipher", |
95 | + ] |
96 | + |
97 | + [[package]] |
98 | + name = "brotli" |
99 | + version = "3.3.4" |
100 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
101 | + checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" |
102 | + dependencies = [ |
103 | + "alloc-no-stdlib", |
104 | + "alloc-stdlib", |
105 | + "brotli-decompressor", |
106 | + ] |
107 | + |
108 | + [[package]] |
109 | + name = "brotli-decompressor" |
110 | + version = "2.3.4" |
111 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
112 | + checksum = "4b6561fd3f895a11e8f72af2cb7d22e08366bebc2b6b57f7744c4bda27034744" |
113 | + dependencies = [ |
114 | + "alloc-no-stdlib", |
115 | + "alloc-stdlib", |
116 | + ] |
117 | + |
118 | + [[package]] |
119 | name = "bstr" |
120 | version = "1.4.0" |
121 | source = "registry+https://github.com/rust-lang/crates.io-index" |
122 | @@ -632,6 +714,16 @@ dependencies = [ |
123 | ] |
124 | |
125 | [[package]] |
126 | + name = "cipher" |
127 | + version = "0.4.4" |
128 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
129 | + checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" |
130 | + dependencies = [ |
131 | + "crypto-common", |
132 | + "inout", |
133 | + ] |
134 | + |
135 | + [[package]] |
136 | name = "clap" |
137 | version = "4.2.7" |
138 | source = "registry+https://github.com/rust-lang/crates.io-index" |
139 | @@ -706,6 +798,25 @@ dependencies = [ |
140 | ] |
141 | |
142 | [[package]] |
143 | + name = "config" |
144 | + version = "0.13.3" |
145 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
146 | + checksum = "d379af7f68bfc21714c6c7dea883544201741d2ce8274bb12fa54f89507f52a7" |
147 | + dependencies = [ |
148 | + "async-trait", |
149 | + "json5", |
150 | + "lazy_static", |
151 | + "nom", |
152 | + "pathdiff", |
153 | + "ron", |
154 | + "rust-ini", |
155 | + "serde", |
156 | + "serde_json", |
157 | + "toml", |
158 | + "yaml-rust", |
159 | + ] |
160 | + |
161 | + [[package]] |
162 | name = "constant_time_eq" |
163 | version = "0.1.5" |
164 | source = "registry+https://github.com/rust-lang/crates.io-index" |
165 | @@ -903,6 +1014,12 @@ dependencies = [ |
166 | ] |
167 | |
168 | [[package]] |
169 | + name = "dlv-list" |
170 | + version = "0.3.0" |
171 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
172 | + checksum = "0688c2a7f92e427f44895cd63841bff7b29f8d7a1648b9e7e07a4a365b2e1257" |
173 | + |
174 | + [[package]] |
175 | name = "doc-comment" |
176 | version = "0.3.3" |
177 | source = "registry+https://github.com/rust-lang/crates.io-index" |
178 | @@ -1475,6 +1592,19 @@ dependencies = [ |
179 | ] |
180 | |
181 | [[package]] |
182 | + name = "hyper-tls" |
183 | + version = "0.5.0" |
184 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
185 | + checksum = "d6183ddfa99b85da61a140bea0efc93fdf56ceaa041b37d553518030827f9905" |
186 | + dependencies = [ |
187 | + "bytes", |
188 | + "hyper", |
189 | + "native-tls", |
190 | + "tokio", |
191 | + "tokio-native-tls", |
192 | + ] |
193 | + |
194 | + [[package]] |
195 | name = "iana-time-zone" |
196 | version = "0.1.56" |
197 | source = "registry+https://github.com/rust-lang/crates.io-index" |
198 | @@ -1546,6 +1676,15 @@ dependencies = [ |
199 | ] |
200 | |
201 | [[package]] |
202 | + name = "inout" |
203 | + version = "0.1.3" |
204 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
205 | + checksum = "a0c10553d664a4d0bcff9f4215d0aac67a639cc68ef660840afe309b807bc9f5" |
206 | + dependencies = [ |
207 | + "generic-array", |
208 | + ] |
209 | + |
210 | + [[package]] |
211 | name = "instant" |
212 | version = "0.1.12" |
213 | source = "registry+https://github.com/rust-lang/crates.io-index" |
214 | @@ -1614,6 +1753,17 @@ dependencies = [ |
215 | ] |
216 | |
217 | [[package]] |
218 | + name = "json5" |
219 | + version = "0.4.1" |
220 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
221 | + checksum = "96b0db21af676c1ce64250b5f40f3ce2cf27e4e47cb91ed91eb6fe9350b430c1" |
222 | + dependencies = [ |
223 | + "pest", |
224 | + "pest_derive", |
225 | + "serde", |
226 | + ] |
227 | + |
228 | + [[package]] |
229 | name = "kernel32-sys" |
230 | version = "0.2.2" |
231 | source = "registry+https://github.com/rust-lang/crates.io-index" |
232 | @@ -1696,6 +1846,12 @@ dependencies = [ |
233 | ] |
234 | |
235 | [[package]] |
236 | + name = "linked-hash-map" |
237 | + version = "0.5.6" |
238 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
239 | + checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f" |
240 | + |
241 | + [[package]] |
242 | name = "linux-raw-sys" |
243 | version = "0.3.7" |
244 | source = "registry+https://github.com/rust-lang/crates.io-index" |
245 | @@ -1816,8 +1972,23 @@ dependencies = [ |
246 | name = "mailpot-http" |
247 | version = "0.1.1" |
248 | dependencies = [ |
249 | + "assert-json-diff", |
250 | + "async-trait", |
251 | + "axum", |
252 | + "bcrypt", |
253 | + "config", |
254 | + "http", |
255 | + "lazy_static", |
256 | + "log", |
257 | "mailpot", |
258 | + "mailpot-web", |
259 | + "reqwest", |
260 | + "serde", |
261 | + "serde_json", |
262 | + "stderrlog", |
263 | + "thiserror", |
264 | "tokio", |
265 | + "tower-http 0.4.0", |
266 | ] |
267 | |
268 | [[package]] |
269 | @@ -2169,6 +2340,16 @@ dependencies = [ |
270 | ] |
271 | |
272 | [[package]] |
273 | + name = "ordered-multimap" |
274 | + version = "0.4.3" |
275 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
276 | + checksum = "ccd746e37177e1711c20dd619a1620f34f5c8b569c53590a72dedd5344d8924a" |
277 | + dependencies = [ |
278 | + "dlv-list", |
279 | + "hashbrown", |
280 | + ] |
281 | + |
282 | + [[package]] |
283 | name = "output_vt100" |
284 | version = "0.1.3" |
285 | source = "registry+https://github.com/rust-lang/crates.io-index" |
286 | @@ -2207,12 +2388,62 @@ dependencies = [ |
287 | ] |
288 | |
289 | [[package]] |
290 | + name = "pathdiff" |
291 | + version = "0.2.1" |
292 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
293 | + checksum = "8835116a5c179084a830efb3adc117ab007512b535bc1a21c991d3b32a6b44dd" |
294 | + |
295 | + [[package]] |
296 | name = "percent-encoding" |
297 | version = "2.2.0" |
298 | source = "registry+https://github.com/rust-lang/crates.io-index" |
299 | checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e" |
300 | |
301 | [[package]] |
302 | + name = "pest" |
303 | + version = "2.6.0" |
304 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
305 | + checksum = "e68e84bfb01f0507134eac1e9b410a12ba379d064eab48c50ba4ce329a527b70" |
306 | + dependencies = [ |
307 | + "thiserror", |
308 | + "ucd-trie", |
309 | + ] |
310 | + |
311 | + [[package]] |
312 | + name = "pest_derive" |
313 | + version = "2.6.0" |
314 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
315 | + checksum = "6b79d4c71c865a25a4322296122e3924d30bc8ee0834c8bfc8b95f7f054afbfb" |
316 | + dependencies = [ |
317 | + "pest", |
318 | + "pest_generator", |
319 | + ] |
320 | + |
321 | + [[package]] |
322 | + name = "pest_generator" |
323 | + version = "2.6.0" |
324 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
325 | + checksum = "6c435bf1076437b851ebc8edc3a18442796b30f1728ffea6262d59bbe28b077e" |
326 | + dependencies = [ |
327 | + "pest", |
328 | + "pest_meta", |
329 | + "proc-macro2", |
330 | + "quote", |
331 | + "syn 2.0.15", |
332 | + ] |
333 | + |
334 | + [[package]] |
335 | + name = "pest_meta" |
336 | + version = "2.6.0" |
337 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
338 | + checksum = "745a452f8eb71e39ffd8ee32b3c5f51d03845f99786fa9b68db6ff509c505411" |
339 | + dependencies = [ |
340 | + "once_cell", |
341 | + "pest", |
342 | + "sha2 0.10.6", |
343 | + ] |
344 | + |
345 | + [[package]] |
346 | name = "pin-project" |
347 | version = "1.0.12" |
348 | source = "registry+https://github.com/rust-lang/crates.io-index" |
349 | @@ -2443,10 +2674,12 @@ dependencies = [ |
350 | "http", |
351 | "http-body", |
352 | "hyper", |
353 | + "hyper-tls", |
354 | "ipnet", |
355 | "js-sys", |
356 | "log", |
357 | "mime", |
358 | + "native-tls", |
359 | "once_cell", |
360 | "percent-encoding", |
361 | "pin-project-lite", |
362 | @@ -2454,6 +2687,7 @@ dependencies = [ |
363 | "serde_json", |
364 | "serde_urlencoded", |
365 | "tokio", |
366 | + "tokio-native-tls", |
367 | "tower-service", |
368 | "url", |
369 | "wasm-bindgen", |
370 | @@ -2484,6 +2718,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" |
371 | checksum = "b833d8d034ea094b1ea68aa6d5c740e0d04bad9d16568d08ba6f76823a114316" |
372 | |
373 | [[package]] |
374 | + name = "ron" |
375 | + version = "0.7.1" |
376 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
377 | + checksum = "88073939a61e5b7680558e6be56b419e208420c2adb92be54921fa6b72283f1a" |
378 | + dependencies = [ |
379 | + "base64 0.13.1", |
380 | + "bitflags", |
381 | + "serde", |
382 | + ] |
383 | + |
384 | + [[package]] |
385 | name = "rusqlite" |
386 | version = "0.28.0" |
387 | source = "registry+https://github.com/rust-lang/crates.io-index" |
388 | @@ -2500,6 +2745,16 @@ dependencies = [ |
389 | ] |
390 | |
391 | [[package]] |
392 | + name = "rust-ini" |
393 | + version = "0.18.0" |
394 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
395 | + checksum = "f6d5f2436026b4f6e79dc829837d467cc7e9a55ee40e750d716713540715a2df" |
396 | + dependencies = [ |
397 | + "cfg-if 1.0.0", |
398 | + "ordered-multimap", |
399 | + ] |
400 | + |
401 | + [[package]] |
402 | name = "rustc_version" |
403 | version = "0.4.0" |
404 | source = "registry+https://github.com/rust-lang/crates.io-index" |
405 | @@ -3006,9 +3261,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" |
406 | |
407 | [[package]] |
408 | name = "tokio" |
409 | - version = "1.28.0" |
410 | + version = "1.28.1" |
411 | source = "registry+https://github.com/rust-lang/crates.io-index" |
412 | - checksum = "c3c786bf8134e5a3a166db9b29ab8f48134739014a3eca7bc6bfa95d673b136f" |
413 | + checksum = "0aa32867d44e6f2ce3385e89dceb990188b8bb0fb25b0cf576647a6f98ac5105" |
414 | dependencies = [ |
415 | "autocfg", |
416 | "bytes", |
417 | @@ -3035,6 +3290,16 @@ dependencies = [ |
418 | ] |
419 | |
420 | [[package]] |
421 | + name = "tokio-native-tls" |
422 | + version = "0.3.1" |
423 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
424 | + checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" |
425 | + dependencies = [ |
426 | + "native-tls", |
427 | + "tokio", |
428 | + ] |
429 | + |
430 | + [[package]] |
431 | name = "tokio-util" |
432 | version = "0.7.8" |
433 | source = "registry+https://github.com/rust-lang/crates.io-index" |
434 | @@ -3098,6 +3363,7 @@ version = "0.4.0" |
435 | source = "registry+https://github.com/rust-lang/crates.io-index" |
436 | checksum = "5d1d42a9b3f3ec46ba828e8d376aec14592ea199f70a06a548587ecd1c4ab658" |
437 | dependencies = [ |
438 | + "async-compression", |
439 | "bitflags", |
440 | "bytes", |
441 | "futures-core", |
442 | @@ -3106,8 +3372,11 @@ dependencies = [ |
443 | "http-body", |
444 | "http-range-header", |
445 | "pin-project-lite", |
446 | + "tokio", |
447 | + "tokio-util", |
448 | "tower-layer", |
449 | "tower-service", |
450 | + "tracing", |
451 | ] |
452 | |
453 | [[package]] |
454 | @@ -3168,6 +3437,12 @@ source = "registry+https://github.com/rust-lang/crates.io-index" |
455 | checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" |
456 | |
457 | [[package]] |
458 | + name = "ucd-trie" |
459 | + version = "0.1.5" |
460 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
461 | + checksum = "9e79c4d996edb816c91e4308506774452e55e95c3c9de07b6729e17e15a5ef81" |
462 | + |
463 | + [[package]] |
464 | name = "unicase" |
465 | version = "2.6.0" |
466 | source = "registry+https://github.com/rust-lang/crates.io-index" |
467 | @@ -3637,6 +3912,15 @@ dependencies = [ |
468 | ] |
469 | |
470 | [[package]] |
471 | + name = "yaml-rust" |
472 | + version = "0.4.5" |
473 | + source = "registry+https://github.com/rust-lang/crates.io-index" |
474 | + checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85" |
475 | + dependencies = [ |
476 | + "linked-hash-map", |
477 | + ] |
478 | + |
479 | + [[package]] |
480 | name = "yansi" |
481 | version = "0.5.1" |
482 | source = "registry+https://github.com/rust-lang/crates.io-index" |
483 | diff --git a/rest-http/.gitignore b/rest-http/.gitignore |
484 | new file mode 100644 |
485 | index 0000000..856c436 |
486 | --- /dev/null |
487 | +++ b/rest-http/.gitignore |
488 | @@ -0,0 +1,2 @@ |
489 | + .env |
490 | + config/local.json |
491 | diff --git a/rest-http/Cargo.toml b/rest-http/Cargo.toml |
492 | index 9cd7197..b237918 100644 |
493 | --- a/rest-http/Cargo.toml |
494 | +++ b/rest-http/Cargo.toml |
495 | @@ -16,5 +16,29 @@ name = "mpot-http" |
496 | path = "src/main.rs" |
497 | |
498 | [dependencies] |
499 | + async-trait = "0.1" |
500 | + axum = { version = "0.6", features = ["headers"] } |
501 | + #jsonwebtoken = "8.3" |
502 | + bcrypt = "0.14" |
503 | + config = "0.13" |
504 | + http = "0.2" |
505 | + lazy_static = "1.4" |
506 | + log = "0.4" |
507 | mailpot = { version = "^0.1", path = "../core" } |
508 | - tokio = { version = "^1", features = ["full"] } |
509 | + mailpot-web = { version = "^0.1", path = "../web" } |
510 | + serde = { version = "1", features = ["derive"] } |
511 | + serde_json = "1" |
512 | + stderrlog = "^0.5" |
513 | + thiserror = "1" |
514 | + tokio = { version = "1", features = ["full"] } |
515 | + tower-http = { version = "0.4", features = [ |
516 | + "trace", |
517 | + "compression-br", |
518 | + "propagate-header", |
519 | + "sensitive-headers", |
520 | + "cors", |
521 | + ] } |
522 | + |
523 | + [dev-dependencies] |
524 | + assert-json-diff = "2" |
525 | + reqwest = { version = "0.11", features = ["json"] } |
526 | diff --git a/rest-http/README.md b/rest-http/README.md |
527 | index 8fac8eb..a89e59d 100644 |
528 | --- a/rest-http/README.md |
529 | +++ b/rest-http/README.md |
530 | @@ -1,3 +1,2 @@ |
531 | # mailpot REST http server |
532 | |
533 | - Not implemented. |
534 | diff --git a/rest-http/config/default.json b/rest-http/config/default.json |
535 | new file mode 100644 |
536 | index 0000000..fba51c5 |
537 | --- /dev/null |
538 | +++ b/rest-http/config/default.json |
539 | @@ -0,0 +1,12 @@ |
540 | + { |
541 | + "environment": "development", |
542 | + "server": { |
543 | + "port": 8080 |
544 | + }, |
545 | + "auth": { |
546 | + "secret": "secret" |
547 | + }, |
548 | + "logger": { |
549 | + "level": "debug" |
550 | + } |
551 | + } |
552 | diff --git a/rest-http/config/production.json b/rest-http/config/production.json |
553 | new file mode 100644 |
554 | index 0000000..0b731fa |
555 | --- /dev/null |
556 | +++ b/rest-http/config/production.json |
557 | @@ -0,0 +1,6 @@ |
558 | + { |
559 | + "environment": "production", |
560 | + "logger": { |
561 | + "level": "info" |
562 | + } |
563 | + } |
564 | diff --git a/rest-http/config/test.json b/rest-http/config/test.json |
565 | new file mode 100644 |
566 | index 0000000..a162f57 |
567 | --- /dev/null |
568 | +++ b/rest-http/config/test.json |
569 | @@ -0,0 +1,9 @@ |
570 | + { |
571 | + "environment": "test", |
572 | + "server": { |
573 | + "port": 8088 |
574 | + }, |
575 | + "logger": { |
576 | + "level": "error" |
577 | + } |
578 | + } |
579 | diff --git a/rest-http/src/errors.rs b/rest-http/src/errors.rs |
580 | new file mode 100644 |
581 | index 0000000..7d78020 |
582 | --- /dev/null |
583 | +++ b/rest-http/src/errors.rs |
584 | @@ -0,0 +1,98 @@ |
585 | + use axum::{ |
586 | + http::StatusCode, |
587 | + response::{IntoResponse, Response}, |
588 | + Json, |
589 | + }; |
590 | + use bcrypt::BcryptError; |
591 | + use serde_json::json; |
592 | + use tokio::task::JoinError; |
593 | + |
594 | + #[derive(thiserror::Error, Debug)] |
595 | + #[error("...")] |
596 | + pub enum Error { |
597 | + #[error("Error parsing ObjectID {0}")] |
598 | + ParseObjectID(String), |
599 | + |
600 | + #[error("{0}")] |
601 | + Authenticate(#[from] AuthenticateError), |
602 | + |
603 | + #[error("{0}")] |
604 | + BadRequest(#[from] BadRequest), |
605 | + |
606 | + #[error("{0}")] |
607 | + NotFound(#[from] NotFound), |
608 | + |
609 | + #[error("{0}")] |
610 | + RunSyncTask(#[from] JoinError), |
611 | + |
612 | + #[error("{0}")] |
613 | + HashPassword(#[from] BcryptError), |
614 | + |
615 | + #[error("{0}")] |
616 | + System(#[from] mailpot::Error), |
617 | + } |
618 | + |
619 | + impl Error { |
620 | + fn get_codes(&self) -> (StatusCode, u16) { |
621 | + match *self { |
622 | + // 4XX Errors |
623 | + Error::ParseObjectID(_) => (StatusCode::BAD_REQUEST, 40001), |
624 | + Error::BadRequest(_) => (StatusCode::BAD_REQUEST, 40002), |
625 | + Error::NotFound(_) => (StatusCode::NOT_FOUND, 40003), |
626 | + Error::Authenticate(AuthenticateError::WrongCredentials) => { |
627 | + (StatusCode::UNAUTHORIZED, 40004) |
628 | + } |
629 | + Error::Authenticate(AuthenticateError::InvalidToken) => { |
630 | + (StatusCode::UNAUTHORIZED, 40005) |
631 | + } |
632 | + Error::Authenticate(AuthenticateError::Locked) => (StatusCode::LOCKED, 40006), |
633 | + |
634 | + // 5XX Errors |
635 | + Error::Authenticate(AuthenticateError::TokenCreation) => { |
636 | + (StatusCode::INTERNAL_SERVER_ERROR, 5001) |
637 | + } |
638 | + Error::RunSyncTask(_) => (StatusCode::INTERNAL_SERVER_ERROR, 5005), |
639 | + Error::HashPassword(_) => (StatusCode::INTERNAL_SERVER_ERROR, 5006), |
640 | + Error::System(_) => (StatusCode::INTERNAL_SERVER_ERROR, 5007), |
641 | + } |
642 | + } |
643 | + |
644 | + pub fn bad_request() -> Self { |
645 | + Error::BadRequest(BadRequest {}) |
646 | + } |
647 | + |
648 | + pub fn not_found() -> Self { |
649 | + Error::NotFound(NotFound {}) |
650 | + } |
651 | + } |
652 | + |
653 | + impl IntoResponse for Error { |
654 | + fn into_response(self) -> Response { |
655 | + let (status_code, code) = self.get_codes(); |
656 | + let message = self.to_string(); |
657 | + let body = Json(json!({ "code": code, "message": message })); |
658 | + |
659 | + (status_code, body).into_response() |
660 | + } |
661 | + } |
662 | + |
663 | + #[derive(thiserror::Error, Debug)] |
664 | + #[error("...")] |
665 | + pub enum AuthenticateError { |
666 | + #[error("Wrong authentication credentials")] |
667 | + WrongCredentials, |
668 | + #[error("Failed to create authentication token")] |
669 | + TokenCreation, |
670 | + #[error("Invalid authentication credentials")] |
671 | + InvalidToken, |
672 | + #[error("User is locked")] |
673 | + Locked, |
674 | + } |
675 | + |
676 | + #[derive(thiserror::Error, Debug)] |
677 | + #[error("Bad Request")] |
678 | + pub struct BadRequest {} |
679 | + |
680 | + #[derive(thiserror::Error, Debug)] |
681 | + #[error("Not found")] |
682 | + pub struct NotFound {} |
683 | diff --git a/rest-http/src/lib.rs b/rest-http/src/lib.rs |
684 | index 48813d6..6bc9fc6 100644 |
685 | --- a/rest-http/src/lib.rs |
686 | +++ b/rest-http/src/lib.rs |
687 | @@ -16,3 +16,13 @@ |
688 | * You should have received a copy of the GNU Affero General Public License |
689 | * along with this program. If not, see <https://www.gnu.org/licenses/>. |
690 | */ |
691 | + |
692 | + pub use std::{net::SocketAddr, sync::Arc}; |
693 | + |
694 | + pub use axum::Router; |
695 | + pub use http::header; |
696 | + pub use log::{debug, info, trace}; |
697 | + pub use mailpot::{models::*, Configuration, Connection}; |
698 | + pub mod errors; |
699 | + pub mod routes; |
700 | + pub mod settings; |
701 | diff --git a/rest-http/src/main.rs b/rest-http/src/main.rs |
702 | index 17d836f..c86a5ef 100644 |
703 | --- a/rest-http/src/main.rs |
704 | +++ b/rest-http/src/main.rs |
705 | @@ -1,20 +1,50 @@ |
706 | - /* |
707 | - * This file is part of mailpot |
708 | - * |
709 | - * Copyright 2020 - Manos Pitsidianakis |
710 | - * |
711 | - * This program is free software: you can redistribute it and/or modify |
712 | - * it under the terms of the GNU Affero General Public License as |
713 | - * published by the Free Software Foundation, either version 3 of the |
714 | - * License, or (at your option) any later version. |
715 | - * |
716 | - * This program is distributed in the hope that it will be useful, |
717 | - * but WITHOUT ANY WARRANTY; without even the implied warranty of |
718 | - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
719 | - * GNU Affero General Public License for more details. |
720 | - * |
721 | - * You should have received a copy of the GNU Affero General Public License |
722 | - * along with this program. If not, see <https://www.gnu.org/licenses/>. |
723 | - */ |
724 | + use mailpot_http::{settings::SETTINGS, *}; |
725 | + use tower_http::{ |
726 | + compression::CompressionLayer, cors::CorsLayer, propagate_header::PropagateHeaderLayer, |
727 | + sensitive_headers::SetSensitiveHeadersLayer, |
728 | + }; |
729 | |
730 | - fn main() {} |
731 | + use crate::routes; |
732 | + |
733 | + #[tokio::main] |
734 | + async fn main() { |
735 | + let app = create_app().await; |
736 | + |
737 | + let port = SETTINGS.server.port; |
738 | + let address = SocketAddr::from(([127, 0, 0, 1], port)); |
739 | + |
740 | + info!("Server listening on {}", &address); |
741 | + axum::Server::bind(&address) |
742 | + .serve(app.into_make_service()) |
743 | + .await |
744 | + .expect("Failed to start server"); |
745 | + } |
746 | + pub async fn create_app() -> Router { |
747 | + let config_path = std::env::args() |
748 | + .nth(1) |
749 | + .expect("Expected configuration file path as first argument."); |
750 | + stderrlog::new() |
751 | + .quiet(false) |
752 | + .verbosity(15) |
753 | + .show_module_names(true) |
754 | + .timestamp(stderrlog::Timestamp::Millisecond) |
755 | + .init() |
756 | + .unwrap(); |
757 | + let conf = Arc::new(Configuration::from_file(config_path).unwrap()); |
758 | + |
759 | + Router::new() |
760 | + .with_state(conf.clone()) |
761 | + .merge(Router::new().nest("/v1", Router::new().merge(routes::list::create_route(conf)))) |
762 | + .layer(SetSensitiveHeadersLayer::new(std::iter::once( |
763 | + header::AUTHORIZATION, |
764 | + ))) |
765 | + // Compress responses |
766 | + .layer(CompressionLayer::new()) |
767 | + // Propagate `X-Request-Id`s from requests to responses |
768 | + .layer(PropagateHeaderLayer::new(header::HeaderName::from_static( |
769 | + "x-request-id", |
770 | + ))) |
771 | + // CORS configuration. This should probably be more restrictive in |
772 | + // production. |
773 | + .layer(CorsLayer::permissive()) |
774 | + } |
775 | diff --git a/rest-http/src/routes/list.rs b/rest-http/src/routes/list.rs |
776 | new file mode 100644 |
777 | index 0000000..6aa7f7d |
778 | --- /dev/null |
779 | +++ b/rest-http/src/routes/list.rs |
780 | @@ -0,0 +1,107 @@ |
781 | + use std::sync::Arc; |
782 | + |
783 | + pub use axum::extract::{Path, Query, State}; |
784 | + use axum::{ |
785 | + http::StatusCode, |
786 | + routing::{get, post}, |
787 | + Json, Router, |
788 | + }; |
789 | + use mailpot_web::{typed_paths::*, ResponseError, RouterExt}; |
790 | + use serde::{Deserialize, Serialize}; |
791 | + |
792 | + use crate::*; |
793 | + |
794 | + pub fn create_route(conf: Arc<Configuration>) -> Router { |
795 | + Router::new() |
796 | + .route("/list/", get(all_lists)) |
797 | + .route("/list/", post(post_list)) |
798 | + .typed_get(get_list) |
799 | + .with_state(conf) |
800 | + } |
801 | + |
802 | + async fn get_list( |
803 | + ListPath(id): ListPath, |
804 | + State(state): State<Arc<Configuration>>, |
805 | + ) -> Result<Json<DbVal<MailingList>>, ResponseError> { |
806 | + let db = Connection::open_db(Configuration::clone(&state))?; |
807 | + let Some(list) = (match id { |
808 | + ListPathIdentifier::Pk(id) => db.list(id)?, |
809 | + ListPathIdentifier::Id(id) => db.list_by_id(id)?, |
810 | + }) else { |
811 | + return Err(mailpot_web::ResponseError::new( |
812 | + "Not found".to_string(), |
813 | + StatusCode::NOT_FOUND, |
814 | + )); |
815 | + }; |
816 | + Ok(Json(list)) |
817 | + } |
818 | + |
819 | + async fn all_lists( |
820 | + Query(GetRequest { |
821 | + filter: _, |
822 | + count, |
823 | + page, |
824 | + }): Query<GetRequest>, |
825 | + State(state): State<Arc<Configuration>>, |
826 | + ) -> Result<Json<GetResponse>, ResponseError> { |
827 | + let db = Connection::open_db(Configuration::clone(&state))?; |
828 | + let lists_values = db.lists()?; |
829 | + let page = page.unwrap_or(0); |
830 | + let Some(count) = count else { |
831 | + let mut stmt = db |
832 | + .connection |
833 | + .prepare("SELECT count(*) FROM list;")?; |
834 | + return Ok(Json(GetResponse { |
835 | + entries: vec![], |
836 | + total: stmt.query_row([], |row| { |
837 | + let count: usize = row.get(0)?; |
838 | + Ok(count) |
839 | + })?, |
840 | + start: 0, |
841 | + })); |
842 | + }; |
843 | + let offset = page * count; |
844 | + let res: Vec<_> = lists_values.into_iter().skip(offset).take(count).collect(); |
845 | + |
846 | + Ok(Json(GetResponse { |
847 | + total: res.len(), |
848 | + start: offset, |
849 | + entries: res, |
850 | + })) |
851 | + } |
852 | + |
853 | + async fn post_list( |
854 | + State(state): State<Arc<Configuration>>, |
855 | + Json(_body): Json<GetRequest>, |
856 | + ) -> Result<Json<()>, ResponseError> { |
857 | + let _db = Connection::open_db(Configuration::clone(&state))?; |
858 | + // let password_hash = list::hash_password(body.password).await?; |
859 | + // let list = list::new(body.name, body.email, password_hash); |
860 | + // let list = list::create(list).await?; |
861 | + // let res = Publiclist::from(list); |
862 | + // |
863 | + |
864 | + Ok(Json(())) |
865 | + } |
866 | + |
867 | + #[derive(Debug, Serialize, Deserialize)] |
868 | + enum GetFilter { |
869 | + Pk(i64), |
870 | + Address(String), |
871 | + Id(String), |
872 | + Name(String), |
873 | + } |
874 | + |
875 | + #[derive(Debug, Serialize, Deserialize)] |
876 | + struct GetRequest { |
877 | + filter: Option<GetFilter>, |
878 | + count: Option<usize>, |
879 | + page: Option<usize>, |
880 | + } |
881 | + |
882 | + #[derive(Debug, Serialize, Deserialize)] |
883 | + struct GetResponse { |
884 | + entries: Vec<DbVal<MailingList>>, |
885 | + total: usize, |
886 | + start: usize, |
887 | + } |
888 | diff --git a/rest-http/src/routes/mod.rs b/rest-http/src/routes/mod.rs |
889 | new file mode 100644 |
890 | index 0000000..d17e233 |
891 | --- /dev/null |
892 | +++ b/rest-http/src/routes/mod.rs |
893 | @@ -0,0 +1 @@ |
894 | + pub mod list; |
895 | diff --git a/rest-http/src/settings.rs b/rest-http/src/settings.rs |
896 | new file mode 100644 |
897 | index 0000000..b1ef467 |
898 | --- /dev/null |
899 | +++ b/rest-http/src/settings.rs |
900 | @@ -0,0 +1,61 @@ |
901 | + use std::{env, fmt}; |
902 | + |
903 | + use config::{Config, ConfigError, Environment, File}; |
904 | + use lazy_static::lazy_static; |
905 | + use serde::Deserialize; |
906 | + |
907 | + lazy_static! { |
908 | + pub static ref SETTINGS: Settings = Settings::new().expect("Failed to setup settings"); |
909 | + } |
910 | + |
911 | + #[derive(Debug, Clone, Deserialize)] |
912 | + pub struct Server { |
913 | + pub port: u16, |
914 | + } |
915 | + |
916 | + #[derive(Debug, Clone, Deserialize)] |
917 | + pub struct Logger { |
918 | + pub level: String, |
919 | + } |
920 | + |
921 | + #[derive(Debug, Clone, Deserialize)] |
922 | + pub struct Auth { |
923 | + pub secret: String, |
924 | + } |
925 | + |
926 | + #[derive(Debug, Clone, Deserialize)] |
927 | + pub struct Settings { |
928 | + pub environment: String, |
929 | + pub server: Server, |
930 | + pub logger: Logger, |
931 | + pub auth: Auth, |
932 | + } |
933 | + |
934 | + impl Settings { |
935 | + pub fn new() -> Result<Self, ConfigError> { |
936 | + let run_mode = env::var("RUN_MODE").unwrap_or_else(|_| "development".into()); |
937 | + |
938 | + let mut builder = Config::builder() |
939 | + .add_source(File::with_name("config/default")) |
940 | + .add_source(File::with_name(&format!("config/{run_mode}")).required(false)) |
941 | + .add_source(File::with_name("config/local").required(false)) |
942 | + .add_source(Environment::default().separator("__")); |
943 | + |
944 | + // Some cloud services like Heroku exposes a randomly assigned port in |
945 | + // the PORT env var and there is no way to change the env var name. |
946 | + if let Ok(port) = env::var("PORT") { |
947 | + builder = builder.set_override("server.port", port)?; |
948 | + } |
949 | + |
950 | + builder |
951 | + .build()? |
952 | + // Deserialize (and thus freeze) the entire configuration. |
953 | + .try_deserialize() |
954 | + } |
955 | + } |
956 | + |
957 | + impl fmt::Display for Server { |
958 | + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { |
959 | + write!(f, "http://localhost:{}", &self.port) |
960 | + } |
961 | + } |