judu/ex-egopublic Fork
⑂ 7255330
Commits
⬇ Clone ▾
git clone https://git.rickub.com/judu/ex-ego.git
git clone ssh://git@rickub.com/judu/ex-ego.git

Host key fingerprint (ed25519): SHA256:iycHnxEyq0Q7uyVpB7JlznP0G7JrTPXLYRcAU5CSLhc — verify it before your first connect.

Download ZIP

feat: application complète de suivi de tempsUnverified

Serveur axum + askama + sqlx (Postgres sslmode=require), auth par
biscuit en cookie (6 mois) avec liste de révocation cachée en mémoire,
CSRF stateless HMAC, stats en SVG serveur (barres empilées par tâche),
PWA minimale, CLI clap (serve / user add / keygen). IDs en UUIDv7
générés côté Rust.
Julien Durillon committed 2026-08-06T18:20:50+02:00 Browse files
7255330
added .gitignore +1 -0
new file mode 100644
@@ -0,0 +1 @@
1+/target
new file mode 100644
@@ -0,0 +1 @@
1+/target
added Cargo.lock +3179 -0
new file mode 100644
@@ -0,0 +1,3179 @@
1+# This file is automatically @generated by Cargo.
2+# It is not intended for manual editing.
3+version = 4
4+
5+[[package]]
6+name = "aho-corasick"
7+version = "1.1.5"
8+source = "registry+https://github.com/rust-lang/crates.io-index"
9+checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
10+dependencies = [
11+ "memchr",
12+]
13+
14+[[package]]
15+name = "allocator-api2"
16+version = "0.2.21"
17+source = "registry+https://github.com/rust-lang/crates.io-index"
18+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
19+
20+[[package]]
21+name = "android_system_properties"
22+version = "0.1.6"
23+source = "registry+https://github.com/rust-lang/crates.io-index"
24+checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc"
25+dependencies = [
26+ "libc",
27+]
28+
29+[[package]]
30+name = "anstream"
31+version = "1.0.0"
32+source = "registry+https://github.com/rust-lang/crates.io-index"
33+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
34+dependencies = [
35+ "anstyle",
36+ "anstyle-parse",
37+ "anstyle-query",
38+ "anstyle-wincon",
39+ "colorchoice",
40+ "is_terminal_polyfill",
41+ "utf8parse",
42+]
43+
44+[[package]]
45+name = "anstyle"
46+version = "1.0.14"
47+source = "registry+https://github.com/rust-lang/crates.io-index"
48+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
49+
50+[[package]]
51+name = "anstyle-parse"
52+version = "1.0.0"
53+source = "registry+https://github.com/rust-lang/crates.io-index"
54+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
55+dependencies = [
56+ "utf8parse",
57+]
58+
59+[[package]]
60+name = "anstyle-query"
61+version = "1.1.5"
62+source = "registry+https://github.com/rust-lang/crates.io-index"
63+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
64+dependencies = [
65+ "windows-sys 0.61.2",
66+]
67+
68+[[package]]
69+name = "anstyle-wincon"
70+version = "3.0.11"
71+source = "registry+https://github.com/rust-lang/crates.io-index"
72+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
73+dependencies = [
74+ "anstyle",
75+ "once_cell_polyfill",
76+ "windows-sys 0.61.2",
77+]
78+
79+[[package]]
80+name = "anyhow"
81+version = "1.0.104"
82+source = "registry+https://github.com/rust-lang/crates.io-index"
83+checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
84+
85+[[package]]
86+name = "argon2"
87+version = "0.5.3"
88+source = "registry+https://github.com/rust-lang/crates.io-index"
89+checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
90+dependencies = [
91+ "base64ct",
92+ "blake2",
93+ "cpufeatures",
94+ "password-hash",
95+]
96+
97+[[package]]
98+name = "askama"
99+version = "0.16.0"
100+source = "registry+https://github.com/rust-lang/crates.io-index"
101+checksum = "f1bf825125edd887a019d0a3a837dcc5499a68b0d034cc3eb594070c3e18addc"
102+dependencies = [
103+ "askama_macros",
104+ "itoa",
105+ "percent-encoding",
106+ "serde",
107+ "serde_json",
108+]
109+
110+[[package]]
111+name = "askama_derive"
112+version = "0.16.0"
113+source = "registry+https://github.com/rust-lang/crates.io-index"
114+checksum = "e1c7065972a130eafa84215f21352ae15b4a7393da48c1f5e103904490736738"
115+dependencies = [
116+ "askama_parser",
117+ "basic-toml",
118+ "glob",
119+ "memchr",
120+ "proc-macro2",
121+ "quote",
122+ "rustc-hash",
123+ "serde",
124+ "serde_derive",
125+ "syn 2.0.119",
126+]
127+
128+[[package]]
129+name = "askama_macros"
130+version = "0.16.0"
131+source = "registry+https://github.com/rust-lang/crates.io-index"
132+checksum = "0e23b1d2c4bd39a41971f6124cef4cc6fd0540913ecb90919b69ab3bbe44ae1a"
133+dependencies = [
134+ "askama_derive",
135+]
136+
137+[[package]]
138+name = "askama_parser"
139+version = "0.16.0"
140+source = "registry+https://github.com/rust-lang/crates.io-index"
141+checksum = "7db09fde9143e7ac4513358fb32ee32847125b63b18ea715afd487956da715da"
142+dependencies = [
143+ "rustc-hash",
144+ "serde",
145+ "serde_derive",
146+ "unicode-ident",
147+ "winnow",
148+]
149+
150+[[package]]
151+name = "atoi"
152+version = "2.0.0"
153+source = "registry+https://github.com/rust-lang/crates.io-index"
154+checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
155+dependencies = [
156+ "num-traits",
157+]
158+
159+[[package]]
160+name = "atomic-waker"
161+version = "1.1.2"
162+source = "registry+https://github.com/rust-lang/crates.io-index"
163+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
164+
165+[[package]]
166+name = "autocfg"
167+version = "1.5.1"
168+source = "registry+https://github.com/rust-lang/crates.io-index"
169+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
170+
171+[[package]]
172+name = "axum"
173+version = "0.8.9"
174+source = "registry+https://github.com/rust-lang/crates.io-index"
175+checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
176+dependencies = [
177+ "axum-core",
178+ "bytes",
179+ "form_urlencoded",
180+ "futures-util",
181+ "http",
182+ "http-body",
183+ "http-body-util",
184+ "hyper",
185+ "hyper-util",
186+ "itoa",
187+ "matchit",
188+ "memchr",
189+ "mime",
190+ "percent-encoding",
191+ "pin-project-lite",
192+ "serde_core",
193+ "serde_json",
194+ "serde_path_to_error",
195+ "serde_urlencoded",
196+ "sync_wrapper",
197+ "tokio",
198+ "tower",
199+ "tower-layer",
200+ "tower-service",
201+ "tracing",
202+]
203+
204+[[package]]
205+name = "axum-core"
206+version = "0.5.6"
207+source = "registry+https://github.com/rust-lang/crates.io-index"
208+checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
209+dependencies = [
210+ "bytes",
211+ "futures-core",
212+ "http",
213+ "http-body",
214+ "http-body-util",
215+ "mime",
216+ "pin-project-lite",
217+ "sync_wrapper",
218+ "tower-layer",
219+ "tower-service",
220+ "tracing",
221+]
222+
223+[[package]]
224+name = "axum-extra"
225+version = "0.12.6"
226+source = "registry+https://github.com/rust-lang/crates.io-index"
227+checksum = "be44683b41ccb9ab2d23a5230015c9c3c55be97a25e4428366de8873103f7970"
228+dependencies = [
229+ "axum",
230+ "axum-core",
231+ "bytes",
232+ "cookie",
233+ "futures-core",
234+ "futures-util",
235+ "http",
236+ "http-body",
237+ "http-body-util",
238+ "mime",
239+ "pin-project-lite",
240+ "tower-layer",
241+ "tower-service",
242+ "tracing",
243+]
244+
245+[[package]]
246+name = "base16ct"
247+version = "0.2.0"
248+source = "registry+https://github.com/rust-lang/crates.io-index"
249+checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
250+
251+[[package]]
252+name = "base64"
253+version = "0.13.1"
254+source = "registry+https://github.com/rust-lang/crates.io-index"
255+checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
256+
257+[[package]]
258+name = "base64"
259+version = "0.22.1"
260+source = "registry+https://github.com/rust-lang/crates.io-index"
261+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
262+
263+[[package]]
264+name = "base64ct"
265+version = "1.8.3"
266+source = "registry+https://github.com/rust-lang/crates.io-index"
267+checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
268+
269+[[package]]
270+name = "basic-toml"
271+version = "0.1.10"
272+source = "registry+https://github.com/rust-lang/crates.io-index"
273+checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a"
274+dependencies = [
275+ "serde",
276+]
277+
278+[[package]]
279+name = "biscuit-auth"
280+version = "6.0.0"
281+source = "registry+https://github.com/rust-lang/crates.io-index"
282+checksum = "d5884fc86b3e21f5649ef4326e17ef729b3096e6502deaf13db7b7fb05bb992b"
283+dependencies = [
284+ "base64 0.13.1",
285+ "biscuit-parser",
286+ "biscuit-quote",
287+ "ecdsa",
288+ "ed25519-dalek",
289+ "elliptic-curve",
290+ "getrandom 0.2.17",
291+ "hex",
292+ "nom",
293+ "p256",
294+ "pkcs8 0.9.0",
295+ "prost",
296+ "prost-types",
297+ "rand",
298+ "rand_core",
299+ "regex",
300+ "serde_json",
301+ "sha2 0.9.9",
302+ "thiserror 1.0.69",
303+ "time",
304+ "zeroize",
305+]
306+
307+[[package]]
308+name = "biscuit-parser"
309+version = "0.2.0"
310+source = "registry+https://github.com/rust-lang/crates.io-index"
311+checksum = "9d7cafdbc8c30e1f0fb87df7161bec77f6f00da652cc33f102b0f95bd1cbc0fa"
312+dependencies = [
313+ "hex",
314+ "nom",
315+ "proc-macro2",
316+ "quote",
317+ "thiserror 1.0.69",
318+ "time",
319+]
320+
321+[[package]]
322+name = "biscuit-quote"
323+version = "0.3.0"
324+source = "registry+https://github.com/rust-lang/crates.io-index"
325+checksum = "49d2332c742a07a846f1fb2760e58a0ee60f2bc30987046fcea816b40630335a"
326+dependencies = [
327+ "biscuit-parser",
328+ "proc-macro-error2",
329+ "proc-macro2",
330+ "quote",
331+ "syn 1.0.109",
332+]
333+
334+[[package]]
335+name = "bitflags"
336+version = "2.13.1"
337+source = "registry+https://github.com/rust-lang/crates.io-index"
338+checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
339+dependencies = [
340+ "serde_core",
341+]
342+
343+[[package]]
344+name = "blake2"
345+version = "0.10.6"
346+source = "registry+https://github.com/rust-lang/crates.io-index"
347+checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
348+dependencies = [
349+ "digest 0.10.7",
350+]
351+
352+[[package]]
353+name = "block-buffer"
354+version = "0.9.0"
355+source = "registry+https://github.com/rust-lang/crates.io-index"
356+checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
357+dependencies = [
358+ "generic-array",
359+]
360+
361+[[package]]
362+name = "block-buffer"
363+version = "0.10.4"
364+source = "registry+https://github.com/rust-lang/crates.io-index"
365+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
366+dependencies = [
367+ "generic-array",
368+]
369+
370+[[package]]
371+name = "bumpalo"
372+version = "3.20.3"
373+source = "registry+https://github.com/rust-lang/crates.io-index"
374+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
375+
376+[[package]]
377+name = "byteorder"
378+version = "1.5.0"
379+source = "registry+https://github.com/rust-lang/crates.io-index"
380+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
381+
382+[[package]]
383+name = "bytes"
384+version = "1.12.1"
385+source = "registry+https://github.com/rust-lang/crates.io-index"
386+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
387+
388+[[package]]
389+name = "cc"
390+version = "1.4.0"
391+source = "registry+https://github.com/rust-lang/crates.io-index"
392+checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9"
393+dependencies = [
394+ "find-msvc-tools",
395+ "shlex",
396+]
397+
398+[[package]]
399+name = "cfg-if"
400+version = "1.0.4"
401+source = "registry+https://github.com/rust-lang/crates.io-index"
402+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
403+
404+[[package]]
405+name = "chrono"
406+version = "0.4.45"
407+source = "registry+https://github.com/rust-lang/crates.io-index"
408+checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
409+dependencies = [
410+ "iana-time-zone",
411+ "js-sys",
412+ "num-traits",
413+ "serde",
414+ "wasm-bindgen",
415+ "windows-link",
416+]
417+
418+[[package]]
419+name = "chrono-tz"
420+version = "0.10.4"
421+source = "registry+https://github.com/rust-lang/crates.io-index"
422+checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3"
423+dependencies = [
424+ "chrono",
425+ "phf",
426+]
427+
428+[[package]]
429+name = "clap"
430+version = "4.6.6"
431+source = "registry+https://github.com/rust-lang/crates.io-index"
432+checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
433+dependencies = [
434+ "clap_builder",
435+ "clap_derive",
436+]
437+
438+[[package]]
439+name = "clap_builder"
440+version = "4.6.6"
441+source = "registry+https://github.com/rust-lang/crates.io-index"
442+checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
443+dependencies = [
444+ "anstream",
445+ "anstyle",
446+ "clap_lex",
447+ "strsim",
448+]
449+
450+[[package]]
451+name = "clap_derive"
452+version = "4.6.4"
453+source = "registry+https://github.com/rust-lang/crates.io-index"
454+checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
455+dependencies = [
456+ "heck",
457+ "proc-macro2",
458+ "quote",
459+ "syn 3.0.3",
460+]
461+
462+[[package]]
463+name = "clap_lex"
464+version = "1.1.0"
465+source = "registry+https://github.com/rust-lang/crates.io-index"
466+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
467+
468+[[package]]
469+name = "colorchoice"
470+version = "1.0.5"
471+source = "registry+https://github.com/rust-lang/crates.io-index"
472+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
473+
474+[[package]]
475+name = "const-oid"
476+version = "0.9.6"
477+source = "registry+https://github.com/rust-lang/crates.io-index"
478+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
479+
480+[[package]]
481+name = "cookie"
482+version = "0.18.1"
483+source = "registry+https://github.com/rust-lang/crates.io-index"
484+checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
485+dependencies = [
486+ "percent-encoding",
487+ "time",
488+ "version_check",
489+]
490+
491+[[package]]
492+name = "core-foundation-sys"
493+version = "0.8.7"
494+source = "registry+https://github.com/rust-lang/crates.io-index"
495+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
496+
497+[[package]]
498+name = "cpufeatures"
499+version = "0.2.17"
500+source = "registry+https://github.com/rust-lang/crates.io-index"
501+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
502+dependencies = [
503+ "libc",
504+]
505+
506+[[package]]
507+name = "crc"
508+version = "3.4.0"
509+source = "registry+https://github.com/rust-lang/crates.io-index"
510+checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
511+dependencies = [
512+ "crc-catalog",
513+]
514+
515+[[package]]
516+name = "crc-catalog"
517+version = "2.5.0"
518+source = "registry+https://github.com/rust-lang/crates.io-index"
519+checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853"
520+
521+[[package]]
522+name = "crossbeam-queue"
523+version = "0.3.13"
524+source = "registry+https://github.com/rust-lang/crates.io-index"
525+checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26"
526+dependencies = [
527+ "crossbeam-utils",
528+]
529+
530+[[package]]
531+name = "crossbeam-utils"
532+version = "0.8.22"
533+source = "registry+https://github.com/rust-lang/crates.io-index"
534+checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
535+
536+[[package]]
537+name = "crypto-bigint"
538+version = "0.5.5"
539+source = "registry+https://github.com/rust-lang/crates.io-index"
540+checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
541+dependencies = [
542+ "generic-array",
543+ "rand_core",
544+ "subtle",
545+ "zeroize",
546+]
547+
548+[[package]]
549+name = "crypto-common"
550+version = "0.1.6"
551+source = "registry+https://github.com/rust-lang/crates.io-index"
552+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
553+dependencies = [
554+ "generic-array",
555+ "typenum",
556+]
557+
558+[[package]]
559+name = "curve25519-dalek"
560+version = "4.1.3"
561+source = "registry+https://github.com/rust-lang/crates.io-index"
562+checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
563+dependencies = [
564+ "cfg-if",
565+ "cpufeatures",
566+ "curve25519-dalek-derive",
567+ "digest 0.10.7",
568+ "fiat-crypto",
569+ "rustc_version",
570+ "subtle",
571+ "zeroize",
572+]
573+
574+[[package]]
575+name = "curve25519-dalek-derive"
576+version = "0.1.1"
577+source = "registry+https://github.com/rust-lang/crates.io-index"
578+checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
579+dependencies = [
580+ "proc-macro2",
581+ "quote",
582+ "syn 2.0.119",
583+]
584+
585+[[package]]
586+name = "der"
587+version = "0.6.1"
588+source = "registry+https://github.com/rust-lang/crates.io-index"
589+checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de"
590+dependencies = [
591+ "const-oid",
592+]
593+
594+[[package]]
595+name = "der"
596+version = "0.7.10"
597+source = "registry+https://github.com/rust-lang/crates.io-index"
598+checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
599+dependencies = [
600+ "const-oid",
601+ "pem-rfc7468",
602+ "zeroize",
603+]
604+
605+[[package]]
606+name = "deranged"
607+version = "0.5.8"
608+source = "registry+https://github.com/rust-lang/crates.io-index"
609+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
610+
611+[[package]]
612+name = "digest"
613+version = "0.9.0"
614+source = "registry+https://github.com/rust-lang/crates.io-index"
615+checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
616+dependencies = [
617+ "generic-array",
618+]
619+
620+[[package]]
621+name = "digest"
622+version = "0.10.7"
623+source = "registry+https://github.com/rust-lang/crates.io-index"
624+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
625+dependencies = [
626+ "block-buffer 0.10.4",
627+ "const-oid",
628+ "crypto-common",
629+ "subtle",
630+]
631+
632+[[package]]
633+name = "displaydoc"
634+version = "0.2.7"
635+source = "registry+https://github.com/rust-lang/crates.io-index"
636+checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
637+dependencies = [
638+ "proc-macro2",
639+ "quote",
640+ "syn 3.0.3",
641+]
642+
643+[[package]]
644+name = "dotenvy"
645+version = "0.15.7"
646+source = "registry+https://github.com/rust-lang/crates.io-index"
647+checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
648+
649+[[package]]
650+name = "ecdsa"
651+version = "0.16.9"
652+source = "registry+https://github.com/rust-lang/crates.io-index"
653+checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
654+dependencies = [
655+ "der 0.7.10",
656+ "digest 0.10.7",
657+ "elliptic-curve",
658+ "rfc6979",
659+ "serdect",
660+ "signature",
661+ "spki 0.7.3",
662+]
663+
664+[[package]]
665+name = "ed25519"
666+version = "2.2.3"
667+source = "registry+https://github.com/rust-lang/crates.io-index"
668+checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
669+dependencies = [
670+ "pkcs8 0.10.2",
671+ "signature",
672+]
673+
674+[[package]]
675+name = "ed25519-dalek"
676+version = "2.2.0"
677+source = "registry+https://github.com/rust-lang/crates.io-index"
678+checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
679+dependencies = [
680+ "curve25519-dalek",
681+ "ed25519",
682+ "rand_core",
683+ "serde",
684+ "sha2 0.10.9",
685+ "subtle",
686+ "zeroize",
687+]
688+
689+[[package]]
690+name = "either"
691+version = "1.17.0"
692+source = "registry+https://github.com/rust-lang/crates.io-index"
693+checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
694+dependencies = [
695+ "serde",
696+]
697+
698+[[package]]
699+name = "elliptic-curve"
700+version = "0.13.8"
701+source = "registry+https://github.com/rust-lang/crates.io-index"
702+checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
703+dependencies = [
704+ "base16ct",
705+ "crypto-bigint",
706+ "digest 0.10.7",
707+ "ff",
708+ "generic-array",
709+ "group",
710+ "pem-rfc7468",
711+ "pkcs8 0.10.2",
712+ "rand_core",
713+ "sec1",
714+ "serdect",
715+ "subtle",
716+ "zeroize",
717+]
718+
719+[[package]]
720+name = "equivalent"
721+version = "1.0.2"
722+source = "registry+https://github.com/rust-lang/crates.io-index"
723+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
724+
725+[[package]]
726+name = "errno"
727+version = "0.3.14"
728+source = "registry+https://github.com/rust-lang/crates.io-index"
729+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
730+dependencies = [
731+ "libc",
732+ "windows-sys 0.61.2",
733+]
734+
735+[[package]]
736+name = "etcetera"
737+version = "0.8.0"
738+source = "registry+https://github.com/rust-lang/crates.io-index"
739+checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
740+dependencies = [
741+ "cfg-if",
742+ "home",
743+ "windows-sys 0.48.0",
744+]
745+
746+[[package]]
747+name = "event-listener"
748+version = "5.4.2"
749+source = "registry+https://github.com/rust-lang/crates.io-index"
750+checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2"
751+dependencies = [
752+ "parking",
753+ "pin-project-lite",
754+]
755+
756+[[package]]
757+name = "ff"
758+version = "0.13.1"
759+source = "registry+https://github.com/rust-lang/crates.io-index"
760+checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
761+dependencies = [
762+ "rand_core",
763+ "subtle",
764+]
765+
766+[[package]]
767+name = "fiat-crypto"
768+version = "0.2.9"
769+source = "registry+https://github.com/rust-lang/crates.io-index"
770+checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
771+
772+[[package]]
773+name = "find-msvc-tools"
774+version = "0.1.9"
775+source = "registry+https://github.com/rust-lang/crates.io-index"
776+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
777+
778+[[package]]
779+name = "flume"
780+version = "0.11.1"
781+source = "registry+https://github.com/rust-lang/crates.io-index"
782+checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
783+dependencies = [
784+ "futures-core",
785+ "futures-sink",
786+ "spin",
787+]
788+
789+[[package]]
790+name = "foldhash"
791+version = "0.1.5"
792+source = "registry+https://github.com/rust-lang/crates.io-index"
793+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
794+
795+[[package]]
796+name = "form_urlencoded"
797+version = "1.2.2"
798+source = "registry+https://github.com/rust-lang/crates.io-index"
799+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
800+dependencies = [
801+ "percent-encoding",
802+]
803+
804+[[package]]
805+name = "futures-channel"
806+version = "0.3.33"
807+source = "registry+https://github.com/rust-lang/crates.io-index"
808+checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae"
809+dependencies = [
810+ "futures-core",
811+ "futures-sink",
812+]
813+
814+[[package]]
815+name = "futures-core"
816+version = "0.3.33"
817+source = "registry+https://github.com/rust-lang/crates.io-index"
818+checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7"
819+
820+[[package]]
821+name = "futures-executor"
822+version = "0.3.33"
823+source = "registry+https://github.com/rust-lang/crates.io-index"
824+checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458"
825+dependencies = [
826+ "futures-core",
827+ "futures-task",
828+ "futures-util",
829+]
830+
831+[[package]]
832+name = "futures-intrusive"
833+version = "0.5.0"
834+source = "registry+https://github.com/rust-lang/crates.io-index"
835+checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
836+dependencies = [
837+ "futures-core",
838+ "lock_api",
839+ "parking_lot",
840+]
841+
842+[[package]]
843+name = "futures-io"
844+version = "0.3.33"
845+source = "registry+https://github.com/rust-lang/crates.io-index"
846+checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a"
847+
848+[[package]]
849+name = "futures-sink"
850+version = "0.3.33"
851+source = "registry+https://github.com/rust-lang/crates.io-index"
852+checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307"
853+
854+[[package]]
855+name = "futures-task"
856+version = "0.3.33"
857+source = "registry+https://github.com/rust-lang/crates.io-index"
858+checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109"
859+
860+[[package]]
861+name = "futures-util"
862+version = "0.3.33"
863+source = "registry+https://github.com/rust-lang/crates.io-index"
864+checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa"
865+dependencies = [
866+ "futures-core",
867+ "futures-io",
868+ "futures-sink",
869+ "futures-task",
870+ "memchr",
871+ "pin-project-lite",
872+ "slab",
873+]
874+
875+[[package]]
876+name = "generic-array"
877+version = "0.14.9"
878+source = "registry+https://github.com/rust-lang/crates.io-index"
879+checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2"
880+dependencies = [
881+ "typenum",
882+ "version_check",
883+ "zeroize",
884+]
885+
886+[[package]]
887+name = "getrandom"
888+version = "0.2.17"
889+source = "registry+https://github.com/rust-lang/crates.io-index"
890+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
891+dependencies = [
892+ "cfg-if",
893+ "libc",
894+ "wasi",
895+]
896+
897+[[package]]
898+name = "getrandom"
899+version = "0.4.3"
900+source = "registry+https://github.com/rust-lang/crates.io-index"
901+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
902+dependencies = [
903+ "cfg-if",
904+ "libc",
905+ "r-efi",
906+]
907+
908+[[package]]
909+name = "glob"
910+version = "0.3.4"
911+source = "registry+https://github.com/rust-lang/crates.io-index"
912+checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
913+
914+[[package]]
915+name = "group"
916+version = "0.13.0"
917+source = "registry+https://github.com/rust-lang/crates.io-index"
918+checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
919+dependencies = [
920+ "ff",
921+ "rand_core",
922+ "subtle",
923+]
924+
925+[[package]]
926+name = "hashbrown"
927+version = "0.15.5"
928+source = "registry+https://github.com/rust-lang/crates.io-index"
929+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
930+dependencies = [
931+ "allocator-api2",
932+ "equivalent",
933+ "foldhash",
934+]
935+
936+[[package]]
937+name = "hashbrown"
938+version = "0.17.1"
939+source = "registry+https://github.com/rust-lang/crates.io-index"
940+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
941+
942+[[package]]
943+name = "hashlink"
944+version = "0.10.0"
945+source = "registry+https://github.com/rust-lang/crates.io-index"
946+checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
947+dependencies = [
948+ "hashbrown 0.15.5",
949+]
950+
951+[[package]]
952+name = "heck"
953+version = "0.5.0"
954+source = "registry+https://github.com/rust-lang/crates.io-index"
955+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
956+
957+[[package]]
958+name = "hex"
959+version = "0.4.3"
960+source = "registry+https://github.com/rust-lang/crates.io-index"
961+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
962+
963+[[package]]
964+name = "hkdf"
965+version = "0.12.4"
966+source = "registry+https://github.com/rust-lang/crates.io-index"
967+checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
968+dependencies = [
969+ "hmac",
970+]
971+
972+[[package]]
973+name = "hmac"
974+version = "0.12.1"
975+source = "registry+https://github.com/rust-lang/crates.io-index"
976+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
977+dependencies = [
978+ "digest 0.10.7",
979+]
980+
981+[[package]]
982+name = "home"
983+version = "0.5.12"
984+source = "registry+https://github.com/rust-lang/crates.io-index"
985+checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
986+dependencies = [
987+ "windows-sys 0.61.2",
988+]
989+
990+[[package]]
991+name = "http"
992+version = "1.5.0"
993+source = "registry+https://github.com/rust-lang/crates.io-index"
994+checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
995+dependencies = [
996+ "bytes",
997+ "itoa",
998+]
999+
1000+[[package]]
1001+name = "http-body"
1002+version = "1.1.0"
1003+source = "registry+https://github.com/rust-lang/crates.io-index"
1004+checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
1005+dependencies = [
1006+ "bytes",
1007+ "http",
1008+]
1009+
1010+[[package]]
1011+name = "http-body-util"
1012+version = "0.1.4"
1013+source = "registry+https://github.com/rust-lang/crates.io-index"
1014+checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2"
1015+dependencies = [
1016+ "bytes",
1017+ "futures-core",
1018+ "http",
1019+ "http-body",
1020+ "pin-project-lite",
1021+]
1022+
1023+[[package]]
1024+name = "httparse"
1025+version = "1.10.1"
1026+source = "registry+https://github.com/rust-lang/crates.io-index"
1027+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
1028+
1029+[[package]]
1030+name = "httpdate"
1031+version = "1.0.3"
1032+source = "registry+https://github.com/rust-lang/crates.io-index"
1033+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
1034+
1035+[[package]]
1036+name = "hyper"
1037+version = "1.11.0"
1038+source = "registry+https://github.com/rust-lang/crates.io-index"
1039+checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72"
1040+dependencies = [
1041+ "atomic-waker",
1042+ "bytes",
1043+ "futures-channel",
1044+ "futures-core",
1045+ "http",
1046+ "http-body",
1047+ "httparse",
1048+ "httpdate",
1049+ "itoa",
1050+ "pin-project-lite",
1051+ "smallvec",
1052+ "tokio",
1053+]
1054+
1055+[[package]]
1056+name = "hyper-util"
1057+version = "0.1.20"
1058+source = "registry+https://github.com/rust-lang/crates.io-index"
1059+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
1060+dependencies = [
1061+ "bytes",
1062+ "http",
1063+ "http-body",
1064+ "hyper",
1065+ "pin-project-lite",
1066+ "tokio",
1067+ "tower-service",
1068+]
1069+
1070+[[package]]
1071+name = "iana-time-zone"
1072+version = "0.1.65"
1073+source = "registry+https://github.com/rust-lang/crates.io-index"
1074+checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
1075+dependencies = [
1076+ "android_system_properties",
1077+ "core-foundation-sys",
1078+ "iana-time-zone-haiku",
1079+ "js-sys",
1080+ "log",
1081+ "wasm-bindgen",
1082+ "windows-core",
1083+]
1084+
1085+[[package]]
1086+name = "iana-time-zone-haiku"
1087+version = "0.1.2"
1088+source = "registry+https://github.com/rust-lang/crates.io-index"
1089+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
1090+dependencies = [
1091+ "cc",
1092+]
1093+
1094+[[package]]
1095+name = "icu_collections"
1096+version = "2.2.0"
1097+source = "registry+https://github.com/rust-lang/crates.io-index"
1098+checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
1099+dependencies = [
1100+ "displaydoc",
1101+ "potential_utf",
1102+ "utf8_iter",
1103+ "yoke",
1104+ "zerofrom",
1105+ "zerovec",
1106+]
1107+
1108+[[package]]
1109+name = "icu_locale_core"
1110+version = "2.2.0"
1111+source = "registry+https://github.com/rust-lang/crates.io-index"
1112+checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
1113+dependencies = [
1114+ "displaydoc",
1115+ "litemap",
1116+ "tinystr",
1117+ "writeable",
1118+ "zerovec",
1119+]
1120+
1121+[[package]]
1122+name = "icu_normalizer"
1123+version = "2.2.0"
1124+source = "registry+https://github.com/rust-lang/crates.io-index"
1125+checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
1126+dependencies = [
1127+ "icu_collections",
1128+ "icu_normalizer_data",
1129+ "icu_properties",
1130+ "icu_provider",
1131+ "smallvec",
1132+ "zerovec",
1133+]
1134+
1135+[[package]]
1136+name = "icu_normalizer_data"
1137+version = "2.2.0"
1138+source = "registry+https://github.com/rust-lang/crates.io-index"
1139+checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
1140+
1141+[[package]]
1142+name = "icu_properties"
1143+version = "2.2.0"
1144+source = "registry+https://github.com/rust-lang/crates.io-index"
1145+checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
1146+dependencies = [
1147+ "icu_collections",
1148+ "icu_locale_core",
1149+ "icu_properties_data",
1150+ "icu_provider",
1151+ "zerotrie",
1152+ "zerovec",
1153+]
1154+
1155+[[package]]
1156+name = "icu_properties_data"
1157+version = "2.2.0"
1158+source = "registry+https://github.com/rust-lang/crates.io-index"
1159+checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
1160+
1161+[[package]]
1162+name = "icu_provider"
1163+version = "2.2.0"
1164+source = "registry+https://github.com/rust-lang/crates.io-index"
1165+checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
1166+dependencies = [
1167+ "displaydoc",
1168+ "icu_locale_core",
1169+ "writeable",
1170+ "yoke",
1171+ "zerofrom",
1172+ "zerotrie",
1173+ "zerovec",
1174+]
1175+
1176+[[package]]
1177+name = "idna"
1178+version = "1.1.0"
1179+source = "registry+https://github.com/rust-lang/crates.io-index"
1180+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
1181+dependencies = [
1182+ "idna_adapter",
1183+ "smallvec",
1184+ "utf8_iter",
1185+]
1186+
1187+[[package]]
1188+name = "idna_adapter"
1189+version = "1.2.2"
1190+source = "registry+https://github.com/rust-lang/crates.io-index"
1191+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
1192+dependencies = [
1193+ "icu_normalizer",
1194+ "icu_properties",
1195+]
1196+
1197+[[package]]
1198+name = "indexmap"
1199+version = "2.14.0"
1200+source = "registry+https://github.com/rust-lang/crates.io-index"
1201+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
1202+dependencies = [
1203+ "equivalent",
1204+ "hashbrown 0.17.1",
1205+]
1206+
1207+[[package]]
1208+name = "is_terminal_polyfill"
1209+version = "1.70.2"
1210+source = "registry+https://github.com/rust-lang/crates.io-index"
1211+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
1212+
1213+[[package]]
1214+name = "itertools"
1215+version = "0.10.5"
1216+source = "registry+https://github.com/rust-lang/crates.io-index"
1217+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
1218+dependencies = [
1219+ "either",
1220+]
1221+
1222+[[package]]
1223+name = "itoa"
1224+version = "1.0.18"
1225+source = "registry+https://github.com/rust-lang/crates.io-index"
1226+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
1227+
1228+[[package]]
1229+name = "js-sys"
1230+version = "0.3.103"
1231+source = "registry+https://github.com/rust-lang/crates.io-index"
1232+checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
1233+dependencies = [
1234+ "cfg-if",
1235+ "futures-util",
1236+ "wasm-bindgen",
1237+]
1238+
1239+[[package]]
1240+name = "lazy_static"
1241+version = "1.5.0"
1242+source = "registry+https://github.com/rust-lang/crates.io-index"
1243+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
1244+dependencies = [
1245+ "spin",
1246+]
1247+
1248+[[package]]
1249+name = "libc"
1250+version = "0.2.189"
1251+source = "registry+https://github.com/rust-lang/crates.io-index"
1252+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
1253+
1254+[[package]]
1255+name = "libm"
1256+version = "0.2.16"
1257+source = "registry+https://github.com/rust-lang/crates.io-index"
1258+checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
1259+
1260+[[package]]
1261+name = "libredox"
1262+version = "0.1.19"
1263+source = "registry+https://github.com/rust-lang/crates.io-index"
1264+checksum = "2026a5056764a10b2bf5d56488cba40da507f5493a6a429340e2004d9ed085fa"
1265+dependencies = [
1266+ "bitflags",
1267+ "libc",
1268+ "plain",
1269+ "redox_syscall 0.9.1",
1270+]
1271+
1272+[[package]]
1273+name = "libsqlite3-sys"
1274+version = "0.30.1"
1275+source = "registry+https://github.com/rust-lang/crates.io-index"
1276+checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
1277+dependencies = [
1278+ "pkg-config",
1279+ "vcpkg",
1280+]
1281+
1282+[[package]]
1283+name = "litemap"
1284+version = "0.8.2"
1285+source = "registry+https://github.com/rust-lang/crates.io-index"
1286+checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
1287+
1288+[[package]]
1289+name = "lock_api"
1290+version = "0.4.14"
1291+source = "registry+https://github.com/rust-lang/crates.io-index"
1292+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
1293+dependencies = [
1294+ "scopeguard",
1295+]
1296+
1297+[[package]]
1298+name = "log"
1299+version = "0.4.33"
1300+source = "registry+https://github.com/rust-lang/crates.io-index"
1301+checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
1302+
1303+[[package]]
1304+name = "matchers"
1305+version = "0.2.0"
1306+source = "registry+https://github.com/rust-lang/crates.io-index"
1307+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
1308+dependencies = [
1309+ "regex-automata",
1310+]
1311+
1312+[[package]]
1313+name = "matchit"
1314+version = "0.8.4"
1315+source = "registry+https://github.com/rust-lang/crates.io-index"
1316+checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
1317+
1318+[[package]]
1319+name = "md-5"
1320+version = "0.10.6"
1321+source = "registry+https://github.com/rust-lang/crates.io-index"
1322+checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
1323+dependencies = [
1324+ "cfg-if",
1325+ "digest 0.10.7",
1326+]
1327+
1328+[[package]]
1329+name = "memchr"
1330+version = "2.8.3"
1331+source = "registry+https://github.com/rust-lang/crates.io-index"
1332+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
1333+
1334+[[package]]
1335+name = "mime"
1336+version = "0.3.17"
1337+source = "registry+https://github.com/rust-lang/crates.io-index"
1338+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
1339+
1340+[[package]]
1341+name = "minimal-lexical"
1342+version = "0.2.1"
1343+source = "registry+https://github.com/rust-lang/crates.io-index"
1344+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
1345+
1346+[[package]]
1347+name = "mio"
1348+version = "1.2.2"
1349+source = "registry+https://github.com/rust-lang/crates.io-index"
1350+checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
1351+dependencies = [
1352+ "libc",
1353+ "wasi",
1354+ "windows-sys 0.61.2",
1355+]
1356+
1357+[[package]]
1358+name = "nom"
1359+version = "7.1.3"
1360+source = "registry+https://github.com/rust-lang/crates.io-index"
1361+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
1362+dependencies = [
1363+ "memchr",
1364+ "minimal-lexical",
1365+]
1366+
1367+[[package]]
1368+name = "nu-ansi-term"
1369+version = "0.50.3"
1370+source = "registry+https://github.com/rust-lang/crates.io-index"
1371+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
1372+dependencies = [
1373+ "windows-sys 0.61.2",
1374+]
1375+
1376+[[package]]
1377+name = "num-bigint-dig"
1378+version = "0.8.6"
1379+source = "registry+https://github.com/rust-lang/crates.io-index"
1380+checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
1381+dependencies = [
1382+ "lazy_static",
1383+ "libm",
1384+ "num-integer",
1385+ "num-iter",
1386+ "num-traits",
1387+ "rand",
1388+ "smallvec",
1389+ "zeroize",
1390+]
1391+
1392+[[package]]
1393+name = "num-conv"
1394+version = "0.2.2"
1395+source = "registry+https://github.com/rust-lang/crates.io-index"
1396+checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
1397+
1398+[[package]]
1399+name = "num-integer"
1400+version = "0.1.46"
1401+source = "registry+https://github.com/rust-lang/crates.io-index"
1402+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
1403+dependencies = [
1404+ "num-traits",
1405+]
1406+
1407+[[package]]
1408+name = "num-iter"
1409+version = "0.1.46"
1410+source = "registry+https://github.com/rust-lang/crates.io-index"
1411+checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b"
1412+dependencies = [
1413+ "num-integer",
1414+ "num-traits",
1415+]
1416+
1417+[[package]]
1418+name = "num-traits"
1419+version = "0.2.19"
1420+source = "registry+https://github.com/rust-lang/crates.io-index"
1421+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
1422+dependencies = [
1423+ "autocfg",
1424+ "libm",
1425+]
1426+
1427+[[package]]
1428+name = "once_cell"
1429+version = "1.21.4"
1430+source = "registry+https://github.com/rust-lang/crates.io-index"
1431+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
1432+
1433+[[package]]
1434+name = "once_cell_polyfill"
1435+version = "1.70.2"
1436+source = "registry+https://github.com/rust-lang/crates.io-index"
1437+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
1438+
1439+[[package]]
1440+name = "opaque-debug"
1441+version = "0.3.1"
1442+source = "registry+https://github.com/rust-lang/crates.io-index"
1443+checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
1444+
1445+[[package]]
1446+name = "p256"
1447+version = "0.13.2"
1448+source = "registry+https://github.com/rust-lang/crates.io-index"
1449+checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
1450+dependencies = [
1451+ "ecdsa",
1452+ "elliptic-curve",
1453+ "primeorder",
1454+ "sha2 0.10.9",
1455+]
1456+
1457+[[package]]
1458+name = "parking"
1459+version = "2.2.1"
1460+source = "registry+https://github.com/rust-lang/crates.io-index"
1461+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
1462+
1463+[[package]]
1464+name = "parking_lot"
1465+version = "0.12.5"
1466+source = "registry+https://github.com/rust-lang/crates.io-index"
1467+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
1468+dependencies = [
1469+ "lock_api",
1470+ "parking_lot_core",
1471+]
1472+
1473+[[package]]
1474+name = "parking_lot_core"
1475+version = "0.9.12"
1476+source = "registry+https://github.com/rust-lang/crates.io-index"
1477+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
1478+dependencies = [
1479+ "cfg-if",
1480+ "libc",
1481+ "redox_syscall 0.5.18",
1482+ "smallvec",
1483+ "windows-link",
1484+]
1485+
1486+[[package]]
1487+name = "password-hash"
1488+version = "0.5.0"
1489+source = "registry+https://github.com/rust-lang/crates.io-index"
1490+checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
1491+dependencies = [
1492+ "base64ct",
1493+ "rand_core",
1494+ "subtle",
1495+]
1496+
1497+[[package]]
1498+name = "pem-rfc7468"
1499+version = "0.7.0"
1500+source = "registry+https://github.com/rust-lang/crates.io-index"
1501+checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
1502+dependencies = [
1503+ "base64ct",
1504+]
1505+
1506+[[package]]
1507+name = "percent-encoding"
1508+version = "2.3.2"
1509+source = "registry+https://github.com/rust-lang/crates.io-index"
1510+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
1511+
1512+[[package]]
1513+name = "phf"
1514+version = "0.12.1"
1515+source = "registry+https://github.com/rust-lang/crates.io-index"
1516+checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7"
1517+dependencies = [
1518+ "phf_shared",
1519+]
1520+
1521+[[package]]
1522+name = "phf_shared"
1523+version = "0.12.1"
1524+source = "registry+https://github.com/rust-lang/crates.io-index"
1525+checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981"
1526+dependencies = [
1527+ "siphasher",
1528+]
1529+
1530+[[package]]
1531+name = "pin-project-lite"
1532+version = "0.2.17"
1533+source = "registry+https://github.com/rust-lang/crates.io-index"
1534+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
1535+
1536+[[package]]
1537+name = "pkcs1"
1538+version = "0.7.5"
1539+source = "registry+https://github.com/rust-lang/crates.io-index"
1540+checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
1541+dependencies = [
1542+ "der 0.7.10",
1543+ "pkcs8 0.10.2",
1544+ "spki 0.7.3",
1545+]
1546+
1547+[[package]]
1548+name = "pkcs8"
1549+version = "0.9.0"
1550+source = "registry+https://github.com/rust-lang/crates.io-index"
1551+checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba"
1552+dependencies = [
1553+ "der 0.6.1",
1554+ "spki 0.6.0",
1555+]
1556+
1557+[[package]]
1558+name = "pkcs8"
1559+version = "0.10.2"
1560+source = "registry+https://github.com/rust-lang/crates.io-index"
1561+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
1562+dependencies = [
1563+ "der 0.7.10",
1564+ "spki 0.7.3",
1565+]
1566+
1567+[[package]]
1568+name = "pkg-config"
1569+version = "0.3.33"
1570+source = "registry+https://github.com/rust-lang/crates.io-index"
1571+checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
1572+
1573+[[package]]
1574+name = "plain"
1575+version = "0.2.3"
1576+source = "registry+https://github.com/rust-lang/crates.io-index"
1577+checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
1578+
1579+[[package]]
1580+name = "potential_utf"
1581+version = "0.1.5"
1582+source = "registry+https://github.com/rust-lang/crates.io-index"
1583+checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
1584+dependencies = [
1585+ "zerovec",
1586+]
1587+
1588+[[package]]
1589+name = "powerfmt"
1590+version = "0.2.0"
1591+source = "registry+https://github.com/rust-lang/crates.io-index"
1592+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
1593+
1594+[[package]]
1595+name = "ppv-lite86"
1596+version = "0.2.21"
1597+source = "registry+https://github.com/rust-lang/crates.io-index"
1598+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
1599+dependencies = [
1600+ "zerocopy",
1601+]
1602+
1603+[[package]]
1604+name = "primeorder"
1605+version = "0.13.6"
1606+source = "registry+https://github.com/rust-lang/crates.io-index"
1607+checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
1608+dependencies = [
1609+ "elliptic-curve",
1610+]
1611+
1612+[[package]]
1613+name = "proc-macro-error-attr2"
1614+version = "2.0.0"
1615+source = "registry+https://github.com/rust-lang/crates.io-index"
1616+checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
1617+dependencies = [
1618+ "proc-macro2",
1619+ "quote",
1620+]
1621+
1622+[[package]]
1623+name = "proc-macro-error2"
1624+version = "2.0.1"
1625+source = "registry+https://github.com/rust-lang/crates.io-index"
1626+checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
1627+dependencies = [
1628+ "proc-macro-error-attr2",
1629+ "proc-macro2",
1630+ "quote",
1631+ "syn 2.0.119",
1632+]
1633+
1634+[[package]]
1635+name = "proc-macro2"
1636+version = "1.0.107"
1637+source = "registry+https://github.com/rust-lang/crates.io-index"
1638+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
1639+dependencies = [
1640+ "unicode-ident",
1641+]
1642+
1643+[[package]]
1644+name = "prost"
1645+version = "0.10.4"
1646+source = "registry+https://github.com/rust-lang/crates.io-index"
1647+checksum = "71adf41db68aa0daaefc69bb30bcd68ded9b9abaad5d1fbb6304c4fb390e083e"
1648+dependencies = [
1649+ "bytes",
1650+ "prost-derive",
1651+]
1652+
1653+[[package]]
1654+name = "prost-derive"
1655+version = "0.10.1"
1656+source = "registry+https://github.com/rust-lang/crates.io-index"
1657+checksum = "7b670f45da57fb8542ebdbb6105a925fe571b67f9e7ed9f47a06a84e72b4e7cc"
1658+dependencies = [
1659+ "anyhow",
1660+ "itertools",
1661+ "proc-macro2",
1662+ "quote",
1663+ "syn 1.0.109",
1664+]
1665+
1666+[[package]]
1667+name = "prost-types"
1668+version = "0.10.1"
1669+source = "registry+https://github.com/rust-lang/crates.io-index"
1670+checksum = "2d0a014229361011dc8e69c8a1ec6c2e8d0f2af7c91e3ea3f5b2170298461e68"
1671+dependencies = [
1672+ "bytes",
1673+ "prost",
1674+]
1675+
1676+[[package]]
1677+name = "quote"
1678+version = "1.0.47"
1679+source = "registry+https://github.com/rust-lang/crates.io-index"
1680+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
1681+dependencies = [
1682+ "proc-macro2",
1683+]
1684+
1685+[[package]]
1686+name = "r-efi"
1687+version = "6.0.0"
1688+source = "registry+https://github.com/rust-lang/crates.io-index"
1689+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
1690+
1691+[[package]]
1692+name = "rand"
1693+version = "0.8.7"
1694+source = "registry+https://github.com/rust-lang/crates.io-index"
1695+checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a"
1696+dependencies = [
1697+ "libc",
1698+ "rand_chacha",
1699+ "rand_core",
1700+]
1701+
1702+[[package]]
1703+name = "rand_chacha"
1704+version = "0.3.1"
1705+source = "registry+https://github.com/rust-lang/crates.io-index"
1706+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
1707+dependencies = [
1708+ "ppv-lite86",
1709+ "rand_core",
1710+]
1711+
1712+[[package]]
1713+name = "rand_core"
1714+version = "0.6.4"
1715+source = "registry+https://github.com/rust-lang/crates.io-index"
1716+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
1717+dependencies = [
1718+ "getrandom 0.2.17",
1719+]
1720+
1721+[[package]]
1722+name = "redox_syscall"
1723+version = "0.5.18"
1724+source = "registry+https://github.com/rust-lang/crates.io-index"
1725+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
1726+dependencies = [
1727+ "bitflags",
1728+]
1729+
1730+[[package]]
1731+name = "redox_syscall"
1732+version = "0.9.1"
1733+source = "registry+https://github.com/rust-lang/crates.io-index"
1734+checksum = "07507be7b4a5f9f26eeb41eeaebb1f5a7ff29dfb29739facc21d35bf8b11c21e"
1735+dependencies = [
1736+ "bitflags",
1737+]
1738+
1739+[[package]]
1740+name = "regex"
1741+version = "1.13.1"
1742+source = "registry+https://github.com/rust-lang/crates.io-index"
1743+checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
1744+dependencies = [
1745+ "aho-corasick",
1746+ "memchr",
1747+ "regex-automata",
1748+ "regex-syntax",
1749+]
1750+
1751+[[package]]
1752+name = "regex-automata"
1753+version = "0.4.18"
1754+source = "registry+https://github.com/rust-lang/crates.io-index"
1755+checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
1756+dependencies = [
1757+ "aho-corasick",
1758+ "memchr",
1759+ "regex-syntax",
1760+]
1761+
1762+[[package]]
1763+name = "regex-syntax"
1764+version = "0.8.11"
1765+source = "registry+https://github.com/rust-lang/crates.io-index"
1766+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
1767+
1768+[[package]]
1769+name = "rfc6979"
1770+version = "0.4.0"
1771+source = "registry+https://github.com/rust-lang/crates.io-index"
1772+checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
1773+dependencies = [
1774+ "hmac",
1775+ "subtle",
1776+]
1777+
1778+[[package]]
1779+name = "ring"
1780+version = "0.17.14"
1781+source = "registry+https://github.com/rust-lang/crates.io-index"
1782+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
1783+dependencies = [
1784+ "cc",
1785+ "cfg-if",
1786+ "getrandom 0.2.17",
1787+ "libc",
1788+ "untrusted",
1789+ "windows-sys 0.52.0",
1790+]
1791+
1792+[[package]]
1793+name = "rpassword"
1794+version = "7.5.4"
1795+source = "registry+https://github.com/rust-lang/crates.io-index"
1796+checksum = "2da316a15f47e3d053de9cb2c439650bd8fa4aaeb9365f2e5f27f492ff73c196"
1797+dependencies = [
1798+ "libc",
1799+ "rtoolbox",
1800+ "windows-sys 0.61.2",
1801+]
1802+
1803+[[package]]
1804+name = "rsa"
1805+version = "0.9.10"
1806+source = "registry+https://github.com/rust-lang/crates.io-index"
1807+checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
1808+dependencies = [
1809+ "const-oid",
1810+ "digest 0.10.7",
1811+ "num-bigint-dig",
1812+ "num-integer",
1813+ "num-traits",
1814+ "pkcs1",
1815+ "pkcs8 0.10.2",
1816+ "rand_core",
1817+ "signature",
1818+ "spki 0.7.3",
1819+ "subtle",
1820+ "zeroize",
1821+]
1822+
1823+[[package]]
1824+name = "rtoolbox"
1825+version = "0.0.5"
1826+source = "registry+https://github.com/rust-lang/crates.io-index"
1827+checksum = "50a0e551c1e27e1731aba276dbeaeac73f53c7cd34d1bda485d02bd1e0f36844"
1828+dependencies = [
1829+ "libc",
1830+ "windows-sys 0.59.0",
1831+]
1832+
1833+[[package]]
1834+name = "rustc-hash"
1835+version = "2.1.3"
1836+source = "registry+https://github.com/rust-lang/crates.io-index"
1837+checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
1838+
1839+[[package]]
1840+name = "rustc_version"
1841+version = "0.4.1"
1842+source = "registry+https://github.com/rust-lang/crates.io-index"
1843+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
1844+dependencies = [
1845+ "semver",
1846+]
1847+
1848+[[package]]
1849+name = "rustls"
1850+version = "0.23.43"
1851+source = "registry+https://github.com/rust-lang/crates.io-index"
1852+checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
1853+dependencies = [
1854+ "once_cell",
1855+ "ring",
1856+ "rustls-pki-types",
1857+ "rustls-webpki",
1858+ "subtle",
1859+ "zeroize",
1860+]
1861+
1862+[[package]]
1863+name = "rustls-pki-types"
1864+version = "1.15.1"
1865+source = "registry+https://github.com/rust-lang/crates.io-index"
1866+checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
1867+dependencies = [
1868+ "zeroize",
1869+]
1870+
1871+[[package]]
1872+name = "rustls-webpki"
1873+version = "0.103.13"
1874+source = "registry+https://github.com/rust-lang/crates.io-index"
1875+checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
1876+dependencies = [
1877+ "ring",
1878+ "rustls-pki-types",
1879+ "untrusted",
1880+]
1881+
1882+[[package]]
1883+name = "rustversion"
1884+version = "1.0.23"
1885+source = "registry+https://github.com/rust-lang/crates.io-index"
1886+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
1887+
1888+[[package]]
1889+name = "ryu"
1890+version = "1.0.23"
1891+source = "registry+https://github.com/rust-lang/crates.io-index"
1892+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
1893+
1894+[[package]]
1895+name = "scopeguard"
1896+version = "1.2.0"
1897+source = "registry+https://github.com/rust-lang/crates.io-index"
1898+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
1899+
1900+[[package]]
1901+name = "sec1"
1902+version = "0.7.3"
1903+source = "registry+https://github.com/rust-lang/crates.io-index"
1904+checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
1905+dependencies = [
1906+ "base16ct",
1907+ "der 0.7.10",
1908+ "generic-array",
1909+ "pkcs8 0.10.2",
1910+ "serdect",
1911+ "subtle",
1912+ "zeroize",
1913+]
1914+
1915+[[package]]
1916+name = "semver"
1917+version = "1.0.28"
1918+source = "registry+https://github.com/rust-lang/crates.io-index"
1919+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
1920+
1921+[[package]]
1922+name = "serde"
1923+version = "1.0.229"
1924+source = "registry+https://github.com/rust-lang/crates.io-index"
1925+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
1926+dependencies = [
1927+ "serde_core",
1928+ "serde_derive",
1929+]
1930+
1931+[[package]]
1932+name = "serde_core"
1933+version = "1.0.229"
1934+source = "registry+https://github.com/rust-lang/crates.io-index"
1935+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
1936+dependencies = [
1937+ "serde_derive",
1938+]
1939+
1940+[[package]]
1941+name = "serde_derive"
1942+version = "1.0.229"
1943+source = "registry+https://github.com/rust-lang/crates.io-index"
1944+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
1945+dependencies = [
1946+ "proc-macro2",
1947+ "quote",
1948+ "syn 3.0.3",
1949+]
1950+
1951+[[package]]
1952+name = "serde_json"
1953+version = "1.0.151"
1954+source = "registry+https://github.com/rust-lang/crates.io-index"
1955+checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
1956+dependencies = [
1957+ "itoa",
1958+ "memchr",
1959+ "serde",
1960+ "serde_core",
1961+ "zmij",
1962+]
1963+
1964+[[package]]
1965+name = "serde_path_to_error"
1966+version = "0.1.20"
1967+source = "registry+https://github.com/rust-lang/crates.io-index"
1968+checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
1969+dependencies = [
1970+ "itoa",
1971+ "serde",
1972+ "serde_core",
1973+]
1974+
1975+[[package]]
1976+name = "serde_urlencoded"
1977+version = "0.7.1"
1978+source = "registry+https://github.com/rust-lang/crates.io-index"
1979+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
1980+dependencies = [
1981+ "form_urlencoded",
1982+ "itoa",
1983+ "ryu",
1984+ "serde",
1985+]
1986+
1987+[[package]]
1988+name = "serdect"
1989+version = "0.2.0"
1990+source = "registry+https://github.com/rust-lang/crates.io-index"
1991+checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177"
1992+dependencies = [
1993+ "base16ct",
1994+ "serde",
1995+]
1996+
1997+[[package]]
1998+name = "sha1"
1999+version = "0.10.7"
2000+source = "registry+https://github.com/rust-lang/crates.io-index"
2001+checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8"
2002+dependencies = [
2003+ "cfg-if",
2004+ "cpufeatures",
2005+ "digest 0.10.7",
2006+]
2007+
2008+[[package]]
2009+name = "sha2"
2010+version = "0.9.9"
2011+source = "registry+https://github.com/rust-lang/crates.io-index"
2012+checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
2013+dependencies = [
2014+ "block-buffer 0.9.0",
2015+ "cfg-if",
2016+ "cpufeatures",
2017+ "digest 0.9.0",
2018+ "opaque-debug",
2019+]
2020+
2021+[[package]]
2022+name = "sha2"
2023+version = "0.10.9"
2024+source = "registry+https://github.com/rust-lang/crates.io-index"
2025+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
2026+dependencies = [
2027+ "cfg-if",
2028+ "cpufeatures",
2029+ "digest 0.10.7",
2030+]
2031+
2032+[[package]]
2033+name = "sharded-slab"
2034+version = "0.1.7"
2035+source = "registry+https://github.com/rust-lang/crates.io-index"
2036+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
2037+dependencies = [
2038+ "lazy_static",
2039+]
2040+
2041+[[package]]
2042+name = "shlex"
2043+version = "2.0.1"
2044+source = "registry+https://github.com/rust-lang/crates.io-index"
2045+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
2046+
2047+[[package]]
2048+name = "signal-hook-registry"
2049+version = "1.4.8"
2050+source = "registry+https://github.com/rust-lang/crates.io-index"
2051+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
2052+dependencies = [
2053+ "errno",
2054+ "libc",
2055+]
2056+
2057+[[package]]
2058+name = "signature"
2059+version = "2.2.0"
2060+source = "registry+https://github.com/rust-lang/crates.io-index"
2061+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
2062+dependencies = [
2063+ "digest 0.10.7",
2064+ "rand_core",
2065+]
2066+
2067+[[package]]
2068+name = "siphasher"
2069+version = "1.0.3"
2070+source = "registry+https://github.com/rust-lang/crates.io-index"
2071+checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
2072+
2073+[[package]]
2074+name = "slab"
2075+version = "0.4.12"
2076+source = "registry+https://github.com/rust-lang/crates.io-index"
2077+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
2078+
2079+[[package]]
2080+name = "smallvec"
2081+version = "1.15.2"
2082+source = "registry+https://github.com/rust-lang/crates.io-index"
2083+checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
2084+dependencies = [
2085+ "serde",
2086+]
2087+
2088+[[package]]
2089+name = "socket2"
2090+version = "0.6.5"
2091+source = "registry+https://github.com/rust-lang/crates.io-index"
2092+checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
2093+dependencies = [
2094+ "libc",
2095+ "windows-sys 0.61.2",
2096+]
2097+
2098+[[package]]
2099+name = "spin"
2100+version = "0.9.9"
2101+source = "registry+https://github.com/rust-lang/crates.io-index"
2102+checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e"
2103+dependencies = [
2104+ "lock_api",
2105+]
2106+
2107+[[package]]
2108+name = "spki"
2109+version = "0.6.0"
2110+source = "registry+https://github.com/rust-lang/crates.io-index"
2111+checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b"
2112+dependencies = [
2113+ "der 0.6.1",
2114+]
2115+
2116+[[package]]
2117+name = "spki"
2118+version = "0.7.3"
2119+source = "registry+https://github.com/rust-lang/crates.io-index"
2120+checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
2121+dependencies = [
2122+ "base64ct",
2123+ "der 0.7.10",
2124+]
2125+
2126+[[package]]
2127+name = "sqlx"
2128+version = "0.8.6"
2129+source = "registry+https://github.com/rust-lang/crates.io-index"
2130+checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc"
2131+dependencies = [
2132+ "sqlx-core",
2133+ "sqlx-macros",
2134+ "sqlx-mysql",
2135+ "sqlx-postgres",
2136+ "sqlx-sqlite",
2137+]
2138+
2139+[[package]]
2140+name = "sqlx-core"
2141+version = "0.8.6"
2142+source = "registry+https://github.com/rust-lang/crates.io-index"
2143+checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
2144+dependencies = [
2145+ "base64 0.22.1",
2146+ "bytes",
2147+ "chrono",
2148+ "crc",
2149+ "crossbeam-queue",
2150+ "either",
2151+ "event-listener",
2152+ "futures-core",
2153+ "futures-intrusive",
2154+ "futures-io",
2155+ "futures-util",
2156+ "hashbrown 0.15.5",
2157+ "hashlink",
2158+ "indexmap",
2159+ "log",
2160+ "memchr",
2161+ "once_cell",
2162+ "percent-encoding",
2163+ "rustls",
2164+ "serde",
2165+ "serde_json",
2166+ "sha2 0.10.9",
2167+ "smallvec",
2168+ "thiserror 2.0.19",
2169+ "tokio",
2170+ "tokio-stream",
2171+ "tracing",
2172+ "url",
2173+ "uuid",
2174+ "webpki-roots 0.26.11",
2175+]
2176+
2177+[[package]]
2178+name = "sqlx-macros"
2179+version = "0.8.6"
2180+source = "registry+https://github.com/rust-lang/crates.io-index"
2181+checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d"
2182+dependencies = [
2183+ "proc-macro2",
2184+ "quote",
2185+ "sqlx-core",
2186+ "sqlx-macros-core",
2187+ "syn 2.0.119",
2188+]
2189+
2190+[[package]]
2191+name = "sqlx-macros-core"
2192+version = "0.8.6"
2193+source = "registry+https://github.com/rust-lang/crates.io-index"
2194+checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b"
2195+dependencies = [
2196+ "dotenvy",
2197+ "either",
2198+ "heck",
2199+ "hex",
2200+ "once_cell",
2201+ "proc-macro2",
2202+ "quote",
2203+ "serde",
2204+ "serde_json",
2205+ "sha2 0.10.9",
2206+ "sqlx-core",
2207+ "sqlx-mysql",
2208+ "sqlx-postgres",
2209+ "sqlx-sqlite",
2210+ "syn 2.0.119",
2211+ "tokio",
2212+ "url",
2213+]
2214+
2215+[[package]]
2216+name = "sqlx-mysql"
2217+version = "0.8.6"
2218+source = "registry+https://github.com/rust-lang/crates.io-index"
2219+checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
2220+dependencies = [
2221+ "atoi",
2222+ "base64 0.22.1",
2223+ "bitflags",
2224+ "byteorder",
2225+ "bytes",
2226+ "chrono",
2227+ "crc",
2228+ "digest 0.10.7",
2229+ "dotenvy",
2230+ "either",
2231+ "futures-channel",
2232+ "futures-core",
2233+ "futures-io",
2234+ "futures-util",
2235+ "generic-array",
2236+ "hex",
2237+ "hkdf",
2238+ "hmac",
2239+ "itoa",
2240+ "log",
2241+ "md-5",
2242+ "memchr",
2243+ "once_cell",
2244+ "percent-encoding",
2245+ "rand",
2246+ "rsa",
2247+ "serde",
2248+ "sha1",
2249+ "sha2 0.10.9",
2250+ "smallvec",
2251+ "sqlx-core",
2252+ "stringprep",
2253+ "thiserror 2.0.19",
2254+ "tracing",
2255+ "uuid",
2256+ "whoami",
2257+]
2258+
2259+[[package]]
2260+name = "sqlx-postgres"
2261+version = "0.8.6"
2262+source = "registry+https://github.com/rust-lang/crates.io-index"
2263+checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
2264+dependencies = [
2265+ "atoi",
2266+ "base64 0.22.1",
2267+ "bitflags",
2268+ "byteorder",
2269+ "chrono",
2270+ "crc",
2271+ "dotenvy",
2272+ "etcetera",
2273+ "futures-channel",
2274+ "futures-core",
2275+ "futures-util",
2276+ "hex",
2277+ "hkdf",
2278+ "hmac",
2279+ "home",
2280+ "itoa",
2281+ "log",
2282+ "md-5",
2283+ "memchr",
2284+ "once_cell",
2285+ "rand",
2286+ "serde",
2287+ "serde_json",
2288+ "sha2 0.10.9",
2289+ "smallvec",
2290+ "sqlx-core",
2291+ "stringprep",
2292+ "thiserror 2.0.19",
2293+ "tracing",
2294+ "uuid",
2295+ "whoami",
2296+]
2297+
2298+[[package]]
2299+name = "sqlx-sqlite"
2300+version = "0.8.6"
2301+source = "registry+https://github.com/rust-lang/crates.io-index"
2302+checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
2303+dependencies = [
2304+ "atoi",
2305+ "chrono",
2306+ "flume",
2307+ "futures-channel",
2308+ "futures-core",
2309+ "futures-executor",
2310+ "futures-intrusive",
2311+ "futures-util",
2312+ "libsqlite3-sys",
2313+ "log",
2314+ "percent-encoding",
2315+ "serde",
2316+ "serde_urlencoded",
2317+ "sqlx-core",
2318+ "thiserror 2.0.19",
2319+ "tracing",
2320+ "url",
2321+ "uuid",
2322+]
2323+
2324+[[package]]
2325+name = "stable_deref_trait"
2326+version = "1.2.1"
2327+source = "registry+https://github.com/rust-lang/crates.io-index"
2328+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
2329+
2330+[[package]]
2331+name = "stringprep"
2332+version = "0.1.5"
2333+source = "registry+https://github.com/rust-lang/crates.io-index"
2334+checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
2335+dependencies = [
2336+ "unicode-bidi",
2337+ "unicode-normalization",
2338+ "unicode-properties",
2339+]
2340+
2341+[[package]]
2342+name = "strsim"
2343+version = "0.11.1"
2344+source = "registry+https://github.com/rust-lang/crates.io-index"
2345+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
2346+
2347+[[package]]
2348+name = "subtle"
2349+version = "2.6.1"
2350+source = "registry+https://github.com/rust-lang/crates.io-index"
2351+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
2352+
2353+[[package]]
2354+name = "syn"
2355+version = "1.0.109"
2356+source = "registry+https://github.com/rust-lang/crates.io-index"
2357+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
2358+dependencies = [
2359+ "proc-macro2",
2360+ "quote",
2361+ "unicode-ident",
2362+]
2363+
2364+[[package]]
2365+name = "syn"
2366+version = "2.0.119"
2367+source = "registry+https://github.com/rust-lang/crates.io-index"
2368+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
2369+dependencies = [
2370+ "proc-macro2",
2371+ "quote",
2372+ "unicode-ident",
2373+]
2374+
2375+[[package]]
2376+name = "syn"
2377+version = "3.0.3"
2378+source = "registry+https://github.com/rust-lang/crates.io-index"
2379+checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
2380+dependencies = [
2381+ "proc-macro2",
2382+ "quote",
2383+ "unicode-ident",
2384+]
2385+
2386+[[package]]
2387+name = "sync_wrapper"
2388+version = "1.0.2"
2389+source = "registry+https://github.com/rust-lang/crates.io-index"
2390+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
2391+
2392+[[package]]
2393+name = "synstructure"
2394+version = "0.13.2"
2395+source = "registry+https://github.com/rust-lang/crates.io-index"
2396+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
2397+dependencies = [
2398+ "proc-macro2",
2399+ "quote",
2400+ "syn 2.0.119",
2401+]
2402+
2403+[[package]]
2404+name = "thiserror"
2405+version = "1.0.69"
2406+source = "registry+https://github.com/rust-lang/crates.io-index"
2407+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
2408+dependencies = [
2409+ "thiserror-impl 1.0.69",
2410+]
2411+
2412+[[package]]
2413+name = "thiserror"
2414+version = "2.0.19"
2415+source = "registry+https://github.com/rust-lang/crates.io-index"
2416+checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9"
2417+dependencies = [
2418+ "thiserror-impl 2.0.19",
2419+]
2420+
2421+[[package]]
2422+name = "thiserror-impl"
2423+version = "1.0.69"
2424+source = "registry+https://github.com/rust-lang/crates.io-index"
2425+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
2426+dependencies = [
2427+ "proc-macro2",
2428+ "quote",
2429+ "syn 2.0.119",
2430+]
2431+
2432+[[package]]
2433+name = "thiserror-impl"
2434+version = "2.0.19"
2435+source = "registry+https://github.com/rust-lang/crates.io-index"
2436+checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd"
2437+dependencies = [
2438+ "proc-macro2",
2439+ "quote",
2440+ "syn 3.0.3",
2441+]
2442+
2443+[[package]]
2444+name = "thread_local"
2445+version = "1.1.10"
2446+source = "registry+https://github.com/rust-lang/crates.io-index"
2447+checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070"
2448+dependencies = [
2449+ "cfg-if",
2450+]
2451+
2452+[[package]]
2453+name = "time"
2454+version = "0.3.55"
2455+source = "registry+https://github.com/rust-lang/crates.io-index"
2456+checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
2457+dependencies = [
2458+ "deranged",
2459+ "num-conv",
2460+ "powerfmt",
2461+ "serde_core",
2462+ "time-core",
2463+ "time-macros",
2464+]
2465+
2466+[[package]]
2467+name = "time-core"
2468+version = "0.1.9"
2469+source = "registry+https://github.com/rust-lang/crates.io-index"
2470+checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
2471+
2472+[[package]]
2473+name = "time-macros"
2474+version = "0.2.32"
2475+source = "registry+https://github.com/rust-lang/crates.io-index"
2476+checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
2477+dependencies = [
2478+ "num-conv",
2479+ "time-core",
2480+]
2481+
2482+[[package]]
2483+name = "time-time"
2484+version = "0.1.0"
2485+dependencies = [
2486+ "anyhow",
2487+ "argon2",
2488+ "askama",
2489+ "axum",
2490+ "axum-extra",
2491+ "biscuit-auth",
2492+ "chrono",
2493+ "chrono-tz",
2494+ "clap",
2495+ "hex",
2496+ "hmac",
2497+ "rpassword",
2498+ "serde",
2499+ "sha2 0.10.9",
2500+ "sqlx",
2501+ "subtle",
2502+ "thiserror 2.0.19",
2503+ "time",
2504+ "tokio",
2505+ "tracing",
2506+ "tracing-subscriber",
2507+ "uuid",
2508+]
2509+
2510+[[package]]
2511+name = "tinystr"
2512+version = "0.8.3"
2513+source = "registry+https://github.com/rust-lang/crates.io-index"
2514+checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
2515+dependencies = [
2516+ "displaydoc",
2517+ "zerovec",
2518+]
2519+
2520+[[package]]
2521+name = "tinyvec"
2522+version = "1.12.0"
2523+source = "registry+https://github.com/rust-lang/crates.io-index"
2524+checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f"
2525+dependencies = [
2526+ "tinyvec_macros",
2527+]
2528+
2529+[[package]]
2530+name = "tinyvec_macros"
2531+version = "0.1.1"
2532+source = "registry+https://github.com/rust-lang/crates.io-index"
2533+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
2534+
2535+[[package]]
2536+name = "tokio"
2537+version = "1.53.1"
2538+source = "registry+https://github.com/rust-lang/crates.io-index"
2539+checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
2540+dependencies = [
2541+ "bytes",
2542+ "libc",
2543+ "mio",
2544+ "pin-project-lite",
2545+ "signal-hook-registry",
2546+ "socket2",
2547+ "tokio-macros",
2548+ "windows-sys 0.61.2",
2549+]
2550+
2551+[[package]]
2552+name = "tokio-macros"
2553+version = "2.7.2"
2554+source = "registry+https://github.com/rust-lang/crates.io-index"
2555+checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
2556+dependencies = [
2557+ "proc-macro2",
2558+ "quote",
2559+ "syn 3.0.3",
2560+]
2561+
2562+[[package]]
2563+name = "tokio-stream"
2564+version = "0.1.19"
2565+source = "registry+https://github.com/rust-lang/crates.io-index"
2566+checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b"
2567+dependencies = [
2568+ "futures-core",
2569+ "pin-project-lite",
2570+ "tokio",
2571+]
2572+
2573+[[package]]
2574+name = "tower"
2575+version = "0.5.3"
2576+source = "registry+https://github.com/rust-lang/crates.io-index"
2577+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
2578+dependencies = [
2579+ "futures-core",
2580+ "futures-util",
2581+ "pin-project-lite",
2582+ "sync_wrapper",
2583+ "tokio",
2584+ "tower-layer",
2585+ "tower-service",
2586+ "tracing",
2587+]
2588+
2589+[[package]]
2590+name = "tower-layer"
2591+version = "0.3.3"
2592+source = "registry+https://github.com/rust-lang/crates.io-index"
2593+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
2594+
2595+[[package]]
2596+name = "tower-service"
2597+version = "0.3.3"
2598+source = "registry+https://github.com/rust-lang/crates.io-index"
2599+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
2600+
2601+[[package]]
2602+name = "tracing"
2603+version = "0.1.44"
2604+source = "registry+https://github.com/rust-lang/crates.io-index"
2605+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
2606+dependencies = [
2607+ "log",
2608+ "pin-project-lite",
2609+ "tracing-attributes",
2610+ "tracing-core",
2611+]
2612+
2613+[[package]]
2614+name = "tracing-attributes"
2615+version = "0.1.31"
2616+source = "registry+https://github.com/rust-lang/crates.io-index"
2617+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
2618+dependencies = [
2619+ "proc-macro2",
2620+ "quote",
2621+ "syn 2.0.119",
2622+]
2623+
2624+[[package]]
2625+name = "tracing-core"
2626+version = "0.1.36"
2627+source = "registry+https://github.com/rust-lang/crates.io-index"
2628+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
2629+dependencies = [
2630+ "once_cell",
2631+ "valuable",
2632+]
2633+
2634+[[package]]
2635+name = "tracing-log"
2636+version = "0.2.0"
2637+source = "registry+https://github.com/rust-lang/crates.io-index"
2638+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
2639+dependencies = [
2640+ "log",
2641+ "once_cell",
2642+ "tracing-core",
2643+]
2644+
2645+[[package]]
2646+name = "tracing-subscriber"
2647+version = "0.3.23"
2648+source = "registry+https://github.com/rust-lang/crates.io-index"
2649+checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
2650+dependencies = [
2651+ "matchers",
2652+ "nu-ansi-term",
2653+ "once_cell",
2654+ "regex-automata",
2655+ "sharded-slab",
2656+ "smallvec",
2657+ "thread_local",
2658+ "tracing",
2659+ "tracing-core",
2660+ "tracing-log",
2661+]
2662+
2663+[[package]]
2664+name = "typenum"
2665+version = "1.20.1"
2666+source = "registry+https://github.com/rust-lang/crates.io-index"
2667+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
2668+
2669+[[package]]
2670+name = "unicode-bidi"
2671+version = "0.3.18"
2672+source = "registry+https://github.com/rust-lang/crates.io-index"
2673+checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
2674+
2675+[[package]]
2676+name = "unicode-ident"
2677+version = "1.0.24"
2678+source = "registry+https://github.com/rust-lang/crates.io-index"
2679+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
2680+
2681+[[package]]
2682+name = "unicode-normalization"
2683+version = "0.1.25"
2684+source = "registry+https://github.com/rust-lang/crates.io-index"
2685+checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
2686+dependencies = [
2687+ "tinyvec",
2688+]
2689+
2690+[[package]]
2691+name = "unicode-properties"
2692+version = "0.1.4"
2693+source = "registry+https://github.com/rust-lang/crates.io-index"
2694+checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
2695+
2696+[[package]]
2697+name = "untrusted"
2698+version = "0.9.0"
2699+source = "registry+https://github.com/rust-lang/crates.io-index"
2700+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
2701+
2702+[[package]]
2703+name = "url"
2704+version = "2.5.8"
2705+source = "registry+https://github.com/rust-lang/crates.io-index"
2706+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
2707+dependencies = [
2708+ "form_urlencoded",
2709+ "idna",
2710+ "percent-encoding",
2711+ "serde",
2712+]
2713+
2714+[[package]]
2715+name = "utf8_iter"
2716+version = "1.0.4"
2717+source = "registry+https://github.com/rust-lang/crates.io-index"
2718+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
2719+
2720+[[package]]
2721+name = "utf8parse"
2722+version = "0.2.2"
2723+source = "registry+https://github.com/rust-lang/crates.io-index"
2724+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
2725+
2726+[[package]]
2727+name = "uuid"
2728+version = "1.24.0"
2729+source = "registry+https://github.com/rust-lang/crates.io-index"
2730+checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239"
2731+dependencies = [
2732+ "getrandom 0.4.3",
2733+ "js-sys",
2734+ "serde_core",
2735+ "wasm-bindgen",
2736+]
2737+
2738+[[package]]
2739+name = "valuable"
2740+version = "0.1.1"
2741+source = "registry+https://github.com/rust-lang/crates.io-index"
2742+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
2743+
2744+[[package]]
2745+name = "vcpkg"
2746+version = "0.2.15"
2747+source = "registry+https://github.com/rust-lang/crates.io-index"
2748+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
2749+
2750+[[package]]
2751+name = "version_check"
2752+version = "0.9.5"
2753+source = "registry+https://github.com/rust-lang/crates.io-index"
2754+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
2755+
2756+[[package]]
2757+name = "wasi"
2758+version = "0.11.1+wasi-snapshot-preview1"
2759+source = "registry+https://github.com/rust-lang/crates.io-index"
2760+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
2761+
2762+[[package]]
2763+name = "wasite"
2764+version = "0.1.0"
2765+source = "registry+https://github.com/rust-lang/crates.io-index"
2766+checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
2767+
2768+[[package]]
2769+name = "wasm-bindgen"
2770+version = "0.2.126"
2771+source = "registry+https://github.com/rust-lang/crates.io-index"
2772+checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
2773+dependencies = [
2774+ "cfg-if",
2775+ "once_cell",
2776+ "rustversion",
2777+ "wasm-bindgen-macro",
2778+ "wasm-bindgen-shared",
2779+]
2780+
2781+[[package]]
2782+name = "wasm-bindgen-macro"
2783+version = "0.2.126"
2784+source = "registry+https://github.com/rust-lang/crates.io-index"
2785+checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
2786+dependencies = [
2787+ "quote",
2788+ "wasm-bindgen-macro-support",
2789+]
2790+
2791+[[package]]
2792+name = "wasm-bindgen-macro-support"
2793+version = "0.2.126"
2794+source = "registry+https://github.com/rust-lang/crates.io-index"
2795+checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
2796+dependencies = [
2797+ "bumpalo",
2798+ "proc-macro2",
2799+ "quote",
2800+ "syn 2.0.119",
2801+ "wasm-bindgen-shared",
2802+]
2803+
2804+[[package]]
2805+name = "wasm-bindgen-shared"
2806+version = "0.2.126"
2807+source = "registry+https://github.com/rust-lang/crates.io-index"
2808+checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
2809+dependencies = [
2810+ "unicode-ident",
2811+]
2812+
2813+[[package]]
2814+name = "webpki-roots"
2815+version = "0.26.11"
2816+source = "registry+https://github.com/rust-lang/crates.io-index"
2817+checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
2818+dependencies = [
2819+ "webpki-roots 1.0.9",
2820+]
2821+
2822+[[package]]
2823+name = "webpki-roots"
2824+version = "1.0.9"
2825+source = "registry+https://github.com/rust-lang/crates.io-index"
2826+checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
2827+dependencies = [
2828+ "rustls-pki-types",
2829+]
2830+
2831+[[package]]
2832+name = "whoami"
2833+version = "1.6.1"
2834+source = "registry+https://github.com/rust-lang/crates.io-index"
2835+checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d"
2836+dependencies = [
2837+ "libredox",
2838+ "wasite",
2839+]
2840+
2841+[[package]]
2842+name = "windows-core"
2843+version = "0.62.2"
2844+source = "registry+https://github.com/rust-lang/crates.io-index"
2845+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
2846+dependencies = [
2847+ "windows-implement",
2848+ "windows-interface",
2849+ "windows-link",
2850+ "windows-result",
2851+ "windows-strings",
2852+]
2853+
2854+[[package]]
2855+name = "windows-implement"
2856+version = "0.60.2"
2857+source = "registry+https://github.com/rust-lang/crates.io-index"
2858+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
2859+dependencies = [
2860+ "proc-macro2",
2861+ "quote",
2862+ "syn 2.0.119",
2863+]
2864+
2865+[[package]]
2866+name = "windows-interface"
2867+version = "0.59.3"
2868+source = "registry+https://github.com/rust-lang/crates.io-index"
2869+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
2870+dependencies = [
2871+ "proc-macro2",
2872+ "quote",
2873+ "syn 2.0.119",
2874+]
2875+
2876+[[package]]
2877+name = "windows-link"
2878+version = "0.2.1"
2879+source = "registry+https://github.com/rust-lang/crates.io-index"
2880+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
2881+
2882+[[package]]
2883+name = "windows-result"
2884+version = "0.4.1"
2885+source = "registry+https://github.com/rust-lang/crates.io-index"
2886+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
2887+dependencies = [
2888+ "windows-link",
2889+]
2890+
2891+[[package]]
2892+name = "windows-strings"
2893+version = "0.5.1"
2894+source = "registry+https://github.com/rust-lang/crates.io-index"
2895+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
2896+dependencies = [
2897+ "windows-link",
2898+]
2899+
2900+[[package]]
2901+name = "windows-sys"
2902+version = "0.48.0"
2903+source = "registry+https://github.com/rust-lang/crates.io-index"
2904+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
2905+dependencies = [
2906+ "windows-targets 0.48.5",
2907+]
2908+
2909+[[package]]
2910+name = "windows-sys"
2911+version = "0.52.0"
2912+source = "registry+https://github.com/rust-lang/crates.io-index"
2913+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
2914+dependencies = [
2915+ "windows-targets 0.52.6",
2916+]
2917+
2918+[[package]]
2919+name = "windows-sys"
2920+version = "0.59.0"
2921+source = "registry+https://github.com/rust-lang/crates.io-index"
2922+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
2923+dependencies = [
2924+ "windows-targets 0.52.6",
2925+]
2926+
2927+[[package]]
2928+name = "windows-sys"
2929+version = "0.61.2"
2930+source = "registry+https://github.com/rust-lang/crates.io-index"
2931+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
2932+dependencies = [
2933+ "windows-link",
2934+]
2935+
2936+[[package]]
2937+name = "windows-targets"
2938+version = "0.48.5"
2939+source = "registry+https://github.com/rust-lang/crates.io-index"
2940+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
2941+dependencies = [
2942+ "windows_aarch64_gnullvm 0.48.5",
2943+ "windows_aarch64_msvc 0.48.5",
2944+ "windows_i686_gnu 0.48.5",
2945+ "windows_i686_msvc 0.48.5",
2946+ "windows_x86_64_gnu 0.48.5",
2947+ "windows_x86_64_gnullvm 0.48.5",
2948+ "windows_x86_64_msvc 0.48.5",
2949+]
2950+
2951+[[package]]
2952+name = "windows-targets"
2953+version = "0.52.6"
2954+source = "registry+https://github.com/rust-lang/crates.io-index"
2955+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
2956+dependencies = [
2957+ "windows_aarch64_gnullvm 0.52.6",
2958+ "windows_aarch64_msvc 0.52.6",
2959+ "windows_i686_gnu 0.52.6",
2960+ "windows_i686_gnullvm",
2961+ "windows_i686_msvc 0.52.6",
2962+ "windows_x86_64_gnu 0.52.6",
2963+ "windows_x86_64_gnullvm 0.52.6",
2964+ "windows_x86_64_msvc 0.52.6",
2965+]
2966+
2967+[[package]]
2968+name = "windows_aarch64_gnullvm"
2969+version = "0.48.5"
2970+source = "registry+https://github.com/rust-lang/crates.io-index"
2971+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
2972+
2973+[[package]]
2974+name = "windows_aarch64_gnullvm"
2975+version = "0.52.6"
2976+source = "registry+https://github.com/rust-lang/crates.io-index"
2977+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
2978+
2979+[[package]]
2980+name = "windows_aarch64_msvc"
2981+version = "0.48.5"
2982+source = "registry+https://github.com/rust-lang/crates.io-index"
2983+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
2984+
2985+[[package]]
2986+name = "windows_aarch64_msvc"
2987+version = "0.52.6"
2988+source = "registry+https://github.com/rust-lang/crates.io-index"
2989+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
2990+
2991+[[package]]
2992+name = "windows_i686_gnu"
2993+version = "0.48.5"
2994+source = "registry+https://github.com/rust-lang/crates.io-index"
2995+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
2996+
2997+[[package]]
2998+name = "windows_i686_gnu"
2999+version = "0.52.6"
3000+source = "registry+https://github.com/rust-lang/crates.io-index"
3001+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
3002+
3003+[[package]]
3004+name = "windows_i686_gnullvm"
3005+version = "0.52.6"
3006+source = "registry+https://github.com/rust-lang/crates.io-index"
3007+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
3008+
3009+[[package]]
3010+name = "windows_i686_msvc"
3011+version = "0.48.5"
3012+source = "registry+https://github.com/rust-lang/crates.io-index"
3013+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
3014+
3015+[[package]]
3016+name = "windows_i686_msvc"
3017+version = "0.52.6"
3018+source = "registry+https://github.com/rust-lang/crates.io-index"
3019+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
3020+
3021+[[package]]
3022+name = "windows_x86_64_gnu"
3023+version = "0.48.5"
3024+source = "registry+https://github.com/rust-lang/crates.io-index"
3025+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
3026+
3027+[[package]]
3028+name = "windows_x86_64_gnu"
3029+version = "0.52.6"
3030+source = "registry+https://github.com/rust-lang/crates.io-index"
3031+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
3032+
3033+[[package]]
3034+name = "windows_x86_64_gnullvm"
3035+version = "0.48.5"
3036+source = "registry+https://github.com/rust-lang/crates.io-index"
3037+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
3038+
3039+[[package]]
3040+name = "windows_x86_64_gnullvm"
3041+version = "0.52.6"
3042+source = "registry+https://github.com/rust-lang/crates.io-index"
3043+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
3044+
3045+[[package]]
3046+name = "windows_x86_64_msvc"
3047+version = "0.48.5"
3048+source = "registry+https://github.com/rust-lang/crates.io-index"
3049+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
3050+
3051+[[package]]
3052+name = "windows_x86_64_msvc"
3053+version = "0.52.6"
3054+source = "registry+https://github.com/rust-lang/crates.io-index"
3055+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
3056+
3057+[[package]]
3058+name = "winnow"
3059+version = "1.0.4"
3060+source = "registry+https://github.com/rust-lang/crates.io-index"
3061+checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
3062+dependencies = [
3063+ "memchr",
3064+]
3065+
3066+[[package]]
3067+name = "writeable"
3068+version = "0.6.3"
3069+source = "registry+https://github.com/rust-lang/crates.io-index"
3070+checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
3071+
3072+[[package]]
3073+name = "yoke"
3074+version = "0.8.3"
3075+source = "registry+https://github.com/rust-lang/crates.io-index"
3076+checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
3077+dependencies = [
3078+ "stable_deref_trait",
3079+ "yoke-derive",
3080+ "zerofrom",
3081+]
3082+
3083+[[package]]
3084+name = "yoke-derive"
3085+version = "0.8.2"
3086+source = "registry+https://github.com/rust-lang/crates.io-index"
3087+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
3088+dependencies = [
3089+ "proc-macro2",
3090+ "quote",
3091+ "syn 2.0.119",
3092+ "synstructure",
3093+]
3094+
3095+[[package]]
3096+name = "zerocopy"
3097+version = "0.8.56"
3098+source = "registry+https://github.com/rust-lang/crates.io-index"
3099+checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb"
3100+dependencies = [
3101+ "zerocopy-derive",
3102+]
3103+
3104+[[package]]
3105+name = "zerocopy-derive"
3106+version = "0.8.56"
3107+source = "registry+https://github.com/rust-lang/crates.io-index"
3108+checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1"
3109+dependencies = [
3110+ "proc-macro2",
3111+ "quote",
3112+ "syn 2.0.119",
3113+]
3114+
3115+[[package]]
3116+name = "zerofrom"
3117+version = "0.1.8"
3118+source = "registry+https://github.com/rust-lang/crates.io-index"
3119+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
3120+dependencies = [
3121+ "zerofrom-derive",
3122+]
3123+
3124+[[package]]
3125+name = "zerofrom-derive"
3126+version = "0.1.7"
3127+source = "registry+https://github.com/rust-lang/crates.io-index"
3128+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
3129+dependencies = [
3130+ "proc-macro2",
3131+ "quote",
3132+ "syn 2.0.119",
3133+ "synstructure",
3134+]
3135+
3136+[[package]]
3137+name = "zeroize"
3138+version = "1.9.0"
3139+source = "registry+https://github.com/rust-lang/crates.io-index"
3140+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
3141+
3142+[[package]]
3143+name = "zerotrie"
3144+version = "0.2.4"
3145+source = "registry+https://github.com/rust-lang/crates.io-index"
3146+checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
3147+dependencies = [
3148+ "displaydoc",
3149+ "yoke",
3150+ "zerofrom",
3151+]
3152+
3153+[[package]]
3154+name = "zerovec"
3155+version = "0.11.6"
3156+source = "registry+https://github.com/rust-lang/crates.io-index"
3157+checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
3158+dependencies = [
3159+ "yoke",
3160+ "zerofrom",
3161+ "zerovec-derive",
3162+]
3163+
3164+[[package]]
3165+name = "zerovec-derive"
3166+version = "0.11.3"
3167+source = "registry+https://github.com/rust-lang/crates.io-index"
3168+checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
3169+dependencies = [
3170+ "proc-macro2",
3171+ "quote",
3172+ "syn 2.0.119",
3173+]
3174+
3175+[[package]]
3176+name = "zmij"
3177+version = "1.0.23"
3178+source = "registry+https://github.com/rust-lang/crates.io-index"
3179+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
new file mode 100644
@@ -0,0 +1,3179 @@
1+# This file is automatically @generated by Cargo.
2+# It is not intended for manual editing.
3+version = 4
4+
5+[[package]]
6+name = "aho-corasick"
7+version = "1.1.5"
8+source = "registry+https://github.com/rust-lang/crates.io-index"
9+checksum = "c982642fa9e8606056828ee9a8505737230110bb1099153c79efe865c59d12ba"
10+dependencies = [
11+ "memchr",
12+]
13+
14+[[package]]
15+name = "allocator-api2"
16+version = "0.2.21"
17+source = "registry+https://github.com/rust-lang/crates.io-index"
18+checksum = "683d7910e743518b0e34f1186f92494becacb047c7b6bf616c96772180fef923"
19+
20+[[package]]
21+name = "android_system_properties"
22+version = "0.1.6"
23+source = "registry+https://github.com/rust-lang/crates.io-index"
24+checksum = "ae221649c9976a6f6c56ae1facf410f3ddb33cc661c4b7b61020a912d4237fbc"
25+dependencies = [
26+ "libc",
27+]
28+
29+[[package]]
30+name = "anstream"
31+version = "1.0.0"
32+source = "registry+https://github.com/rust-lang/crates.io-index"
33+checksum = "824a212faf96e9acacdbd09febd34438f8f711fb84e09a8916013cd7815ca28d"
34+dependencies = [
35+ "anstyle",
36+ "anstyle-parse",
37+ "anstyle-query",
38+ "anstyle-wincon",
39+ "colorchoice",
40+ "is_terminal_polyfill",
41+ "utf8parse",
42+]
43+
44+[[package]]
45+name = "anstyle"
46+version = "1.0.14"
47+source = "registry+https://github.com/rust-lang/crates.io-index"
48+checksum = "940b3a0ca603d1eade50a4846a2afffd5ef57a9feac2c0e2ec2e14f9ead76000"
49+
50+[[package]]
51+name = "anstyle-parse"
52+version = "1.0.0"
53+source = "registry+https://github.com/rust-lang/crates.io-index"
54+checksum = "52ce7f38b242319f7cabaa6813055467063ecdc9d355bbb4ce0c68908cd8130e"
55+dependencies = [
56+ "utf8parse",
57+]
58+
59+[[package]]
60+name = "anstyle-query"
61+version = "1.1.5"
62+source = "registry+https://github.com/rust-lang/crates.io-index"
63+checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
64+dependencies = [
65+ "windows-sys 0.61.2",
66+]
67+
68+[[package]]
69+name = "anstyle-wincon"
70+version = "3.0.11"
71+source = "registry+https://github.com/rust-lang/crates.io-index"
72+checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
73+dependencies = [
74+ "anstyle",
75+ "once_cell_polyfill",
76+ "windows-sys 0.61.2",
77+]
78+
79+[[package]]
80+name = "anyhow"
81+version = "1.0.104"
82+source = "registry+https://github.com/rust-lang/crates.io-index"
83+checksum = "330a5ed07fa54e4702c9d6c4174f74427fc0ef6e214bbd677ae50a5099946470"
84+
85+[[package]]
86+name = "argon2"
87+version = "0.5.3"
88+source = "registry+https://github.com/rust-lang/crates.io-index"
89+checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072"
90+dependencies = [
91+ "base64ct",
92+ "blake2",
93+ "cpufeatures",
94+ "password-hash",
95+]
96+
97+[[package]]
98+name = "askama"
99+version = "0.16.0"
100+source = "registry+https://github.com/rust-lang/crates.io-index"
101+checksum = "f1bf825125edd887a019d0a3a837dcc5499a68b0d034cc3eb594070c3e18addc"
102+dependencies = [
103+ "askama_macros",
104+ "itoa",
105+ "percent-encoding",
106+ "serde",
107+ "serde_json",
108+]
109+
110+[[package]]
111+name = "askama_derive"
112+version = "0.16.0"
113+source = "registry+https://github.com/rust-lang/crates.io-index"
114+checksum = "e1c7065972a130eafa84215f21352ae15b4a7393da48c1f5e103904490736738"
115+dependencies = [
116+ "askama_parser",
117+ "basic-toml",
118+ "glob",
119+ "memchr",
120+ "proc-macro2",
121+ "quote",
122+ "rustc-hash",
123+ "serde",
124+ "serde_derive",
125+ "syn 2.0.119",
126+]
127+
128+[[package]]
129+name = "askama_macros"
130+version = "0.16.0"
131+source = "registry+https://github.com/rust-lang/crates.io-index"
132+checksum = "0e23b1d2c4bd39a41971f6124cef4cc6fd0540913ecb90919b69ab3bbe44ae1a"
133+dependencies = [
134+ "askama_derive",
135+]
136+
137+[[package]]
138+name = "askama_parser"
139+version = "0.16.0"
140+source = "registry+https://github.com/rust-lang/crates.io-index"
141+checksum = "7db09fde9143e7ac4513358fb32ee32847125b63b18ea715afd487956da715da"
142+dependencies = [
143+ "rustc-hash",
144+ "serde",
145+ "serde_derive",
146+ "unicode-ident",
147+ "winnow",
148+]
149+
150+[[package]]
151+name = "atoi"
152+version = "2.0.0"
153+source = "registry+https://github.com/rust-lang/crates.io-index"
154+checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528"
155+dependencies = [
156+ "num-traits",
157+]
158+
159+[[package]]
160+name = "atomic-waker"
161+version = "1.1.2"
162+source = "registry+https://github.com/rust-lang/crates.io-index"
163+checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0"
164+
165+[[package]]
166+name = "autocfg"
167+version = "1.5.1"
168+source = "registry+https://github.com/rust-lang/crates.io-index"
169+checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
170+
171+[[package]]
172+name = "axum"
173+version = "0.8.9"
174+source = "registry+https://github.com/rust-lang/crates.io-index"
175+checksum = "31b698c5f9a010f6573133b09e0de5408834d0c82f8d7475a89fc1867a71cd90"
176+dependencies = [
177+ "axum-core",
178+ "bytes",
179+ "form_urlencoded",
180+ "futures-util",
181+ "http",
182+ "http-body",
183+ "http-body-util",
184+ "hyper",
185+ "hyper-util",
186+ "itoa",
187+ "matchit",
188+ "memchr",
189+ "mime",
190+ "percent-encoding",
191+ "pin-project-lite",
192+ "serde_core",
193+ "serde_json",
194+ "serde_path_to_error",
195+ "serde_urlencoded",
196+ "sync_wrapper",
197+ "tokio",
198+ "tower",
199+ "tower-layer",
200+ "tower-service",
201+ "tracing",
202+]
203+
204+[[package]]
205+name = "axum-core"
206+version = "0.5.6"
207+source = "registry+https://github.com/rust-lang/crates.io-index"
208+checksum = "08c78f31d7b1291f7ee735c1c6780ccde7785daae9a9206026862dab7d8792d1"
209+dependencies = [
210+ "bytes",
211+ "futures-core",
212+ "http",
213+ "http-body",
214+ "http-body-util",
215+ "mime",
216+ "pin-project-lite",
217+ "sync_wrapper",
218+ "tower-layer",
219+ "tower-service",
220+ "tracing",
221+]
222+
223+[[package]]
224+name = "axum-extra"
225+version = "0.12.6"
226+source = "registry+https://github.com/rust-lang/crates.io-index"
227+checksum = "be44683b41ccb9ab2d23a5230015c9c3c55be97a25e4428366de8873103f7970"
228+dependencies = [
229+ "axum",
230+ "axum-core",
231+ "bytes",
232+ "cookie",
233+ "futures-core",
234+ "futures-util",
235+ "http",
236+ "http-body",
237+ "http-body-util",
238+ "mime",
239+ "pin-project-lite",
240+ "tower-layer",
241+ "tower-service",
242+ "tracing",
243+]
244+
245+[[package]]
246+name = "base16ct"
247+version = "0.2.0"
248+source = "registry+https://github.com/rust-lang/crates.io-index"
249+checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf"
250+
251+[[package]]
252+name = "base64"
253+version = "0.13.1"
254+source = "registry+https://github.com/rust-lang/crates.io-index"
255+checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8"
256+
257+[[package]]
258+name = "base64"
259+version = "0.22.1"
260+source = "registry+https://github.com/rust-lang/crates.io-index"
261+checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6"
262+
263+[[package]]
264+name = "base64ct"
265+version = "1.8.3"
266+source = "registry+https://github.com/rust-lang/crates.io-index"
267+checksum = "2af50177e190e07a26ab74f8b1efbfe2ef87da2116221318cb1c2e82baf7de06"
268+
269+[[package]]
270+name = "basic-toml"
271+version = "0.1.10"
272+source = "registry+https://github.com/rust-lang/crates.io-index"
273+checksum = "ba62675e8242a4c4e806d12f11d136e626e6c8361d6b829310732241652a178a"
274+dependencies = [
275+ "serde",
276+]
277+
278+[[package]]
279+name = "biscuit-auth"
280+version = "6.0.0"
281+source = "registry+https://github.com/rust-lang/crates.io-index"
282+checksum = "d5884fc86b3e21f5649ef4326e17ef729b3096e6502deaf13db7b7fb05bb992b"
283+dependencies = [
284+ "base64 0.13.1",
285+ "biscuit-parser",
286+ "biscuit-quote",
287+ "ecdsa",
288+ "ed25519-dalek",
289+ "elliptic-curve",
290+ "getrandom 0.2.17",
291+ "hex",
292+ "nom",
293+ "p256",
294+ "pkcs8 0.9.0",
295+ "prost",
296+ "prost-types",
297+ "rand",
298+ "rand_core",
299+ "regex",
300+ "serde_json",
301+ "sha2 0.9.9",
302+ "thiserror 1.0.69",
303+ "time",
304+ "zeroize",
305+]
306+
307+[[package]]
308+name = "biscuit-parser"
309+version = "0.2.0"
310+source = "registry+https://github.com/rust-lang/crates.io-index"
311+checksum = "9d7cafdbc8c30e1f0fb87df7161bec77f6f00da652cc33f102b0f95bd1cbc0fa"
312+dependencies = [
313+ "hex",
314+ "nom",
315+ "proc-macro2",
316+ "quote",
317+ "thiserror 1.0.69",
318+ "time",
319+]
320+
321+[[package]]
322+name = "biscuit-quote"
323+version = "0.3.0"
324+source = "registry+https://github.com/rust-lang/crates.io-index"
325+checksum = "49d2332c742a07a846f1fb2760e58a0ee60f2bc30987046fcea816b40630335a"
326+dependencies = [
327+ "biscuit-parser",
328+ "proc-macro-error2",
329+ "proc-macro2",
330+ "quote",
331+ "syn 1.0.109",
332+]
333+
334+[[package]]
335+name = "bitflags"
336+version = "2.13.1"
337+source = "registry+https://github.com/rust-lang/crates.io-index"
338+checksum = "b588b76d00fde79687d7646a9b5bdf3cc0f655e0bbd080335a95d7e96f3587da"
339+dependencies = [
340+ "serde_core",
341+]
342+
343+[[package]]
344+name = "blake2"
345+version = "0.10.6"
346+source = "registry+https://github.com/rust-lang/crates.io-index"
347+checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe"
348+dependencies = [
349+ "digest 0.10.7",
350+]
351+
352+[[package]]
353+name = "block-buffer"
354+version = "0.9.0"
355+source = "registry+https://github.com/rust-lang/crates.io-index"
356+checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4"
357+dependencies = [
358+ "generic-array",
359+]
360+
361+[[package]]
362+name = "block-buffer"
363+version = "0.10.4"
364+source = "registry+https://github.com/rust-lang/crates.io-index"
365+checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71"
366+dependencies = [
367+ "generic-array",
368+]
369+
370+[[package]]
371+name = "bumpalo"
372+version = "3.20.3"
373+source = "registry+https://github.com/rust-lang/crates.io-index"
374+checksum = "72f5acc6cb2ba439de613abc23857ec3d78374d8ed5ac84e9d11336e87da8649"
375+
376+[[package]]
377+name = "byteorder"
378+version = "1.5.0"
379+source = "registry+https://github.com/rust-lang/crates.io-index"
380+checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
381+
382+[[package]]
383+name = "bytes"
384+version = "1.12.1"
385+source = "registry+https://github.com/rust-lang/crates.io-index"
386+checksum = "fc652a48c352aef3ea3aed32080501cf3ef6ed5da78602a020c991775b0aff04"
387+
388+[[package]]
389+name = "cc"
390+version = "1.4.0"
391+source = "registry+https://github.com/rust-lang/crates.io-index"
392+checksum = "5add81bb678e6cb321aff7fa0dc7689ad82b112dbc032cea19f91d6b8e3582b9"
393+dependencies = [
394+ "find-msvc-tools",
395+ "shlex",
396+]
397+
398+[[package]]
399+name = "cfg-if"
400+version = "1.0.4"
401+source = "registry+https://github.com/rust-lang/crates.io-index"
402+checksum = "9330f8b2ff13f34540b44e946ef35111825727b38d33286ef986142615121801"
403+
404+[[package]]
405+name = "chrono"
406+version = "0.4.45"
407+source = "registry+https://github.com/rust-lang/crates.io-index"
408+checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327"
409+dependencies = [
410+ "iana-time-zone",
411+ "js-sys",
412+ "num-traits",
413+ "serde",
414+ "wasm-bindgen",
415+ "windows-link",
416+]
417+
418+[[package]]
419+name = "chrono-tz"
420+version = "0.10.4"
421+source = "registry+https://github.com/rust-lang/crates.io-index"
422+checksum = "a6139a8597ed92cf816dfb33f5dd6cf0bb93a6adc938f11039f371bc5bcd26c3"
423+dependencies = [
424+ "chrono",
425+ "phf",
426+]
427+
428+[[package]]
429+name = "clap"
430+version = "4.6.6"
431+source = "registry+https://github.com/rust-lang/crates.io-index"
432+checksum = "473c7e07f409a8d772161724aa8db6a765a2532a70f9667eeb7b49d3d02fbdca"
433+dependencies = [
434+ "clap_builder",
435+ "clap_derive",
436+]
437+
438+[[package]]
439+name = "clap_builder"
440+version = "4.6.6"
441+source = "registry+https://github.com/rust-lang/crates.io-index"
442+checksum = "7b48fea5a88e9ae728a2dcbedbfc0e730f7d60da42e1cb049a83c9fb8b789889"
443+dependencies = [
444+ "anstream",
445+ "anstyle",
446+ "clap_lex",
447+ "strsim",
448+]
449+
450+[[package]]
451+name = "clap_derive"
452+version = "4.6.4"
453+source = "registry+https://github.com/rust-lang/crates.io-index"
454+checksum = "d012d2b9d65aca7f18f4d9878a045bc17899bba951561ba5ec3c2ba1eed9a061"
455+dependencies = [
456+ "heck",
457+ "proc-macro2",
458+ "quote",
459+ "syn 3.0.3",
460+]
461+
462+[[package]]
463+name = "clap_lex"
464+version = "1.1.0"
465+source = "registry+https://github.com/rust-lang/crates.io-index"
466+checksum = "c8d4a3bb8b1e0c1050499d1815f5ab16d04f0959b233085fb31653fbfc9d98f9"
467+
468+[[package]]
469+name = "colorchoice"
470+version = "1.0.5"
471+source = "registry+https://github.com/rust-lang/crates.io-index"
472+checksum = "1d07550c9036bf2ae0c684c4297d503f838287c83c53686d05370d0e139ae570"
473+
474+[[package]]
475+name = "const-oid"
476+version = "0.9.6"
477+source = "registry+https://github.com/rust-lang/crates.io-index"
478+checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8"
479+
480+[[package]]
481+name = "cookie"
482+version = "0.18.1"
483+source = "registry+https://github.com/rust-lang/crates.io-index"
484+checksum = "4ddef33a339a91ea89fb53151bd0a4689cfce27055c291dfa69945475d22c747"
485+dependencies = [
486+ "percent-encoding",
487+ "time",
488+ "version_check",
489+]
490+
491+[[package]]
492+name = "core-foundation-sys"
493+version = "0.8.7"
494+source = "registry+https://github.com/rust-lang/crates.io-index"
495+checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b"
496+
497+[[package]]
498+name = "cpufeatures"
499+version = "0.2.17"
500+source = "registry+https://github.com/rust-lang/crates.io-index"
501+checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280"
502+dependencies = [
503+ "libc",
504+]
505+
506+[[package]]
507+name = "crc"
508+version = "3.4.0"
509+source = "registry+https://github.com/rust-lang/crates.io-index"
510+checksum = "5eb8a2a1cd12ab0d987a5d5e825195d372001a4094a0376319d5a0ad71c1ba0d"
511+dependencies = [
512+ "crc-catalog",
513+]
514+
515+[[package]]
516+name = "crc-catalog"
517+version = "2.5.0"
518+source = "registry+https://github.com/rust-lang/crates.io-index"
519+checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853"
520+
521+[[package]]
522+name = "crossbeam-queue"
523+version = "0.3.13"
524+source = "registry+https://github.com/rust-lang/crates.io-index"
525+checksum = "803d13fb3b09d88be9f4dbc29062c66b19bf7170867ceb746d2a8689bf6c7a26"
526+dependencies = [
527+ "crossbeam-utils",
528+]
529+
530+[[package]]
531+name = "crossbeam-utils"
532+version = "0.8.22"
533+source = "registry+https://github.com/rust-lang/crates.io-index"
534+checksum = "61803da095bee82a81bb1a452ecc25d3b2f1416d1897eb86430c6159ef717c17"
535+
536+[[package]]
537+name = "crypto-bigint"
538+version = "0.5.5"
539+source = "registry+https://github.com/rust-lang/crates.io-index"
540+checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76"
541+dependencies = [
542+ "generic-array",
543+ "rand_core",
544+ "subtle",
545+ "zeroize",
546+]
547+
548+[[package]]
549+name = "crypto-common"
550+version = "0.1.6"
551+source = "registry+https://github.com/rust-lang/crates.io-index"
552+checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3"
553+dependencies = [
554+ "generic-array",
555+ "typenum",
556+]
557+
558+[[package]]
559+name = "curve25519-dalek"
560+version = "4.1.3"
561+source = "registry+https://github.com/rust-lang/crates.io-index"
562+checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be"
563+dependencies = [
564+ "cfg-if",
565+ "cpufeatures",
566+ "curve25519-dalek-derive",
567+ "digest 0.10.7",
568+ "fiat-crypto",
569+ "rustc_version",
570+ "subtle",
571+ "zeroize",
572+]
573+
574+[[package]]
575+name = "curve25519-dalek-derive"
576+version = "0.1.1"
577+source = "registry+https://github.com/rust-lang/crates.io-index"
578+checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3"
579+dependencies = [
580+ "proc-macro2",
581+ "quote",
582+ "syn 2.0.119",
583+]
584+
585+[[package]]
586+name = "der"
587+version = "0.6.1"
588+source = "registry+https://github.com/rust-lang/crates.io-index"
589+checksum = "f1a467a65c5e759bce6e65eaf91cc29f466cdc57cb65777bd646872a8a1fd4de"
590+dependencies = [
591+ "const-oid",
592+]
593+
594+[[package]]
595+name = "der"
596+version = "0.7.10"
597+source = "registry+https://github.com/rust-lang/crates.io-index"
598+checksum = "e7c1832837b905bbfb5101e07cc24c8deddf52f93225eee6ead5f4d63d53ddcb"
599+dependencies = [
600+ "const-oid",
601+ "pem-rfc7468",
602+ "zeroize",
603+]
604+
605+[[package]]
606+name = "deranged"
607+version = "0.5.8"
608+source = "registry+https://github.com/rust-lang/crates.io-index"
609+checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
610+
611+[[package]]
612+name = "digest"
613+version = "0.9.0"
614+source = "registry+https://github.com/rust-lang/crates.io-index"
615+checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066"
616+dependencies = [
617+ "generic-array",
618+]
619+
620+[[package]]
621+name = "digest"
622+version = "0.10.7"
623+source = "registry+https://github.com/rust-lang/crates.io-index"
624+checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292"
625+dependencies = [
626+ "block-buffer 0.10.4",
627+ "const-oid",
628+ "crypto-common",
629+ "subtle",
630+]
631+
632+[[package]]
633+name = "displaydoc"
634+version = "0.2.7"
635+source = "registry+https://github.com/rust-lang/crates.io-index"
636+checksum = "c6232dd377dcc64799954cbd3a9bb882e9cdc1308ccd87b1c098f1fb2eaf82a8"
637+dependencies = [
638+ "proc-macro2",
639+ "quote",
640+ "syn 3.0.3",
641+]
642+
643+[[package]]
644+name = "dotenvy"
645+version = "0.15.7"
646+source = "registry+https://github.com/rust-lang/crates.io-index"
647+checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b"
648+
649+[[package]]
650+name = "ecdsa"
651+version = "0.16.9"
652+source = "registry+https://github.com/rust-lang/crates.io-index"
653+checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca"
654+dependencies = [
655+ "der 0.7.10",
656+ "digest 0.10.7",
657+ "elliptic-curve",
658+ "rfc6979",
659+ "serdect",
660+ "signature",
661+ "spki 0.7.3",
662+]
663+
664+[[package]]
665+name = "ed25519"
666+version = "2.2.3"
667+source = "registry+https://github.com/rust-lang/crates.io-index"
668+checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53"
669+dependencies = [
670+ "pkcs8 0.10.2",
671+ "signature",
672+]
673+
674+[[package]]
675+name = "ed25519-dalek"
676+version = "2.2.0"
677+source = "registry+https://github.com/rust-lang/crates.io-index"
678+checksum = "70e796c081cee67dc755e1a36a0a172b897fab85fc3f6bc48307991f64e4eca9"
679+dependencies = [
680+ "curve25519-dalek",
681+ "ed25519",
682+ "rand_core",
683+ "serde",
684+ "sha2 0.10.9",
685+ "subtle",
686+ "zeroize",
687+]
688+
689+[[package]]
690+name = "either"
691+version = "1.17.0"
692+source = "registry+https://github.com/rust-lang/crates.io-index"
693+checksum = "9e5e8f6c15a24b9a3ee5efec809ccd006d3b30e8b3bb63c39af737c7f87daa1d"
694+dependencies = [
695+ "serde",
696+]
697+
698+[[package]]
699+name = "elliptic-curve"
700+version = "0.13.8"
701+source = "registry+https://github.com/rust-lang/crates.io-index"
702+checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47"
703+dependencies = [
704+ "base16ct",
705+ "crypto-bigint",
706+ "digest 0.10.7",
707+ "ff",
708+ "generic-array",
709+ "group",
710+ "pem-rfc7468",
711+ "pkcs8 0.10.2",
712+ "rand_core",
713+ "sec1",
714+ "serdect",
715+ "subtle",
716+ "zeroize",
717+]
718+
719+[[package]]
720+name = "equivalent"
721+version = "1.0.2"
722+source = "registry+https://github.com/rust-lang/crates.io-index"
723+checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
724+
725+[[package]]
726+name = "errno"
727+version = "0.3.14"
728+source = "registry+https://github.com/rust-lang/crates.io-index"
729+checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
730+dependencies = [
731+ "libc",
732+ "windows-sys 0.61.2",
733+]
734+
735+[[package]]
736+name = "etcetera"
737+version = "0.8.0"
738+source = "registry+https://github.com/rust-lang/crates.io-index"
739+checksum = "136d1b5283a1ab77bd9257427ffd09d8667ced0570b6f938942bc7568ed5b943"
740+dependencies = [
741+ "cfg-if",
742+ "home",
743+ "windows-sys 0.48.0",
744+]
745+
746+[[package]]
747+name = "event-listener"
748+version = "5.4.2"
749+source = "registry+https://github.com/rust-lang/crates.io-index"
750+checksum = "5a23add41df1562121a9393cb065eab5146a1242410f23a644851e90cfd669d2"
751+dependencies = [
752+ "parking",
753+ "pin-project-lite",
754+]
755+
756+[[package]]
757+name = "ff"
758+version = "0.13.1"
759+source = "registry+https://github.com/rust-lang/crates.io-index"
760+checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393"
761+dependencies = [
762+ "rand_core",
763+ "subtle",
764+]
765+
766+[[package]]
767+name = "fiat-crypto"
768+version = "0.2.9"
769+source = "registry+https://github.com/rust-lang/crates.io-index"
770+checksum = "28dea519a9695b9977216879a3ebfddf92f1c08c05d984f8996aecd6ecdc811d"
771+
772+[[package]]
773+name = "find-msvc-tools"
774+version = "0.1.9"
775+source = "registry+https://github.com/rust-lang/crates.io-index"
776+checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582"
777+
778+[[package]]
779+name = "flume"
780+version = "0.11.1"
781+source = "registry+https://github.com/rust-lang/crates.io-index"
782+checksum = "da0e4dd2a88388a1f4ccc7c9ce104604dab68d9f408dc34cd45823d5a9069095"
783+dependencies = [
784+ "futures-core",
785+ "futures-sink",
786+ "spin",
787+]
788+
789+[[package]]
790+name = "foldhash"
791+version = "0.1.5"
792+source = "registry+https://github.com/rust-lang/crates.io-index"
793+checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
794+
795+[[package]]
796+name = "form_urlencoded"
797+version = "1.2.2"
798+source = "registry+https://github.com/rust-lang/crates.io-index"
799+checksum = "cb4cb245038516f5f85277875cdaa4f7d2c9a0fa0468de06ed190163b1581fcf"
800+dependencies = [
801+ "percent-encoding",
802+]
803+
804+[[package]]
805+name = "futures-channel"
806+version = "0.3.33"
807+source = "registry+https://github.com/rust-lang/crates.io-index"
808+checksum = "262590f4fe6afeb0bc83be1daa64e52657fe185690a958af7f3ad0e92085c5ae"
809+dependencies = [
810+ "futures-core",
811+ "futures-sink",
812+]
813+
814+[[package]]
815+name = "futures-core"
816+version = "0.3.33"
817+source = "registry+https://github.com/rust-lang/crates.io-index"
818+checksum = "2cd50c473c80f6d7c3670a752354b8e569b1a7cbfdc0419ec88e5edad85e0dc7"
819+
820+[[package]]
821+name = "futures-executor"
822+version = "0.3.33"
823+source = "registry+https://github.com/rust-lang/crates.io-index"
824+checksum = "6754879cc9f2c66f88c6e5c35344bb0bdb0708b0352b1201815667c7eabc7458"
825+dependencies = [
826+ "futures-core",
827+ "futures-task",
828+ "futures-util",
829+]
830+
831+[[package]]
832+name = "futures-intrusive"
833+version = "0.5.0"
834+source = "registry+https://github.com/rust-lang/crates.io-index"
835+checksum = "1d930c203dd0b6ff06e0201a4a2fe9149b43c684fd4420555b26d21b1a02956f"
836+dependencies = [
837+ "futures-core",
838+ "lock_api",
839+ "parking_lot",
840+]
841+
842+[[package]]
843+name = "futures-io"
844+version = "0.3.33"
845+source = "registry+https://github.com/rust-lang/crates.io-index"
846+checksum = "4577ecaa3c4f96589d473f679a71b596316f6641bc350038b962a5daf0085d7a"
847+
848+[[package]]
849+name = "futures-sink"
850+version = "0.3.33"
851+source = "registry+https://github.com/rust-lang/crates.io-index"
852+checksum = "e34418ac499d6305c2fb5ad0ed2f6ac998c5f8ca209b4510f7f94242c647e307"
853+
854+[[package]]
855+name = "futures-task"
856+version = "0.3.33"
857+source = "registry+https://github.com/rust-lang/crates.io-index"
858+checksum = "b231ed28831efb4a61a08580c4bc233ec56bc009f4cd8f52da2c3cb97df0c109"
859+
860+[[package]]
861+name = "futures-util"
862+version = "0.3.33"
863+source = "registry+https://github.com/rust-lang/crates.io-index"
864+checksum = "a77a90a256fce34da66415271e30f94ee91c57b04b8a2c042d9cf3220179deaa"
865+dependencies = [
866+ "futures-core",
867+ "futures-io",
868+ "futures-sink",
869+ "futures-task",
870+ "memchr",
871+ "pin-project-lite",
872+ "slab",
873+]
874+
875+[[package]]
876+name = "generic-array"
877+version = "0.14.9"
878+source = "registry+https://github.com/rust-lang/crates.io-index"
879+checksum = "4bb6743198531e02858aeaea5398fcc883e71851fcbcb5a2f773e2fb6cb1edf2"
880+dependencies = [
881+ "typenum",
882+ "version_check",
883+ "zeroize",
884+]
885+
886+[[package]]
887+name = "getrandom"
888+version = "0.2.17"
889+source = "registry+https://github.com/rust-lang/crates.io-index"
890+checksum = "ff2abc00be7fca6ebc474524697ae276ad847ad0a6b3faa4bcb027e9a4614ad0"
891+dependencies = [
892+ "cfg-if",
893+ "libc",
894+ "wasi",
895+]
896+
897+[[package]]
898+name = "getrandom"
899+version = "0.4.3"
900+source = "registry+https://github.com/rust-lang/crates.io-index"
901+checksum = "300e883d756b2e4ec94e02791f39b04b522276138852cfc41d9fb7e904106099"
902+dependencies = [
903+ "cfg-if",
904+ "libc",
905+ "r-efi",
906+]
907+
908+[[package]]
909+name = "glob"
910+version = "0.3.4"
911+source = "registry+https://github.com/rust-lang/crates.io-index"
912+checksum = "e4eba85ea1d0a966a983acd07deee566e67395d2d96b6fb39e62b5a833f1eb0b"
913+
914+[[package]]
915+name = "group"
916+version = "0.13.0"
917+source = "registry+https://github.com/rust-lang/crates.io-index"
918+checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63"
919+dependencies = [
920+ "ff",
921+ "rand_core",
922+ "subtle",
923+]
924+
925+[[package]]
926+name = "hashbrown"
927+version = "0.15.5"
928+source = "registry+https://github.com/rust-lang/crates.io-index"
929+checksum = "9229cfe53dfd69f0609a49f65461bd93001ea1ef889cd5529dd176593f5338a1"
930+dependencies = [
931+ "allocator-api2",
932+ "equivalent",
933+ "foldhash",
934+]
935+
936+[[package]]
937+name = "hashbrown"
938+version = "0.17.1"
939+source = "registry+https://github.com/rust-lang/crates.io-index"
940+checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
941+
942+[[package]]
943+name = "hashlink"
944+version = "0.10.0"
945+source = "registry+https://github.com/rust-lang/crates.io-index"
946+checksum = "7382cf6263419f2d8df38c55d7da83da5c18aef87fc7a7fc1fb1e344edfe14c1"
947+dependencies = [
948+ "hashbrown 0.15.5",
949+]
950+
951+[[package]]
952+name = "heck"
953+version = "0.5.0"
954+source = "registry+https://github.com/rust-lang/crates.io-index"
955+checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
956+
957+[[package]]
958+name = "hex"
959+version = "0.4.3"
960+source = "registry+https://github.com/rust-lang/crates.io-index"
961+checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70"
962+
963+[[package]]
964+name = "hkdf"
965+version = "0.12.4"
966+source = "registry+https://github.com/rust-lang/crates.io-index"
967+checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7"
968+dependencies = [
969+ "hmac",
970+]
971+
972+[[package]]
973+name = "hmac"
974+version = "0.12.1"
975+source = "registry+https://github.com/rust-lang/crates.io-index"
976+checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e"
977+dependencies = [
978+ "digest 0.10.7",
979+]
980+
981+[[package]]
982+name = "home"
983+version = "0.5.12"
984+source = "registry+https://github.com/rust-lang/crates.io-index"
985+checksum = "cc627f471c528ff0c4a49e1d5e60450c8f6461dd6d10ba9dcd3a61d3dff7728d"
986+dependencies = [
987+ "windows-sys 0.61.2",
988+]
989+
990+[[package]]
991+name = "http"
992+version = "1.5.0"
993+source = "registry+https://github.com/rust-lang/crates.io-index"
994+checksum = "918d3568bebf352712bc2ef3d46a8bcf1a75b373be6539de198e9105cbbf9ce0"
995+dependencies = [
996+ "bytes",
997+ "itoa",
998+]
999+
1000+[[package]]
1001+name = "http-body"
1002+version = "1.1.0"
1003+source = "registry+https://github.com/rust-lang/crates.io-index"
1004+checksum = "ca2a8f2913ee65f60facd6a5905613afaa448497a0230cc41ce022d93290bc2c"
1005+dependencies = [
1006+ "bytes",
1007+ "http",
1008+]
1009+
1010+[[package]]
1011+name = "http-body-util"
1012+version = "0.1.4"
1013+source = "registry+https://github.com/rust-lang/crates.io-index"
1014+checksum = "e9f41fd6a08e4d4ec69df65976da761afd5ad5e58a9d4acb46bd1c953a9e3ff2"
1015+dependencies = [
1016+ "bytes",
1017+ "futures-core",
1018+ "http",
1019+ "http-body",
1020+ "pin-project-lite",
1021+]
1022+
1023+[[package]]
1024+name = "httparse"
1025+version = "1.10.1"
1026+source = "registry+https://github.com/rust-lang/crates.io-index"
1027+checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87"
1028+
1029+[[package]]
1030+name = "httpdate"
1031+version = "1.0.3"
1032+source = "registry+https://github.com/rust-lang/crates.io-index"
1033+checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9"
1034+
1035+[[package]]
1036+name = "hyper"
1037+version = "1.11.0"
1038+source = "registry+https://github.com/rust-lang/crates.io-index"
1039+checksum = "d22053281f852e11534f5198498373cbb59295120a20771d90f7ed1897490a72"
1040+dependencies = [
1041+ "atomic-waker",
1042+ "bytes",
1043+ "futures-channel",
1044+ "futures-core",
1045+ "http",
1046+ "http-body",
1047+ "httparse",
1048+ "httpdate",
1049+ "itoa",
1050+ "pin-project-lite",
1051+ "smallvec",
1052+ "tokio",
1053+]
1054+
1055+[[package]]
1056+name = "hyper-util"
1057+version = "0.1.20"
1058+source = "registry+https://github.com/rust-lang/crates.io-index"
1059+checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0"
1060+dependencies = [
1061+ "bytes",
1062+ "http",
1063+ "http-body",
1064+ "hyper",
1065+ "pin-project-lite",
1066+ "tokio",
1067+ "tower-service",
1068+]
1069+
1070+[[package]]
1071+name = "iana-time-zone"
1072+version = "0.1.65"
1073+source = "registry+https://github.com/rust-lang/crates.io-index"
1074+checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470"
1075+dependencies = [
1076+ "android_system_properties",
1077+ "core-foundation-sys",
1078+ "iana-time-zone-haiku",
1079+ "js-sys",
1080+ "log",
1081+ "wasm-bindgen",
1082+ "windows-core",
1083+]
1084+
1085+[[package]]
1086+name = "iana-time-zone-haiku"
1087+version = "0.1.2"
1088+source = "registry+https://github.com/rust-lang/crates.io-index"
1089+checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f"
1090+dependencies = [
1091+ "cc",
1092+]
1093+
1094+[[package]]
1095+name = "icu_collections"
1096+version = "2.2.0"
1097+source = "registry+https://github.com/rust-lang/crates.io-index"
1098+checksum = "2984d1cd16c883d7935b9e07e44071dca8d917fd52ecc02c04d5fa0b5a3f191c"
1099+dependencies = [
1100+ "displaydoc",
1101+ "potential_utf",
1102+ "utf8_iter",
1103+ "yoke",
1104+ "zerofrom",
1105+ "zerovec",
1106+]
1107+
1108+[[package]]
1109+name = "icu_locale_core"
1110+version = "2.2.0"
1111+source = "registry+https://github.com/rust-lang/crates.io-index"
1112+checksum = "92219b62b3e2b4d88ac5119f8904c10f8f61bf7e95b640d25ba3075e6cac2c29"
1113+dependencies = [
1114+ "displaydoc",
1115+ "litemap",
1116+ "tinystr",
1117+ "writeable",
1118+ "zerovec",
1119+]
1120+
1121+[[package]]
1122+name = "icu_normalizer"
1123+version = "2.2.0"
1124+source = "registry+https://github.com/rust-lang/crates.io-index"
1125+checksum = "c56e5ee99d6e3d33bd91c5d85458b6005a22140021cc324cea84dd0e72cff3b4"
1126+dependencies = [
1127+ "icu_collections",
1128+ "icu_normalizer_data",
1129+ "icu_properties",
1130+ "icu_provider",
1131+ "smallvec",
1132+ "zerovec",
1133+]
1134+
1135+[[package]]
1136+name = "icu_normalizer_data"
1137+version = "2.2.0"
1138+source = "registry+https://github.com/rust-lang/crates.io-index"
1139+checksum = "da3be0ae77ea334f4da67c12f149704f19f81d1adf7c51cf482943e84a2bad38"
1140+
1141+[[package]]
1142+name = "icu_properties"
1143+version = "2.2.0"
1144+source = "registry+https://github.com/rust-lang/crates.io-index"
1145+checksum = "bee3b67d0ea5c2cca5003417989af8996f8604e34fb9ddf96208a033901e70de"
1146+dependencies = [
1147+ "icu_collections",
1148+ "icu_locale_core",
1149+ "icu_properties_data",
1150+ "icu_provider",
1151+ "zerotrie",
1152+ "zerovec",
1153+]
1154+
1155+[[package]]
1156+name = "icu_properties_data"
1157+version = "2.2.0"
1158+source = "registry+https://github.com/rust-lang/crates.io-index"
1159+checksum = "8e2bbb201e0c04f7b4b3e14382af113e17ba4f63e2c9d2ee626b720cbce54a14"
1160+
1161+[[package]]
1162+name = "icu_provider"
1163+version = "2.2.0"
1164+source = "registry+https://github.com/rust-lang/crates.io-index"
1165+checksum = "139c4cf31c8b5f33d7e199446eff9c1e02decfc2f0eec2c8d71f65befa45b421"
1166+dependencies = [
1167+ "displaydoc",
1168+ "icu_locale_core",
1169+ "writeable",
1170+ "yoke",
1171+ "zerofrom",
1172+ "zerotrie",
1173+ "zerovec",
1174+]
1175+
1176+[[package]]
1177+name = "idna"
1178+version = "1.1.0"
1179+source = "registry+https://github.com/rust-lang/crates.io-index"
1180+checksum = "3b0875f23caa03898994f6ddc501886a45c7d3d62d04d2d90788d47be1b1e4de"
1181+dependencies = [
1182+ "idna_adapter",
1183+ "smallvec",
1184+ "utf8_iter",
1185+]
1186+
1187+[[package]]
1188+name = "idna_adapter"
1189+version = "1.2.2"
1190+source = "registry+https://github.com/rust-lang/crates.io-index"
1191+checksum = "cb68373c0d6620ef8105e855e7745e18b0d00d3bdb07fb532e434244cdb9a714"
1192+dependencies = [
1193+ "icu_normalizer",
1194+ "icu_properties",
1195+]
1196+
1197+[[package]]
1198+name = "indexmap"
1199+version = "2.14.0"
1200+source = "registry+https://github.com/rust-lang/crates.io-index"
1201+checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
1202+dependencies = [
1203+ "equivalent",
1204+ "hashbrown 0.17.1",
1205+]
1206+
1207+[[package]]
1208+name = "is_terminal_polyfill"
1209+version = "1.70.2"
1210+source = "registry+https://github.com/rust-lang/crates.io-index"
1211+checksum = "a6cb138bb79a146c1bd460005623e142ef0181e3d0219cb493e02f7d08a35695"
1212+
1213+[[package]]
1214+name = "itertools"
1215+version = "0.10.5"
1216+source = "registry+https://github.com/rust-lang/crates.io-index"
1217+checksum = "b0fd2260e829bddf4cb6ea802289de2f86d6a7a690192fbe91b3f46e0f2c8473"
1218+dependencies = [
1219+ "either",
1220+]
1221+
1222+[[package]]
1223+name = "itoa"
1224+version = "1.0.18"
1225+source = "registry+https://github.com/rust-lang/crates.io-index"
1226+checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
1227+
1228+[[package]]
1229+name = "js-sys"
1230+version = "0.3.103"
1231+source = "registry+https://github.com/rust-lang/crates.io-index"
1232+checksum = "53b44bfcdb3f8d5837a46dae1ca9660a837176eee74a28b229bc626816589102"
1233+dependencies = [
1234+ "cfg-if",
1235+ "futures-util",
1236+ "wasm-bindgen",
1237+]
1238+
1239+[[package]]
1240+name = "lazy_static"
1241+version = "1.5.0"
1242+source = "registry+https://github.com/rust-lang/crates.io-index"
1243+checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe"
1244+dependencies = [
1245+ "spin",
1246+]
1247+
1248+[[package]]
1249+name = "libc"
1250+version = "0.2.189"
1251+source = "registry+https://github.com/rust-lang/crates.io-index"
1252+checksum = "3eaf3ede3fee6db1a4c2ee091bf8a8b4dccdc6d17f656fb07896ee72867612f2"
1253+
1254+[[package]]
1255+name = "libm"
1256+version = "0.2.16"
1257+source = "registry+https://github.com/rust-lang/crates.io-index"
1258+checksum = "b6d2cec3eae94f9f509c767b45932f1ada8350c4bdb85af2fcab4a3c14807981"
1259+
1260+[[package]]
1261+name = "libredox"
1262+version = "0.1.19"
1263+source = "registry+https://github.com/rust-lang/crates.io-index"
1264+checksum = "2026a5056764a10b2bf5d56488cba40da507f5493a6a429340e2004d9ed085fa"
1265+dependencies = [
1266+ "bitflags",
1267+ "libc",
1268+ "plain",
1269+ "redox_syscall 0.9.1",
1270+]
1271+
1272+[[package]]
1273+name = "libsqlite3-sys"
1274+version = "0.30.1"
1275+source = "registry+https://github.com/rust-lang/crates.io-index"
1276+checksum = "2e99fb7a497b1e3339bc746195567ed8d3e24945ecd636e3619d20b9de9e9149"
1277+dependencies = [
1278+ "pkg-config",
1279+ "vcpkg",
1280+]
1281+
1282+[[package]]
1283+name = "litemap"
1284+version = "0.8.2"
1285+source = "registry+https://github.com/rust-lang/crates.io-index"
1286+checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0"
1287+
1288+[[package]]
1289+name = "lock_api"
1290+version = "0.4.14"
1291+source = "registry+https://github.com/rust-lang/crates.io-index"
1292+checksum = "224399e74b87b5f3557511d98dff8b14089b3dadafcab6bb93eab67d3aace965"
1293+dependencies = [
1294+ "scopeguard",
1295+]
1296+
1297+[[package]]
1298+name = "log"
1299+version = "0.4.33"
1300+source = "registry+https://github.com/rust-lang/crates.io-index"
1301+checksum = "0ceec5bc11778974d1bcb055b18002eba7f4b3518b6a0081b3af5f21666da9ad"
1302+
1303+[[package]]
1304+name = "matchers"
1305+version = "0.2.0"
1306+source = "registry+https://github.com/rust-lang/crates.io-index"
1307+checksum = "d1525a2a28c7f4fa0fc98bb91ae755d1e2d1505079e05539e35bc876b5d65ae9"
1308+dependencies = [
1309+ "regex-automata",
1310+]
1311+
1312+[[package]]
1313+name = "matchit"
1314+version = "0.8.4"
1315+source = "registry+https://github.com/rust-lang/crates.io-index"
1316+checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
1317+
1318+[[package]]
1319+name = "md-5"
1320+version = "0.10.6"
1321+source = "registry+https://github.com/rust-lang/crates.io-index"
1322+checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf"
1323+dependencies = [
1324+ "cfg-if",
1325+ "digest 0.10.7",
1326+]
1327+
1328+[[package]]
1329+name = "memchr"
1330+version = "2.8.3"
1331+source = "registry+https://github.com/rust-lang/crates.io-index"
1332+checksum = "cf8baf1c55e62ffcace7a9f06f4bd9cd3f0c4beb022d3b367256b91b87513d98"
1333+
1334+[[package]]
1335+name = "mime"
1336+version = "0.3.17"
1337+source = "registry+https://github.com/rust-lang/crates.io-index"
1338+checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a"
1339+
1340+[[package]]
1341+name = "minimal-lexical"
1342+version = "0.2.1"
1343+source = "registry+https://github.com/rust-lang/crates.io-index"
1344+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
1345+
1346+[[package]]
1347+name = "mio"
1348+version = "1.2.2"
1349+source = "registry+https://github.com/rust-lang/crates.io-index"
1350+checksum = "30d65c71f1ce40ab09135ce117d742b9f8a19ff91a41a8b57ed50bc2de59c427"
1351+dependencies = [
1352+ "libc",
1353+ "wasi",
1354+ "windows-sys 0.61.2",
1355+]
1356+
1357+[[package]]
1358+name = "nom"
1359+version = "7.1.3"
1360+source = "registry+https://github.com/rust-lang/crates.io-index"
1361+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
1362+dependencies = [
1363+ "memchr",
1364+ "minimal-lexical",
1365+]
1366+
1367+[[package]]
1368+name = "nu-ansi-term"
1369+version = "0.50.3"
1370+source = "registry+https://github.com/rust-lang/crates.io-index"
1371+checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
1372+dependencies = [
1373+ "windows-sys 0.61.2",
1374+]
1375+
1376+[[package]]
1377+name = "num-bigint-dig"
1378+version = "0.8.6"
1379+source = "registry+https://github.com/rust-lang/crates.io-index"
1380+checksum = "e661dda6640fad38e827a6d4a310ff4763082116fe217f279885c97f511bb0b7"
1381+dependencies = [
1382+ "lazy_static",
1383+ "libm",
1384+ "num-integer",
1385+ "num-iter",
1386+ "num-traits",
1387+ "rand",
1388+ "smallvec",
1389+ "zeroize",
1390+]
1391+
1392+[[package]]
1393+name = "num-conv"
1394+version = "0.2.2"
1395+source = "registry+https://github.com/rust-lang/crates.io-index"
1396+checksum = "521739c6d2bac4aa25192232afe6841231376b2b26d4d9fae5ecf8ca5772e441"
1397+
1398+[[package]]
1399+name = "num-integer"
1400+version = "0.1.46"
1401+source = "registry+https://github.com/rust-lang/crates.io-index"
1402+checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f"
1403+dependencies = [
1404+ "num-traits",
1405+]
1406+
1407+[[package]]
1408+name = "num-iter"
1409+version = "0.1.46"
1410+source = "registry+https://github.com/rust-lang/crates.io-index"
1411+checksum = "c92800bd69a1eac91786bcfe9da64a897eb72911b8dc3095decbd07429e8048b"
1412+dependencies = [
1413+ "num-integer",
1414+ "num-traits",
1415+]
1416+
1417+[[package]]
1418+name = "num-traits"
1419+version = "0.2.19"
1420+source = "registry+https://github.com/rust-lang/crates.io-index"
1421+checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841"
1422+dependencies = [
1423+ "autocfg",
1424+ "libm",
1425+]
1426+
1427+[[package]]
1428+name = "once_cell"
1429+version = "1.21.4"
1430+source = "registry+https://github.com/rust-lang/crates.io-index"
1431+checksum = "9f7c3e4beb33f85d45ae3e3a1792185706c8e16d043238c593331cc7cd313b50"
1432+
1433+[[package]]
1434+name = "once_cell_polyfill"
1435+version = "1.70.2"
1436+source = "registry+https://github.com/rust-lang/crates.io-index"
1437+checksum = "384b8ab6d37215f3c5301a95a4accb5d64aa607f1fcb26a11b5303878451b4fe"
1438+
1439+[[package]]
1440+name = "opaque-debug"
1441+version = "0.3.1"
1442+source = "registry+https://github.com/rust-lang/crates.io-index"
1443+checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381"
1444+
1445+[[package]]
1446+name = "p256"
1447+version = "0.13.2"
1448+source = "registry+https://github.com/rust-lang/crates.io-index"
1449+checksum = "c9863ad85fa8f4460f9c48cb909d38a0d689dba1f6f6988a5e3e0d31071bcd4b"
1450+dependencies = [
1451+ "ecdsa",
1452+ "elliptic-curve",
1453+ "primeorder",
1454+ "sha2 0.10.9",
1455+]
1456+
1457+[[package]]
1458+name = "parking"
1459+version = "2.2.1"
1460+source = "registry+https://github.com/rust-lang/crates.io-index"
1461+checksum = "f38d5652c16fde515bb1ecef450ab0f6a219d619a7274976324d5e377f7dceba"
1462+
1463+[[package]]
1464+name = "parking_lot"
1465+version = "0.12.5"
1466+source = "registry+https://github.com/rust-lang/crates.io-index"
1467+checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a"
1468+dependencies = [
1469+ "lock_api",
1470+ "parking_lot_core",
1471+]
1472+
1473+[[package]]
1474+name = "parking_lot_core"
1475+version = "0.9.12"
1476+source = "registry+https://github.com/rust-lang/crates.io-index"
1477+checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1"
1478+dependencies = [
1479+ "cfg-if",
1480+ "libc",
1481+ "redox_syscall 0.5.18",
1482+ "smallvec",
1483+ "windows-link",
1484+]
1485+
1486+[[package]]
1487+name = "password-hash"
1488+version = "0.5.0"
1489+source = "registry+https://github.com/rust-lang/crates.io-index"
1490+checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166"
1491+dependencies = [
1492+ "base64ct",
1493+ "rand_core",
1494+ "subtle",
1495+]
1496+
1497+[[package]]
1498+name = "pem-rfc7468"
1499+version = "0.7.0"
1500+source = "registry+https://github.com/rust-lang/crates.io-index"
1501+checksum = "88b39c9bfcfc231068454382784bb460aae594343fb030d46e9f50a645418412"
1502+dependencies = [
1503+ "base64ct",
1504+]
1505+
1506+[[package]]
1507+name = "percent-encoding"
1508+version = "2.3.2"
1509+source = "registry+https://github.com/rust-lang/crates.io-index"
1510+checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220"
1511+
1512+[[package]]
1513+name = "phf"
1514+version = "0.12.1"
1515+source = "registry+https://github.com/rust-lang/crates.io-index"
1516+checksum = "913273894cec178f401a31ec4b656318d95473527be05c0752cc41cdc32be8b7"
1517+dependencies = [
1518+ "phf_shared",
1519+]
1520+
1521+[[package]]
1522+name = "phf_shared"
1523+version = "0.12.1"
1524+source = "registry+https://github.com/rust-lang/crates.io-index"
1525+checksum = "06005508882fb681fd97892ecff4b7fd0fee13ef1aa569f8695dae7ab9099981"
1526+dependencies = [
1527+ "siphasher",
1528+]
1529+
1530+[[package]]
1531+name = "pin-project-lite"
1532+version = "0.2.17"
1533+source = "registry+https://github.com/rust-lang/crates.io-index"
1534+checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
1535+
1536+[[package]]
1537+name = "pkcs1"
1538+version = "0.7.5"
1539+source = "registry+https://github.com/rust-lang/crates.io-index"
1540+checksum = "c8ffb9f10fa047879315e6625af03c164b16962a5368d724ed16323b68ace47f"
1541+dependencies = [
1542+ "der 0.7.10",
1543+ "pkcs8 0.10.2",
1544+ "spki 0.7.3",
1545+]
1546+
1547+[[package]]
1548+name = "pkcs8"
1549+version = "0.9.0"
1550+source = "registry+https://github.com/rust-lang/crates.io-index"
1551+checksum = "9eca2c590a5f85da82668fa685c09ce2888b9430e83299debf1f34b65fd4a4ba"
1552+dependencies = [
1553+ "der 0.6.1",
1554+ "spki 0.6.0",
1555+]
1556+
1557+[[package]]
1558+name = "pkcs8"
1559+version = "0.10.2"
1560+source = "registry+https://github.com/rust-lang/crates.io-index"
1561+checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7"
1562+dependencies = [
1563+ "der 0.7.10",
1564+ "spki 0.7.3",
1565+]
1566+
1567+[[package]]
1568+name = "pkg-config"
1569+version = "0.3.33"
1570+source = "registry+https://github.com/rust-lang/crates.io-index"
1571+checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
1572+
1573+[[package]]
1574+name = "plain"
1575+version = "0.2.3"
1576+source = "registry+https://github.com/rust-lang/crates.io-index"
1577+checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6"
1578+
1579+[[package]]
1580+name = "potential_utf"
1581+version = "0.1.5"
1582+source = "registry+https://github.com/rust-lang/crates.io-index"
1583+checksum = "0103b1cef7ec0cf76490e969665504990193874ea05c85ff9bab8b911d0a0564"
1584+dependencies = [
1585+ "zerovec",
1586+]
1587+
1588+[[package]]
1589+name = "powerfmt"
1590+version = "0.2.0"
1591+source = "registry+https://github.com/rust-lang/crates.io-index"
1592+checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
1593+
1594+[[package]]
1595+name = "ppv-lite86"
1596+version = "0.2.21"
1597+source = "registry+https://github.com/rust-lang/crates.io-index"
1598+checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9"
1599+dependencies = [
1600+ "zerocopy",
1601+]
1602+
1603+[[package]]
1604+name = "primeorder"
1605+version = "0.13.6"
1606+source = "registry+https://github.com/rust-lang/crates.io-index"
1607+checksum = "353e1ca18966c16d9deb1c69278edbc5f194139612772bd9537af60ac231e1e6"
1608+dependencies = [
1609+ "elliptic-curve",
1610+]
1611+
1612+[[package]]
1613+name = "proc-macro-error-attr2"
1614+version = "2.0.0"
1615+source = "registry+https://github.com/rust-lang/crates.io-index"
1616+checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5"
1617+dependencies = [
1618+ "proc-macro2",
1619+ "quote",
1620+]
1621+
1622+[[package]]
1623+name = "proc-macro-error2"
1624+version = "2.0.1"
1625+source = "registry+https://github.com/rust-lang/crates.io-index"
1626+checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802"
1627+dependencies = [
1628+ "proc-macro-error-attr2",
1629+ "proc-macro2",
1630+ "quote",
1631+ "syn 2.0.119",
1632+]
1633+
1634+[[package]]
1635+name = "proc-macro2"
1636+version = "1.0.107"
1637+source = "registry+https://github.com/rust-lang/crates.io-index"
1638+checksum = "985e7ec9bb745e6ce6535b544d84d6cd6f7ad8bd711c398938ae983b91a766d9"
1639+dependencies = [
1640+ "unicode-ident",
1641+]
1642+
1643+[[package]]
1644+name = "prost"
1645+version = "0.10.4"
1646+source = "registry+https://github.com/rust-lang/crates.io-index"
1647+checksum = "71adf41db68aa0daaefc69bb30bcd68ded9b9abaad5d1fbb6304c4fb390e083e"
1648+dependencies = [
1649+ "bytes",
1650+ "prost-derive",
1651+]
1652+
1653+[[package]]
1654+name = "prost-derive"
1655+version = "0.10.1"
1656+source = "registry+https://github.com/rust-lang/crates.io-index"
1657+checksum = "7b670f45da57fb8542ebdbb6105a925fe571b67f9e7ed9f47a06a84e72b4e7cc"
1658+dependencies = [
1659+ "anyhow",
1660+ "itertools",
1661+ "proc-macro2",
1662+ "quote",
1663+ "syn 1.0.109",
1664+]
1665+
1666+[[package]]
1667+name = "prost-types"
1668+version = "0.10.1"
1669+source = "registry+https://github.com/rust-lang/crates.io-index"
1670+checksum = "2d0a014229361011dc8e69c8a1ec6c2e8d0f2af7c91e3ea3f5b2170298461e68"
1671+dependencies = [
1672+ "bytes",
1673+ "prost",
1674+]
1675+
1676+[[package]]
1677+name = "quote"
1678+version = "1.0.47"
1679+source = "registry+https://github.com/rust-lang/crates.io-index"
1680+checksum = "1fbf4db142a473a8d80c26bbf18454ed458bf8d26c8219c331daecfdbd079001"
1681+dependencies = [
1682+ "proc-macro2",
1683+]
1684+
1685+[[package]]
1686+name = "r-efi"
1687+version = "6.0.0"
1688+source = "registry+https://github.com/rust-lang/crates.io-index"
1689+checksum = "f8dcc9c7d52a811697d2151c701e0d08956f92b0e24136cf4cf27b57a6a0d9bf"
1690+
1691+[[package]]
1692+name = "rand"
1693+version = "0.8.7"
1694+source = "registry+https://github.com/rust-lang/crates.io-index"
1695+checksum = "22f6172bdec972074665ed81ed53b71da00bfc44b65a753cfde883ec4c702a1a"
1696+dependencies = [
1697+ "libc",
1698+ "rand_chacha",
1699+ "rand_core",
1700+]
1701+
1702+[[package]]
1703+name = "rand_chacha"
1704+version = "0.3.1"
1705+source = "registry+https://github.com/rust-lang/crates.io-index"
1706+checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88"
1707+dependencies = [
1708+ "ppv-lite86",
1709+ "rand_core",
1710+]
1711+
1712+[[package]]
1713+name = "rand_core"
1714+version = "0.6.4"
1715+source = "registry+https://github.com/rust-lang/crates.io-index"
1716+checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c"
1717+dependencies = [
1718+ "getrandom 0.2.17",
1719+]
1720+
1721+[[package]]
1722+name = "redox_syscall"
1723+version = "0.5.18"
1724+source = "registry+https://github.com/rust-lang/crates.io-index"
1725+checksum = "ed2bf2547551a7053d6fdfafda3f938979645c44812fbfcda098faae3f1a362d"
1726+dependencies = [
1727+ "bitflags",
1728+]
1729+
1730+[[package]]
1731+name = "redox_syscall"
1732+version = "0.9.1"
1733+source = "registry+https://github.com/rust-lang/crates.io-index"
1734+checksum = "07507be7b4a5f9f26eeb41eeaebb1f5a7ff29dfb29739facc21d35bf8b11c21e"
1735+dependencies = [
1736+ "bitflags",
1737+]
1738+
1739+[[package]]
1740+name = "regex"
1741+version = "1.13.1"
1742+source = "registry+https://github.com/rust-lang/crates.io-index"
1743+checksum = "f020237b6c8eed93db2e2cb53c00c60a8e1bc73da7d073199a1180401450218d"
1744+dependencies = [
1745+ "aho-corasick",
1746+ "memchr",
1747+ "regex-automata",
1748+ "regex-syntax",
1749+]
1750+
1751+[[package]]
1752+name = "regex-automata"
1753+version = "0.4.18"
1754+source = "registry+https://github.com/rust-lang/crates.io-index"
1755+checksum = "ad8553b9b26413251cbf30e620595c7a41b3887f03da04579c0e6b0d6a06b4b2"
1756+dependencies = [
1757+ "aho-corasick",
1758+ "memchr",
1759+ "regex-syntax",
1760+]
1761+
1762+[[package]]
1763+name = "regex-syntax"
1764+version = "0.8.11"
1765+source = "registry+https://github.com/rust-lang/crates.io-index"
1766+checksum = "d6f6ff9a378485b298a5286656da665ba74413d36db0979633275d2e708145d4"
1767+
1768+[[package]]
1769+name = "rfc6979"
1770+version = "0.4.0"
1771+source = "registry+https://github.com/rust-lang/crates.io-index"
1772+checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2"
1773+dependencies = [
1774+ "hmac",
1775+ "subtle",
1776+]
1777+
1778+[[package]]
1779+name = "ring"
1780+version = "0.17.14"
1781+source = "registry+https://github.com/rust-lang/crates.io-index"
1782+checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7"
1783+dependencies = [
1784+ "cc",
1785+ "cfg-if",
1786+ "getrandom 0.2.17",
1787+ "libc",
1788+ "untrusted",
1789+ "windows-sys 0.52.0",
1790+]
1791+
1792+[[package]]
1793+name = "rpassword"
1794+version = "7.5.4"
1795+source = "registry+https://github.com/rust-lang/crates.io-index"
1796+checksum = "2da316a15f47e3d053de9cb2c439650bd8fa4aaeb9365f2e5f27f492ff73c196"
1797+dependencies = [
1798+ "libc",
1799+ "rtoolbox",
1800+ "windows-sys 0.61.2",
1801+]
1802+
1803+[[package]]
1804+name = "rsa"
1805+version = "0.9.10"
1806+source = "registry+https://github.com/rust-lang/crates.io-index"
1807+checksum = "b8573f03f5883dcaebdfcf4725caa1ecb9c15b2ef50c43a07b816e06799bb12d"
1808+dependencies = [
1809+ "const-oid",
1810+ "digest 0.10.7",
1811+ "num-bigint-dig",
1812+ "num-integer",
1813+ "num-traits",
1814+ "pkcs1",
1815+ "pkcs8 0.10.2",
1816+ "rand_core",
1817+ "signature",
1818+ "spki 0.7.3",
1819+ "subtle",
1820+ "zeroize",
1821+]
1822+
1823+[[package]]
1824+name = "rtoolbox"
1825+version = "0.0.5"
1826+source = "registry+https://github.com/rust-lang/crates.io-index"
1827+checksum = "50a0e551c1e27e1731aba276dbeaeac73f53c7cd34d1bda485d02bd1e0f36844"
1828+dependencies = [
1829+ "libc",
1830+ "windows-sys 0.59.0",
1831+]
1832+
1833+[[package]]
1834+name = "rustc-hash"
1835+version = "2.1.3"
1836+source = "registry+https://github.com/rust-lang/crates.io-index"
1837+checksum = "6b1e7f9a428571be2dc5bc0505c13fb6bf936822b894ec87abf8a08a4e51742d"
1838+
1839+[[package]]
1840+name = "rustc_version"
1841+version = "0.4.1"
1842+source = "registry+https://github.com/rust-lang/crates.io-index"
1843+checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
1844+dependencies = [
1845+ "semver",
1846+]
1847+
1848+[[package]]
1849+name = "rustls"
1850+version = "0.23.43"
1851+source = "registry+https://github.com/rust-lang/crates.io-index"
1852+checksum = "0283386ce02abc0151e1761d08802dfe86c173b0b494af5cbc086574e453da06"
1853+dependencies = [
1854+ "once_cell",
1855+ "ring",
1856+ "rustls-pki-types",
1857+ "rustls-webpki",
1858+ "subtle",
1859+ "zeroize",
1860+]
1861+
1862+[[package]]
1863+name = "rustls-pki-types"
1864+version = "1.15.1"
1865+source = "registry+https://github.com/rust-lang/crates.io-index"
1866+checksum = "2f4925028c7eb5d1fcdaf196971378ed9d2c1c4efc7dc5d011256f76c99c0a96"
1867+dependencies = [
1868+ "zeroize",
1869+]
1870+
1871+[[package]]
1872+name = "rustls-webpki"
1873+version = "0.103.13"
1874+source = "registry+https://github.com/rust-lang/crates.io-index"
1875+checksum = "61c429a8649f110dddef65e2a5ad240f747e85f7758a6bccc7e5777bd33f756e"
1876+dependencies = [
1877+ "ring",
1878+ "rustls-pki-types",
1879+ "untrusted",
1880+]
1881+
1882+[[package]]
1883+name = "rustversion"
1884+version = "1.0.23"
1885+source = "registry+https://github.com/rust-lang/crates.io-index"
1886+checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f"
1887+
1888+[[package]]
1889+name = "ryu"
1890+version = "1.0.23"
1891+source = "registry+https://github.com/rust-lang/crates.io-index"
1892+checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f"
1893+
1894+[[package]]
1895+name = "scopeguard"
1896+version = "1.2.0"
1897+source = "registry+https://github.com/rust-lang/crates.io-index"
1898+checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49"
1899+
1900+[[package]]
1901+name = "sec1"
1902+version = "0.7.3"
1903+source = "registry+https://github.com/rust-lang/crates.io-index"
1904+checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc"
1905+dependencies = [
1906+ "base16ct",
1907+ "der 0.7.10",
1908+ "generic-array",
1909+ "pkcs8 0.10.2",
1910+ "serdect",
1911+ "subtle",
1912+ "zeroize",
1913+]
1914+
1915+[[package]]
1916+name = "semver"
1917+version = "1.0.28"
1918+source = "registry+https://github.com/rust-lang/crates.io-index"
1919+checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
1920+
1921+[[package]]
1922+name = "serde"
1923+version = "1.0.229"
1924+source = "registry+https://github.com/rust-lang/crates.io-index"
1925+checksum = "4148590afebada386688f18773da617792bf2ef03ffc1e4cbd2b1d45b023e0ba"
1926+dependencies = [
1927+ "serde_core",
1928+ "serde_derive",
1929+]
1930+
1931+[[package]]
1932+name = "serde_core"
1933+version = "1.0.229"
1934+source = "registry+https://github.com/rust-lang/crates.io-index"
1935+checksum = "67dca2c9c51e58a4791a4b1ed58308b39c64224d349a935ab5039aa360942a48"
1936+dependencies = [
1937+ "serde_derive",
1938+]
1939+
1940+[[package]]
1941+name = "serde_derive"
1942+version = "1.0.229"
1943+source = "registry+https://github.com/rust-lang/crates.io-index"
1944+checksum = "e7a5d71263a5a7d47b41f6b3f06ba276f10cc18b0931f1799f710578e2309348"
1945+dependencies = [
1946+ "proc-macro2",
1947+ "quote",
1948+ "syn 3.0.3",
1949+]
1950+
1951+[[package]]
1952+name = "serde_json"
1953+version = "1.0.151"
1954+source = "registry+https://github.com/rust-lang/crates.io-index"
1955+checksum = "c841b55ecdae098c80dcae9cf767f6f8a0c2cdb3416bbef72181df4d0fe73f14"
1956+dependencies = [
1957+ "itoa",
1958+ "memchr",
1959+ "serde",
1960+ "serde_core",
1961+ "zmij",
1962+]
1963+
1964+[[package]]
1965+name = "serde_path_to_error"
1966+version = "0.1.20"
1967+source = "registry+https://github.com/rust-lang/crates.io-index"
1968+checksum = "10a9ff822e371bb5403e391ecd83e182e0e77ba7f6fe0160b795797109d1b457"
1969+dependencies = [
1970+ "itoa",
1971+ "serde",
1972+ "serde_core",
1973+]
1974+
1975+[[package]]
1976+name = "serde_urlencoded"
1977+version = "0.7.1"
1978+source = "registry+https://github.com/rust-lang/crates.io-index"
1979+checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd"
1980+dependencies = [
1981+ "form_urlencoded",
1982+ "itoa",
1983+ "ryu",
1984+ "serde",
1985+]
1986+
1987+[[package]]
1988+name = "serdect"
1989+version = "0.2.0"
1990+source = "registry+https://github.com/rust-lang/crates.io-index"
1991+checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177"
1992+dependencies = [
1993+ "base16ct",
1994+ "serde",
1995+]
1996+
1997+[[package]]
1998+name = "sha1"
1999+version = "0.10.7"
2000+source = "registry+https://github.com/rust-lang/crates.io-index"
2001+checksum = "a978451301f4db1d02937a4ab3ccce137717b81826e79b7d49ffe3244a13c3b8"
2002+dependencies = [
2003+ "cfg-if",
2004+ "cpufeatures",
2005+ "digest 0.10.7",
2006+]
2007+
2008+[[package]]
2009+name = "sha2"
2010+version = "0.9.9"
2011+source = "registry+https://github.com/rust-lang/crates.io-index"
2012+checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800"
2013+dependencies = [
2014+ "block-buffer 0.9.0",
2015+ "cfg-if",
2016+ "cpufeatures",
2017+ "digest 0.9.0",
2018+ "opaque-debug",
2019+]
2020+
2021+[[package]]
2022+name = "sha2"
2023+version = "0.10.9"
2024+source = "registry+https://github.com/rust-lang/crates.io-index"
2025+checksum = "a7507d819769d01a365ab707794a4084392c824f54a7a6a7862f8c3d0892b283"
2026+dependencies = [
2027+ "cfg-if",
2028+ "cpufeatures",
2029+ "digest 0.10.7",
2030+]
2031+
2032+[[package]]
2033+name = "sharded-slab"
2034+version = "0.1.7"
2035+source = "registry+https://github.com/rust-lang/crates.io-index"
2036+checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6"
2037+dependencies = [
2038+ "lazy_static",
2039+]
2040+
2041+[[package]]
2042+name = "shlex"
2043+version = "2.0.1"
2044+source = "registry+https://github.com/rust-lang/crates.io-index"
2045+checksum = "f8fadd59c855ef2080decdef8ff161eb6661b86933c9d82e5ba29dc602a55aba"
2046+
2047+[[package]]
2048+name = "signal-hook-registry"
2049+version = "1.4.8"
2050+source = "registry+https://github.com/rust-lang/crates.io-index"
2051+checksum = "c4db69cba1110affc0e9f7bcd48bbf87b3f4fc7c61fc9155afd4c469eb3d6c1b"
2052+dependencies = [
2053+ "errno",
2054+ "libc",
2055+]
2056+
2057+[[package]]
2058+name = "signature"
2059+version = "2.2.0"
2060+source = "registry+https://github.com/rust-lang/crates.io-index"
2061+checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de"
2062+dependencies = [
2063+ "digest 0.10.7",
2064+ "rand_core",
2065+]
2066+
2067+[[package]]
2068+name = "siphasher"
2069+version = "1.0.3"
2070+source = "registry+https://github.com/rust-lang/crates.io-index"
2071+checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649"
2072+
2073+[[package]]
2074+name = "slab"
2075+version = "0.4.12"
2076+source = "registry+https://github.com/rust-lang/crates.io-index"
2077+checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
2078+
2079+[[package]]
2080+name = "smallvec"
2081+version = "1.15.2"
2082+source = "registry+https://github.com/rust-lang/crates.io-index"
2083+checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90"
2084+dependencies = [
2085+ "serde",
2086+]
2087+
2088+[[package]]
2089+name = "socket2"
2090+version = "0.6.5"
2091+source = "registry+https://github.com/rust-lang/crates.io-index"
2092+checksum = "c3d1e2c7f27f8d4cb10542a02c49005dbd6e93095799d6f3be745fae9f8fedd4"
2093+dependencies = [
2094+ "libc",
2095+ "windows-sys 0.61.2",
2096+]
2097+
2098+[[package]]
2099+name = "spin"
2100+version = "0.9.9"
2101+source = "registry+https://github.com/rust-lang/crates.io-index"
2102+checksum = "3763264f6b73151db08c50ff20d7d8a0b8796e021cdea7ceedad07b80155fa0e"
2103+dependencies = [
2104+ "lock_api",
2105+]
2106+
2107+[[package]]
2108+name = "spki"
2109+version = "0.6.0"
2110+source = "registry+https://github.com/rust-lang/crates.io-index"
2111+checksum = "67cf02bbac7a337dc36e4f5a693db6c21e7863f45070f7064577eb4367a3212b"
2112+dependencies = [
2113+ "der 0.6.1",
2114+]
2115+
2116+[[package]]
2117+name = "spki"
2118+version = "0.7.3"
2119+source = "registry+https://github.com/rust-lang/crates.io-index"
2120+checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d"
2121+dependencies = [
2122+ "base64ct",
2123+ "der 0.7.10",
2124+]
2125+
2126+[[package]]
2127+name = "sqlx"
2128+version = "0.8.6"
2129+source = "registry+https://github.com/rust-lang/crates.io-index"
2130+checksum = "1fefb893899429669dcdd979aff487bd78f4064e5e7907e4269081e0ef7d97dc"
2131+dependencies = [
2132+ "sqlx-core",
2133+ "sqlx-macros",
2134+ "sqlx-mysql",
2135+ "sqlx-postgres",
2136+ "sqlx-sqlite",
2137+]
2138+
2139+[[package]]
2140+name = "sqlx-core"
2141+version = "0.8.6"
2142+source = "registry+https://github.com/rust-lang/crates.io-index"
2143+checksum = "ee6798b1838b6a0f69c007c133b8df5866302197e404e8b6ee8ed3e3a5e68dc6"
2144+dependencies = [
2145+ "base64 0.22.1",
2146+ "bytes",
2147+ "chrono",
2148+ "crc",
2149+ "crossbeam-queue",
2150+ "either",
2151+ "event-listener",
2152+ "futures-core",
2153+ "futures-intrusive",
2154+ "futures-io",
2155+ "futures-util",
2156+ "hashbrown 0.15.5",
2157+ "hashlink",
2158+ "indexmap",
2159+ "log",
2160+ "memchr",
2161+ "once_cell",
2162+ "percent-encoding",
2163+ "rustls",
2164+ "serde",
2165+ "serde_json",
2166+ "sha2 0.10.9",
2167+ "smallvec",
2168+ "thiserror 2.0.19",
2169+ "tokio",
2170+ "tokio-stream",
2171+ "tracing",
2172+ "url",
2173+ "uuid",
2174+ "webpki-roots 0.26.11",
2175+]
2176+
2177+[[package]]
2178+name = "sqlx-macros"
2179+version = "0.8.6"
2180+source = "registry+https://github.com/rust-lang/crates.io-index"
2181+checksum = "a2d452988ccaacfbf5e0bdbc348fb91d7c8af5bee192173ac3636b5fb6e6715d"
2182+dependencies = [
2183+ "proc-macro2",
2184+ "quote",
2185+ "sqlx-core",
2186+ "sqlx-macros-core",
2187+ "syn 2.0.119",
2188+]
2189+
2190+[[package]]
2191+name = "sqlx-macros-core"
2192+version = "0.8.6"
2193+source = "registry+https://github.com/rust-lang/crates.io-index"
2194+checksum = "19a9c1841124ac5a61741f96e1d9e2ec77424bf323962dd894bdb93f37d5219b"
2195+dependencies = [
2196+ "dotenvy",
2197+ "either",
2198+ "heck",
2199+ "hex",
2200+ "once_cell",
2201+ "proc-macro2",
2202+ "quote",
2203+ "serde",
2204+ "serde_json",
2205+ "sha2 0.10.9",
2206+ "sqlx-core",
2207+ "sqlx-mysql",
2208+ "sqlx-postgres",
2209+ "sqlx-sqlite",
2210+ "syn 2.0.119",
2211+ "tokio",
2212+ "url",
2213+]
2214+
2215+[[package]]
2216+name = "sqlx-mysql"
2217+version = "0.8.6"
2218+source = "registry+https://github.com/rust-lang/crates.io-index"
2219+checksum = "aa003f0038df784eb8fecbbac13affe3da23b45194bd57dba231c8f48199c526"
2220+dependencies = [
2221+ "atoi",
2222+ "base64 0.22.1",
2223+ "bitflags",
2224+ "byteorder",
2225+ "bytes",
2226+ "chrono",
2227+ "crc",
2228+ "digest 0.10.7",
2229+ "dotenvy",
2230+ "either",
2231+ "futures-channel",
2232+ "futures-core",
2233+ "futures-io",
2234+ "futures-util",
2235+ "generic-array",
2236+ "hex",
2237+ "hkdf",
2238+ "hmac",
2239+ "itoa",
2240+ "log",
2241+ "md-5",
2242+ "memchr",
2243+ "once_cell",
2244+ "percent-encoding",
2245+ "rand",
2246+ "rsa",
2247+ "serde",
2248+ "sha1",
2249+ "sha2 0.10.9",
2250+ "smallvec",
2251+ "sqlx-core",
2252+ "stringprep",
2253+ "thiserror 2.0.19",
2254+ "tracing",
2255+ "uuid",
2256+ "whoami",
2257+]
2258+
2259+[[package]]
2260+name = "sqlx-postgres"
2261+version = "0.8.6"
2262+source = "registry+https://github.com/rust-lang/crates.io-index"
2263+checksum = "db58fcd5a53cf07c184b154801ff91347e4c30d17a3562a635ff028ad5deda46"
2264+dependencies = [
2265+ "atoi",
2266+ "base64 0.22.1",
2267+ "bitflags",
2268+ "byteorder",
2269+ "chrono",
2270+ "crc",
2271+ "dotenvy",
2272+ "etcetera",
2273+ "futures-channel",
2274+ "futures-core",
2275+ "futures-util",
2276+ "hex",
2277+ "hkdf",
2278+ "hmac",
2279+ "home",
2280+ "itoa",
2281+ "log",
2282+ "md-5",
2283+ "memchr",
2284+ "once_cell",
2285+ "rand",
2286+ "serde",
2287+ "serde_json",
2288+ "sha2 0.10.9",
2289+ "smallvec",
2290+ "sqlx-core",
2291+ "stringprep",
2292+ "thiserror 2.0.19",
2293+ "tracing",
2294+ "uuid",
2295+ "whoami",
2296+]
2297+
2298+[[package]]
2299+name = "sqlx-sqlite"
2300+version = "0.8.6"
2301+source = "registry+https://github.com/rust-lang/crates.io-index"
2302+checksum = "c2d12fe70b2c1b4401038055f90f151b78208de1f9f89a7dbfd41587a10c3eea"
2303+dependencies = [
2304+ "atoi",
2305+ "chrono",
2306+ "flume",
2307+ "futures-channel",
2308+ "futures-core",
2309+ "futures-executor",
2310+ "futures-intrusive",
2311+ "futures-util",
2312+ "libsqlite3-sys",
2313+ "log",
2314+ "percent-encoding",
2315+ "serde",
2316+ "serde_urlencoded",
2317+ "sqlx-core",
2318+ "thiserror 2.0.19",
2319+ "tracing",
2320+ "url",
2321+ "uuid",
2322+]
2323+
2324+[[package]]
2325+name = "stable_deref_trait"
2326+version = "1.2.1"
2327+source = "registry+https://github.com/rust-lang/crates.io-index"
2328+checksum = "6ce2be8dc25455e1f91df71bfa12ad37d7af1092ae736f3a6cd0e37bc7810596"
2329+
2330+[[package]]
2331+name = "stringprep"
2332+version = "0.1.5"
2333+source = "registry+https://github.com/rust-lang/crates.io-index"
2334+checksum = "7b4df3d392d81bd458a8a621b8bffbd2302a12ffe288a9d931670948749463b1"
2335+dependencies = [
2336+ "unicode-bidi",
2337+ "unicode-normalization",
2338+ "unicode-properties",
2339+]
2340+
2341+[[package]]
2342+name = "strsim"
2343+version = "0.11.1"
2344+source = "registry+https://github.com/rust-lang/crates.io-index"
2345+checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f"
2346+
2347+[[package]]
2348+name = "subtle"
2349+version = "2.6.1"
2350+source = "registry+https://github.com/rust-lang/crates.io-index"
2351+checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292"
2352+
2353+[[package]]
2354+name = "syn"
2355+version = "1.0.109"
2356+source = "registry+https://github.com/rust-lang/crates.io-index"
2357+checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237"
2358+dependencies = [
2359+ "proc-macro2",
2360+ "quote",
2361+ "unicode-ident",
2362+]
2363+
2364+[[package]]
2365+name = "syn"
2366+version = "2.0.119"
2367+source = "registry+https://github.com/rust-lang/crates.io-index"
2368+checksum = "872831b642d1a07999a962a351ed35b955ea2cfc8f3862091e2a240a84f17297"
2369+dependencies = [
2370+ "proc-macro2",
2371+ "quote",
2372+ "unicode-ident",
2373+]
2374+
2375+[[package]]
2376+name = "syn"
2377+version = "3.0.3"
2378+source = "registry+https://github.com/rust-lang/crates.io-index"
2379+checksum = "53e9bae58849f64dfa4f5d5ae372c8341f7305f82a3868709269343628b659a3"
2380+dependencies = [
2381+ "proc-macro2",
2382+ "quote",
2383+ "unicode-ident",
2384+]
2385+
2386+[[package]]
2387+name = "sync_wrapper"
2388+version = "1.0.2"
2389+source = "registry+https://github.com/rust-lang/crates.io-index"
2390+checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263"
2391+
2392+[[package]]
2393+name = "synstructure"
2394+version = "0.13.2"
2395+source = "registry+https://github.com/rust-lang/crates.io-index"
2396+checksum = "728a70f3dbaf5bab7f0c4b1ac8d7ae5ea60a4b5549c8a5914361c99147a709d2"
2397+dependencies = [
2398+ "proc-macro2",
2399+ "quote",
2400+ "syn 2.0.119",
2401+]
2402+
2403+[[package]]
2404+name = "thiserror"
2405+version = "1.0.69"
2406+source = "registry+https://github.com/rust-lang/crates.io-index"
2407+checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52"
2408+dependencies = [
2409+ "thiserror-impl 1.0.69",
2410+]
2411+
2412+[[package]]
2413+name = "thiserror"
2414+version = "2.0.19"
2415+source = "registry+https://github.com/rust-lang/crates.io-index"
2416+checksum = "09a43598840e33d5b0331f38c5e30d13bb11c11210a4b58f0d9b18a5a5eefcd9"
2417+dependencies = [
2418+ "thiserror-impl 2.0.19",
2419+]
2420+
2421+[[package]]
2422+name = "thiserror-impl"
2423+version = "1.0.69"
2424+source = "registry+https://github.com/rust-lang/crates.io-index"
2425+checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
2426+dependencies = [
2427+ "proc-macro2",
2428+ "quote",
2429+ "syn 2.0.119",
2430+]
2431+
2432+[[package]]
2433+name = "thiserror-impl"
2434+version = "2.0.19"
2435+source = "registry+https://github.com/rust-lang/crates.io-index"
2436+checksum = "43cbfe0cf76104d42a574802844187e84a305e531ed54455f11fbde0f10541cd"
2437+dependencies = [
2438+ "proc-macro2",
2439+ "quote",
2440+ "syn 3.0.3",
2441+]
2442+
2443+[[package]]
2444+name = "thread_local"
2445+version = "1.1.10"
2446+source = "registry+https://github.com/rust-lang/crates.io-index"
2447+checksum = "1ad99c4c6d32803332c548b1af0540b357b3f5fc0be8f6c6bfe8b2e6ae784070"
2448+dependencies = [
2449+ "cfg-if",
2450+]
2451+
2452+[[package]]
2453+name = "time"
2454+version = "0.3.55"
2455+source = "registry+https://github.com/rust-lang/crates.io-index"
2456+checksum = "cdb87b95ec50ddfa440816d227a17b2ccbdda963a316a727fda0fc4334f7d134"
2457+dependencies = [
2458+ "deranged",
2459+ "num-conv",
2460+ "powerfmt",
2461+ "serde_core",
2462+ "time-core",
2463+ "time-macros",
2464+]
2465+
2466+[[package]]
2467+name = "time-core"
2468+version = "0.1.9"
2469+source = "registry+https://github.com/rust-lang/crates.io-index"
2470+checksum = "9e1c906769ad99c88eaa54e728060edef082f8e358ff32030cb7c7d315e81109"
2471+
2472+[[package]]
2473+name = "time-macros"
2474+version = "0.2.32"
2475+source = "registry+https://github.com/rust-lang/crates.io-index"
2476+checksum = "7e689342a48d2ea927c87ea50cabf8594854bf940e9310208848d680d668ed85"
2477+dependencies = [
2478+ "num-conv",
2479+ "time-core",
2480+]
2481+
2482+[[package]]
2483+name = "time-time"
2484+version = "0.1.0"
2485+dependencies = [
2486+ "anyhow",
2487+ "argon2",
2488+ "askama",
2489+ "axum",
2490+ "axum-extra",
2491+ "biscuit-auth",
2492+ "chrono",
2493+ "chrono-tz",
2494+ "clap",
2495+ "hex",
2496+ "hmac",
2497+ "rpassword",
2498+ "serde",
2499+ "sha2 0.10.9",
2500+ "sqlx",
2501+ "subtle",
2502+ "thiserror 2.0.19",
2503+ "time",
2504+ "tokio",
2505+ "tracing",
2506+ "tracing-subscriber",
2507+ "uuid",
2508+]
2509+
2510+[[package]]
2511+name = "tinystr"
2512+version = "0.8.3"
2513+source = "registry+https://github.com/rust-lang/crates.io-index"
2514+checksum = "c8323304221c2a851516f22236c5722a72eaa19749016521d6dff0824447d96d"
2515+dependencies = [
2516+ "displaydoc",
2517+ "zerovec",
2518+]
2519+
2520+[[package]]
2521+name = "tinyvec"
2522+version = "1.12.0"
2523+source = "registry+https://github.com/rust-lang/crates.io-index"
2524+checksum = "bb4ebadaa0af04fab11ae01eb5f9fdb5f9c5b875506e210e71c07873528baa7f"
2525+dependencies = [
2526+ "tinyvec_macros",
2527+]
2528+
2529+[[package]]
2530+name = "tinyvec_macros"
2531+version = "0.1.1"
2532+source = "registry+https://github.com/rust-lang/crates.io-index"
2533+checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
2534+
2535+[[package]]
2536+name = "tokio"
2537+version = "1.53.1"
2538+source = "registry+https://github.com/rust-lang/crates.io-index"
2539+checksum = "202caea871b69668250d242070849eb495be178ed697a3e98aebce5bc81a0bed"
2540+dependencies = [
2541+ "bytes",
2542+ "libc",
2543+ "mio",
2544+ "pin-project-lite",
2545+ "signal-hook-registry",
2546+ "socket2",
2547+ "tokio-macros",
2548+ "windows-sys 0.61.2",
2549+]
2550+
2551+[[package]]
2552+name = "tokio-macros"
2553+version = "2.7.2"
2554+source = "registry+https://github.com/rust-lang/crates.io-index"
2555+checksum = "78773a2a397f451582ce068015985c33193cf6dea8b74d2a639fe457b2f07b0e"
2556+dependencies = [
2557+ "proc-macro2",
2558+ "quote",
2559+ "syn 3.0.3",
2560+]
2561+
2562+[[package]]
2563+name = "tokio-stream"
2564+version = "0.1.19"
2565+source = "registry+https://github.com/rust-lang/crates.io-index"
2566+checksum = "a3d06f0b082ba57c26b79407372e57cf2a1e28124f78e9479fe80322cf53420b"
2567+dependencies = [
2568+ "futures-core",
2569+ "pin-project-lite",
2570+ "tokio",
2571+]
2572+
2573+[[package]]
2574+name = "tower"
2575+version = "0.5.3"
2576+source = "registry+https://github.com/rust-lang/crates.io-index"
2577+checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4"
2578+dependencies = [
2579+ "futures-core",
2580+ "futures-util",
2581+ "pin-project-lite",
2582+ "sync_wrapper",
2583+ "tokio",
2584+ "tower-layer",
2585+ "tower-service",
2586+ "tracing",
2587+]
2588+
2589+[[package]]
2590+name = "tower-layer"
2591+version = "0.3.3"
2592+source = "registry+https://github.com/rust-lang/crates.io-index"
2593+checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
2594+
2595+[[package]]
2596+name = "tower-service"
2597+version = "0.3.3"
2598+source = "registry+https://github.com/rust-lang/crates.io-index"
2599+checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
2600+
2601+[[package]]
2602+name = "tracing"
2603+version = "0.1.44"
2604+source = "registry+https://github.com/rust-lang/crates.io-index"
2605+checksum = "63e71662fa4b2a2c3a26f570f037eb95bb1f85397f3cd8076caed2f026a6d100"
2606+dependencies = [
2607+ "log",
2608+ "pin-project-lite",
2609+ "tracing-attributes",
2610+ "tracing-core",
2611+]
2612+
2613+[[package]]
2614+name = "tracing-attributes"
2615+version = "0.1.31"
2616+source = "registry+https://github.com/rust-lang/crates.io-index"
2617+checksum = "7490cfa5ec963746568740651ac6781f701c9c5ea257c58e057f3ba8cf69e8da"
2618+dependencies = [
2619+ "proc-macro2",
2620+ "quote",
2621+ "syn 2.0.119",
2622+]
2623+
2624+[[package]]
2625+name = "tracing-core"
2626+version = "0.1.36"
2627+source = "registry+https://github.com/rust-lang/crates.io-index"
2628+checksum = "db97caf9d906fbde555dd62fa95ddba9eecfd14cb388e4f491a66d74cd5fb79a"
2629+dependencies = [
2630+ "once_cell",
2631+ "valuable",
2632+]
2633+
2634+[[package]]
2635+name = "tracing-log"
2636+version = "0.2.0"
2637+source = "registry+https://github.com/rust-lang/crates.io-index"
2638+checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3"
2639+dependencies = [
2640+ "log",
2641+ "once_cell",
2642+ "tracing-core",
2643+]
2644+
2645+[[package]]
2646+name = "tracing-subscriber"
2647+version = "0.3.23"
2648+source = "registry+https://github.com/rust-lang/crates.io-index"
2649+checksum = "cb7f578e5945fb242538965c2d0b04418d38ec25c79d160cd279bf0731c8d319"
2650+dependencies = [
2651+ "matchers",
2652+ "nu-ansi-term",
2653+ "once_cell",
2654+ "regex-automata",
2655+ "sharded-slab",
2656+ "smallvec",
2657+ "thread_local",
2658+ "tracing",
2659+ "tracing-core",
2660+ "tracing-log",
2661+]
2662+
2663+[[package]]
2664+name = "typenum"
2665+version = "1.20.1"
2666+source = "registry+https://github.com/rust-lang/crates.io-index"
2667+checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20"
2668+
2669+[[package]]
2670+name = "unicode-bidi"
2671+version = "0.3.18"
2672+source = "registry+https://github.com/rust-lang/crates.io-index"
2673+checksum = "5c1cb5db39152898a79168971543b1cb5020dff7fe43c8dc468b0885f5e29df5"
2674+
2675+[[package]]
2676+name = "unicode-ident"
2677+version = "1.0.24"
2678+source = "registry+https://github.com/rust-lang/crates.io-index"
2679+checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
2680+
2681+[[package]]
2682+name = "unicode-normalization"
2683+version = "0.1.25"
2684+source = "registry+https://github.com/rust-lang/crates.io-index"
2685+checksum = "5fd4f6878c9cb28d874b009da9e8d183b5abc80117c40bbd187a1fde336be6e8"
2686+dependencies = [
2687+ "tinyvec",
2688+]
2689+
2690+[[package]]
2691+name = "unicode-properties"
2692+version = "0.1.4"
2693+source = "registry+https://github.com/rust-lang/crates.io-index"
2694+checksum = "7df058c713841ad818f1dc5d3fd88063241cc61f49f5fbea4b951e8cf5a8d71d"
2695+
2696+[[package]]
2697+name = "untrusted"
2698+version = "0.9.0"
2699+source = "registry+https://github.com/rust-lang/crates.io-index"
2700+checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1"
2701+
2702+[[package]]
2703+name = "url"
2704+version = "2.5.8"
2705+source = "registry+https://github.com/rust-lang/crates.io-index"
2706+checksum = "ff67a8a4397373c3ef660812acab3268222035010ab8680ec4215f38ba3d0eed"
2707+dependencies = [
2708+ "form_urlencoded",
2709+ "idna",
2710+ "percent-encoding",
2711+ "serde",
2712+]
2713+
2714+[[package]]
2715+name = "utf8_iter"
2716+version = "1.0.4"
2717+source = "registry+https://github.com/rust-lang/crates.io-index"
2718+checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be"
2719+
2720+[[package]]
2721+name = "utf8parse"
2722+version = "0.2.2"
2723+source = "registry+https://github.com/rust-lang/crates.io-index"
2724+checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821"
2725+
2726+[[package]]
2727+name = "uuid"
2728+version = "1.24.0"
2729+source = "registry+https://github.com/rust-lang/crates.io-index"
2730+checksum = "bf3923a6f5c4c6382e0b653c4117f48d631ea17f38ed86e2a828e6f7412f5239"
2731+dependencies = [
2732+ "getrandom 0.4.3",
2733+ "js-sys",
2734+ "serde_core",
2735+ "wasm-bindgen",
2736+]
2737+
2738+[[package]]
2739+name = "valuable"
2740+version = "0.1.1"
2741+source = "registry+https://github.com/rust-lang/crates.io-index"
2742+checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
2743+
2744+[[package]]
2745+name = "vcpkg"
2746+version = "0.2.15"
2747+source = "registry+https://github.com/rust-lang/crates.io-index"
2748+checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426"
2749+
2750+[[package]]
2751+name = "version_check"
2752+version = "0.9.5"
2753+source = "registry+https://github.com/rust-lang/crates.io-index"
2754+checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
2755+
2756+[[package]]
2757+name = "wasi"
2758+version = "0.11.1+wasi-snapshot-preview1"
2759+source = "registry+https://github.com/rust-lang/crates.io-index"
2760+checksum = "ccf3ec651a847eb01de73ccad15eb7d99f80485de043efb2f370cd654f4ea44b"
2761+
2762+[[package]]
2763+name = "wasite"
2764+version = "0.1.0"
2765+source = "registry+https://github.com/rust-lang/crates.io-index"
2766+checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b"
2767+
2768+[[package]]
2769+name = "wasm-bindgen"
2770+version = "0.2.126"
2771+source = "registry+https://github.com/rust-lang/crates.io-index"
2772+checksum = "4b067c0c11094aef6b7a801c1e34a26affafdf3d051dba08456b868789aaf9a4"
2773+dependencies = [
2774+ "cfg-if",
2775+ "once_cell",
2776+ "rustversion",
2777+ "wasm-bindgen-macro",
2778+ "wasm-bindgen-shared",
2779+]
2780+
2781+[[package]]
2782+name = "wasm-bindgen-macro"
2783+version = "0.2.126"
2784+source = "registry+https://github.com/rust-lang/crates.io-index"
2785+checksum = "167ce5e579f6bcf889c4f7175a8a5a585de84e8ff93976ce393efa5f2837aab1"
2786+dependencies = [
2787+ "quote",
2788+ "wasm-bindgen-macro-support",
2789+]
2790+
2791+[[package]]
2792+name = "wasm-bindgen-macro-support"
2793+version = "0.2.126"
2794+source = "registry+https://github.com/rust-lang/crates.io-index"
2795+checksum = "f3997c7839262f4ef12cf90b818d6340c18e80f263f1a94bf157d0ec4420380e"
2796+dependencies = [
2797+ "bumpalo",
2798+ "proc-macro2",
2799+ "quote",
2800+ "syn 2.0.119",
2801+ "wasm-bindgen-shared",
2802+]
2803+
2804+[[package]]
2805+name = "wasm-bindgen-shared"
2806+version = "0.2.126"
2807+source = "registry+https://github.com/rust-lang/crates.io-index"
2808+checksum = "dc1b4cb0cc549fcf58d7dfc081778139b3d283a081644e833e84682ad71cea24"
2809+dependencies = [
2810+ "unicode-ident",
2811+]
2812+
2813+[[package]]
2814+name = "webpki-roots"
2815+version = "0.26.11"
2816+source = "registry+https://github.com/rust-lang/crates.io-index"
2817+checksum = "521bc38abb08001b01866da9f51eb7c5d647a19260e00054a8c7fd5f9e57f7a9"
2818+dependencies = [
2819+ "webpki-roots 1.0.9",
2820+]
2821+
2822+[[package]]
2823+name = "webpki-roots"
2824+version = "1.0.9"
2825+source = "registry+https://github.com/rust-lang/crates.io-index"
2826+checksum = "7dcd9d09a39985f5344844e66b0c530a33843579125f23e21e9f0f220850f22a"
2827+dependencies = [
2828+ "rustls-pki-types",
2829+]
2830+
2831+[[package]]
2832+name = "whoami"
2833+version = "1.6.1"
2834+source = "registry+https://github.com/rust-lang/crates.io-index"
2835+checksum = "5d4a4db5077702ca3015d3d02d74974948aba2ad9e12ab7df718ee64ccd7e97d"
2836+dependencies = [
2837+ "libredox",
2838+ "wasite",
2839+]
2840+
2841+[[package]]
2842+name = "windows-core"
2843+version = "0.62.2"
2844+source = "registry+https://github.com/rust-lang/crates.io-index"
2845+checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb"
2846+dependencies = [
2847+ "windows-implement",
2848+ "windows-interface",
2849+ "windows-link",
2850+ "windows-result",
2851+ "windows-strings",
2852+]
2853+
2854+[[package]]
2855+name = "windows-implement"
2856+version = "0.60.2"
2857+source = "registry+https://github.com/rust-lang/crates.io-index"
2858+checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf"
2859+dependencies = [
2860+ "proc-macro2",
2861+ "quote",
2862+ "syn 2.0.119",
2863+]
2864+
2865+[[package]]
2866+name = "windows-interface"
2867+version = "0.59.3"
2868+source = "registry+https://github.com/rust-lang/crates.io-index"
2869+checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358"
2870+dependencies = [
2871+ "proc-macro2",
2872+ "quote",
2873+ "syn 2.0.119",
2874+]
2875+
2876+[[package]]
2877+name = "windows-link"
2878+version = "0.2.1"
2879+source = "registry+https://github.com/rust-lang/crates.io-index"
2880+checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
2881+
2882+[[package]]
2883+name = "windows-result"
2884+version = "0.4.1"
2885+source = "registry+https://github.com/rust-lang/crates.io-index"
2886+checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5"
2887+dependencies = [
2888+ "windows-link",
2889+]
2890+
2891+[[package]]
2892+name = "windows-strings"
2893+version = "0.5.1"
2894+source = "registry+https://github.com/rust-lang/crates.io-index"
2895+checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091"
2896+dependencies = [
2897+ "windows-link",
2898+]
2899+
2900+[[package]]
2901+name = "windows-sys"
2902+version = "0.48.0"
2903+source = "registry+https://github.com/rust-lang/crates.io-index"
2904+checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9"
2905+dependencies = [
2906+ "windows-targets 0.48.5",
2907+]
2908+
2909+[[package]]
2910+name = "windows-sys"
2911+version = "0.52.0"
2912+source = "registry+https://github.com/rust-lang/crates.io-index"
2913+checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d"
2914+dependencies = [
2915+ "windows-targets 0.52.6",
2916+]
2917+
2918+[[package]]
2919+name = "windows-sys"
2920+version = "0.59.0"
2921+source = "registry+https://github.com/rust-lang/crates.io-index"
2922+checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b"
2923+dependencies = [
2924+ "windows-targets 0.52.6",
2925+]
2926+
2927+[[package]]
2928+name = "windows-sys"
2929+version = "0.61.2"
2930+source = "registry+https://github.com/rust-lang/crates.io-index"
2931+checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
2932+dependencies = [
2933+ "windows-link",
2934+]
2935+
2936+[[package]]
2937+name = "windows-targets"
2938+version = "0.48.5"
2939+source = "registry+https://github.com/rust-lang/crates.io-index"
2940+checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c"
2941+dependencies = [
2942+ "windows_aarch64_gnullvm 0.48.5",
2943+ "windows_aarch64_msvc 0.48.5",
2944+ "windows_i686_gnu 0.48.5",
2945+ "windows_i686_msvc 0.48.5",
2946+ "windows_x86_64_gnu 0.48.5",
2947+ "windows_x86_64_gnullvm 0.48.5",
2948+ "windows_x86_64_msvc 0.48.5",
2949+]
2950+
2951+[[package]]
2952+name = "windows-targets"
2953+version = "0.52.6"
2954+source = "registry+https://github.com/rust-lang/crates.io-index"
2955+checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973"
2956+dependencies = [
2957+ "windows_aarch64_gnullvm 0.52.6",
2958+ "windows_aarch64_msvc 0.52.6",
2959+ "windows_i686_gnu 0.52.6",
2960+ "windows_i686_gnullvm",
2961+ "windows_i686_msvc 0.52.6",
2962+ "windows_x86_64_gnu 0.52.6",
2963+ "windows_x86_64_gnullvm 0.52.6",
2964+ "windows_x86_64_msvc 0.52.6",
2965+]
2966+
2967+[[package]]
2968+name = "windows_aarch64_gnullvm"
2969+version = "0.48.5"
2970+source = "registry+https://github.com/rust-lang/crates.io-index"
2971+checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8"
2972+
2973+[[package]]
2974+name = "windows_aarch64_gnullvm"
2975+version = "0.52.6"
2976+source = "registry+https://github.com/rust-lang/crates.io-index"
2977+checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3"
2978+
2979+[[package]]
2980+name = "windows_aarch64_msvc"
2981+version = "0.48.5"
2982+source = "registry+https://github.com/rust-lang/crates.io-index"
2983+checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc"
2984+
2985+[[package]]
2986+name = "windows_aarch64_msvc"
2987+version = "0.52.6"
2988+source = "registry+https://github.com/rust-lang/crates.io-index"
2989+checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469"
2990+
2991+[[package]]
2992+name = "windows_i686_gnu"
2993+version = "0.48.5"
2994+source = "registry+https://github.com/rust-lang/crates.io-index"
2995+checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e"
2996+
2997+[[package]]
2998+name = "windows_i686_gnu"
2999+version = "0.52.6"
3000+source = "registry+https://github.com/rust-lang/crates.io-index"
3001+checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b"
3002+
3003+[[package]]
3004+name = "windows_i686_gnullvm"
3005+version = "0.52.6"
3006+source = "registry+https://github.com/rust-lang/crates.io-index"
3007+checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66"
3008+
3009+[[package]]
3010+name = "windows_i686_msvc"
3011+version = "0.48.5"
3012+source = "registry+https://github.com/rust-lang/crates.io-index"
3013+checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406"
3014+
3015+[[package]]
3016+name = "windows_i686_msvc"
3017+version = "0.52.6"
3018+source = "registry+https://github.com/rust-lang/crates.io-index"
3019+checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66"
3020+
3021+[[package]]
3022+name = "windows_x86_64_gnu"
3023+version = "0.48.5"
3024+source = "registry+https://github.com/rust-lang/crates.io-index"
3025+checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e"
3026+
3027+[[package]]
3028+name = "windows_x86_64_gnu"
3029+version = "0.52.6"
3030+source = "registry+https://github.com/rust-lang/crates.io-index"
3031+checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78"
3032+
3033+[[package]]
3034+name = "windows_x86_64_gnullvm"
3035+version = "0.48.5"
3036+source = "registry+https://github.com/rust-lang/crates.io-index"
3037+checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc"
3038+
3039+[[package]]
3040+name = "windows_x86_64_gnullvm"
3041+version = "0.52.6"
3042+source = "registry+https://github.com/rust-lang/crates.io-index"
3043+checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d"
3044+
3045+[[package]]
3046+name = "windows_x86_64_msvc"
3047+version = "0.48.5"
3048+source = "registry+https://github.com/rust-lang/crates.io-index"
3049+checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538"
3050+
3051+[[package]]
3052+name = "windows_x86_64_msvc"
3053+version = "0.52.6"
3054+source = "registry+https://github.com/rust-lang/crates.io-index"
3055+checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
3056+
3057+[[package]]
3058+name = "winnow"
3059+version = "1.0.4"
3060+source = "registry+https://github.com/rust-lang/crates.io-index"
3061+checksum = "23b97319f7b8343df12cc98938e5c3eb436064524c8d2b4e30a1d3a36eecdf81"
3062+dependencies = [
3063+ "memchr",
3064+]
3065+
3066+[[package]]
3067+name = "writeable"
3068+version = "0.6.3"
3069+source = "registry+https://github.com/rust-lang/crates.io-index"
3070+checksum = "1ffae5123b2d3fc086436f8834ae3ab053a283cfac8fe0a0b8eaae044768a4c4"
3071+
3072+[[package]]
3073+name = "yoke"
3074+version = "0.8.3"
3075+source = "registry+https://github.com/rust-lang/crates.io-index"
3076+checksum = "709fe23a0424b6a435d82152b1bd3fdfb0833487d5fa90d05d42762a9891fef5"
3077+dependencies = [
3078+ "stable_deref_trait",
3079+ "yoke-derive",
3080+ "zerofrom",
3081+]
3082+
3083+[[package]]
3084+name = "yoke-derive"
3085+version = "0.8.2"
3086+source = "registry+https://github.com/rust-lang/crates.io-index"
3087+checksum = "de844c262c8848816172cef550288e7dc6c7b7814b4ee56b3e1553f275f1858e"
3088+dependencies = [
3089+ "proc-macro2",
3090+ "quote",
3091+ "syn 2.0.119",
3092+ "synstructure",
3093+]
3094+
3095+[[package]]
3096+name = "zerocopy"
3097+version = "0.8.56"
3098+source = "registry+https://github.com/rust-lang/crates.io-index"
3099+checksum = "556764e583adb45a9f8d413c2a147fa7e8d821e48e12b14fd560b607998b75eb"
3100+dependencies = [
3101+ "zerocopy-derive",
3102+]
3103+
3104+[[package]]
3105+name = "zerocopy-derive"
3106+version = "0.8.56"
3107+source = "registry+https://github.com/rust-lang/crates.io-index"
3108+checksum = "f2ab42fc20575779bd240faa45f94a74256f755c0fa9e89f0ede20d91d0cdfc1"
3109+dependencies = [
3110+ "proc-macro2",
3111+ "quote",
3112+ "syn 2.0.119",
3113+]
3114+
3115+[[package]]
3116+name = "zerofrom"
3117+version = "0.1.8"
3118+source = "registry+https://github.com/rust-lang/crates.io-index"
3119+checksum = "0ec05a11813ea801ff6d75110ad09cd0824ddba17dfe17128ea0d5f68e6c5272"
3120+dependencies = [
3121+ "zerofrom-derive",
3122+]
3123+
3124+[[package]]
3125+name = "zerofrom-derive"
3126+version = "0.1.7"
3127+source = "registry+https://github.com/rust-lang/crates.io-index"
3128+checksum = "11532158c46691caf0f2593ea8358fed6bbf68a0315e80aae9bd41fbade684a1"
3129+dependencies = [
3130+ "proc-macro2",
3131+ "quote",
3132+ "syn 2.0.119",
3133+ "synstructure",
3134+]
3135+
3136+[[package]]
3137+name = "zeroize"
3138+version = "1.9.0"
3139+source = "registry+https://github.com/rust-lang/crates.io-index"
3140+checksum = "e13c156562582aa81c60cb29407084cdb54c4164760106ab78e6c5b0858cf64e"
3141+
3142+[[package]]
3143+name = "zerotrie"
3144+version = "0.2.4"
3145+source = "registry+https://github.com/rust-lang/crates.io-index"
3146+checksum = "0f9152d31db0792fa83f70fb2f83148effb5c1f5b8c7686c3459e361d9bc20bf"
3147+dependencies = [
3148+ "displaydoc",
3149+ "yoke",
3150+ "zerofrom",
3151+]
3152+
3153+[[package]]
3154+name = "zerovec"
3155+version = "0.11.6"
3156+source = "registry+https://github.com/rust-lang/crates.io-index"
3157+checksum = "90f911cbc359ab6af17377d242225f4d75119aec87ea711a880987b18cd7b239"
3158+dependencies = [
3159+ "yoke",
3160+ "zerofrom",
3161+ "zerovec-derive",
3162+]
3163+
3164+[[package]]
3165+name = "zerovec-derive"
3166+version = "0.11.3"
3167+source = "registry+https://github.com/rust-lang/crates.io-index"
3168+checksum = "625dc425cab0dca6dc3c3319506e6593dcb08a9f387ea3b284dbd52a92c40555"
3169+dependencies = [
3170+ "proc-macro2",
3171+ "quote",
3172+ "syn 2.0.119",
3173+]
3174+
3175+[[package]]
3176+name = "zmij"
3177+version = "1.0.23"
3178+source = "registry+https://github.com/rust-lang/crates.io-index"
3179+checksum = "29666d0abbfad1e3dc4dcf6144730dd3a3ab225bbbdac83319345b1b44ccfc1b"
added Cargo.toml +31 -0
new file mode 100644
@@ -0,0 +1,31 @@
1+[package]
2+name = "time-time"
3+version = "0.1.0"
4+edition = "2024"
5+
6+[dependencies]
7+axum = "0.8"
8+axum-extra = { version = "0.12", features = ["cookie"] }
9+tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal"] }
10+askama = "0.16"
11+sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls-ring", "postgres", "chrono", "migrate", "uuid"] }
12+uuid = { version = "1", features = ["v7", "serde"] }
13+biscuit-auth = "6"
14+argon2 = "0.5"
15+chrono = { version = "0.4", features = ["serde"] }
16+chrono-tz = "0.10"
17+clap = { version = "4", features = ["derive"] }
18+thiserror = "2"
19+anyhow = "1"
20+hmac = "0.12"
21+sha2 = "0.10"
22+hex = "0.4"
23+subtle = "2"
24+time = "0.3"
25+rpassword = "7"
26+serde = { version = "1", features = ["derive"] }
27+tracing = "0.1"
28+tracing-subscriber = { version = "0.3", features = ["env-filter"] }
29+
30+[profile.release]
31+lto = "thin"
new file mode 100644
@@ -0,0 +1,31 @@
1+[package]
2+name = "time-time"
3+version = "0.1.0"
4+edition = "2024"
5+
6+[dependencies]
7+axum = "0.8"
8+axum-extra = { version = "0.12", features = ["cookie"] }
9+tokio = { version = "1", features = ["rt-multi-thread", "macros", "signal"] }
10+askama = "0.16"
11+sqlx = { version = "0.8", features = ["runtime-tokio", "tls-rustls-ring", "postgres", "chrono", "migrate", "uuid"] }
12+uuid = { version = "1", features = ["v7", "serde"] }
13+biscuit-auth = "6"
14+argon2 = "0.5"
15+chrono = { version = "0.4", features = ["serde"] }
16+chrono-tz = "0.10"
17+clap = { version = "4", features = ["derive"] }
18+thiserror = "2"
19+anyhow = "1"
20+hmac = "0.12"
21+sha2 = "0.10"
22+hex = "0.4"
23+subtle = "2"
24+time = "0.3"
25+rpassword = "7"
26+serde = { version = "1", features = ["derive"] }
27+tracing = "0.1"
28+tracing-subscriber = { version = "0.3", features = ["env-filter"] }
29+
30+[profile.release]
31+lto = "thin"
added assets/icon-512.png +0 -0
new file mode 100644
Binary files /dev/null and b/assets/icon-512.png differ
new file mode 100644
Binary files /dev/null and b/assets/icon-512.png differBinary files /dev/null and b/assets/icon-512.png differ
added assets/manifest.json +18 -0
new file mode 100644
@@ -0,0 +1,18 @@
1+{
2+ "name": "time-time",
3+ "short_name": "time-time",
4+ "description": "Comptage de temps passé sur des tâches",
5+ "start_url": "/",
6+ "display": "standalone",
7+ "background_color": "#f7f8fa",
8+ "theme_color": "#1d3557",
9+ "lang": "fr",
10+ "icons": [
11+ {
12+ "src": "/icon-512.png",
13+ "sizes": "512x512",
14+ "type": "image/png",
15+ "purpose": "any maskable"
16+ }
17+ ]
18+}
new file mode 100644
@@ -0,0 +1,18 @@
1+{
2+ "name": "time-time",
3+ "short_name": "time-time",
4+ "description": "Comptage de temps passé sur des tâches",
5+ "start_url": "/",
6+ "display": "standalone",
7+ "background_color": "#f7f8fa",
8+ "theme_color": "#1d3557",
9+ "lang": "fr",
10+ "icons": [
11+ {
12+ "src": "/icon-512.png",
13+ "sizes": "512x512",
14+ "type": "image/png",
15+ "purpose": "any maskable"
16+ }
17+ ]
18+}
added assets/style.css +177 -0
new file mode 100644
@@ -0,0 +1,177 @@
1+/* Mobile-first, zéro JS. */
2+:root {
3+ --bg: #f7f8fa;
4+ --card: #ffffff;
5+ --ink: #1a2233;
6+ --muted: #5b6474;
7+ --primary: #1d3557;
8+ --primary-ink: #ffffff;
9+ --accent: #4e79a7;
10+ --danger: #c0392b;
11+ --border: #d8dde6;
12+ --ok-bg: #e7f6ec;
13+ --ok-ink: #1e7a3c;
14+ --err-bg: #fdecea;
15+ --err-ink: #b23327;
16+ --radius: 12px;
17+}
18+
19+* { box-sizing: border-box; }
20+
21+body {
22+ margin: 0;
23+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
24+ background: var(--bg);
25+ color: var(--ink);
26+ font-size: 17px;
27+ line-height: 1.45;
28+}
29+
30+.topbar {
31+ background: var(--primary);
32+ padding: .7rem 1rem;
33+ position: sticky;
34+ top: 0;
35+}
36+.brand {
37+ color: var(--primary-ink);
38+ text-decoration: none;
39+ font-weight: 700;
40+ font-size: 1.05rem;
41+}
42+
43+.page {
44+ max-width: 560px;
45+ margin: 0 auto;
46+ padding: 1rem 1rem 3rem;
47+}
48+
49+h1 { font-size: 1.4rem; margin: .5rem 0 1rem; }
50+h2 { font-size: 1.1rem; margin: 1.6rem 0 .6rem; }
51+
52+/* Boutons — cibles tactiles généreuses. */
53+.btn {
54+ display: inline-block;
55+ padding: .65rem 1rem;
56+ min-height: 44px;
57+ border: 1px solid var(--border);
58+ border-radius: var(--radius);
59+ background: var(--card);
60+ color: var(--ink);
61+ text-decoration: none;
62+ text-align: center;
63+ font-size: 1rem;
64+ cursor: pointer;
65+}
66+.btn:active { transform: translateY(1px); }
67+.btn-primary { background: var(--primary); border-color: var(--primary); color: var(--primary-ink); }
68+.btn-danger { background: var(--danger); border-color: var(--danger); color: #fff; }
69+.btn-big { display: block; width: 100%; padding: 1rem; font-size: 1.15rem; }
70+.btn-small { padding: .4rem .7rem; min-height: 36px; font-size: .9rem; }
71+
72+.menu { margin-top: 2rem; }
73+
74+.stack { display: flex; flex-direction: column; gap: .5rem; }
75+.stack label { font-weight: 600; margin-top: .6rem; }
76+.stack .btn { margin-top: 1rem; }
77+
78+input, select {
79+ width: 100%;
80+ padding: .65rem .8rem;
81+ font-size: 1.05rem;
82+ border: 1px solid var(--border);
83+ border-radius: var(--radius);
84+ background: var(--card);
85+ color: var(--ink);
86+}
87+input:focus, select:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
88+
89+.presets { display: flex; gap: .5rem; flex-wrap: wrap; }
90+.btn-preset { flex: 1 1 auto; }
91+
92+.flash {
93+ padding: .6rem .9rem;
94+ border-radius: var(--radius);
95+ margin: .5rem 0;
96+}
97+.flash-ok { background: var(--ok-bg); color: var(--ok-ink); }
98+.flash-error { background: var(--err-bg); color: var(--err-ink); }
99+
100+/* Liste de mes temps */
101+.entries { list-style: none; margin: 0; padding: 0; }
102+.entries li + li { margin-top: .5rem; }
103+.entry {
104+ display: grid;
105+ grid-template-columns: auto 1fr auto;
106+ gap: .8rem;
107+ align-items: center;
108+ padding: .8rem 1rem;
109+ background: var(--card);
110+ border: 1px solid var(--border);
111+ border-radius: var(--radius);
112+ text-decoration: none;
113+ color: var(--ink);
114+}
115+.entry-date { color: var(--muted); font-variant-numeric: tabular-nums; }
116+.entry-task { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
117+.entry-duration { font-variant-numeric: tabular-nums; }
118+
119+.pager { display: flex; justify-content: space-between; gap: .5rem; margin: 1rem 0; }
120+
121+/* Stats */
122+.chart-block {
123+ background: var(--card);
124+ border: 1px solid var(--border);
125+ border-radius: var(--radius);
126+ padding: .8rem;
127+ margin-bottom: 1.2rem;
128+}
129+.chart-head {
130+ display: flex;
131+ align-items: center;
132+ justify-content: space-between;
133+ gap: .5rem;
134+ margin-bottom: .4rem;
135+}
136+.chart-head h2 { margin: 0; font-size: 1rem; text-align: center; flex: 1; }
137+.btn-nav { min-width: 44px; }
138+.btn-nav-placeholder { min-width: 44px; display: inline-block; }
139+
140+.chart { display: block; }
141+.gridline { stroke: var(--border); stroke-width: 1; }
142+.axis { stroke: var(--muted); stroke-width: 1; }
143+.tick { font-size: 10px; fill: var(--muted); }
144+.bar-label { font-size: 11px; fill: var(--ink); font-weight: 600; }
145+.total-label { font-size: 10px; fill: var(--muted); }
146+.empty-label { font-size: 12px; fill: var(--muted); }
147+
148+.legend {
149+ list-style: none;
150+ margin: 0 0 1rem;
151+ padding: 0;
152+ display: flex;
153+ flex-wrap: wrap;
154+ gap: .4rem .9rem;
155+ font-size: .9rem;
156+}
157+.legend li { display: flex; align-items: center; gap: .35rem; }
158+.swatch {
159+ width: 12px; height: 12px;
160+ border-radius: 3px;
161+ display: inline-block;
162+}
163+
164+/* Paramètres */
165+.task-list { list-style: none; margin: 0; padding: 0; }
166+.task {
167+ display: flex;
168+ gap: .5rem;
169+ align-items: center;
170+ padding: .5rem 0;
171+ border-bottom: 1px solid var(--border);
172+}
173+.task-rename { display: flex; gap: .5rem; flex: 1; }
174+.task-archived input { color: var(--muted); }
175+.task-add { display: flex; gap: .5rem; margin-top: .8rem; }
176+.hint { color: var(--muted); font-size: .85rem; }
177+.danger-zone { margin-top: 2rem; }
new file mode 100644
@@ -0,0 +1,177 @@
1+/* Mobile-first, zéro JS. */
2+:root {
3+ --bg: #f7f8fa;
4+ --card: #ffffff;
5+ --ink: #1a2233;
6+ --muted: #5b6474;
7+ --primary: #1d3557;
8+ --primary-ink: #ffffff;
9+ --accent: #4e79a7;
10+ --danger: #c0392b;
11+ --border: #d8dde6;
12+ --ok-bg: #e7f6ec;
13+ --ok-ink: #1e7a3c;
14+ --err-bg: #fdecea;
15+ --err-ink: #b23327;
16+ --radius: 12px;
17+}
18+
19+* { box-sizing: border-box; }
20+
21+body {
22+ margin: 0;
23+ font-family: system-ui, -apple-system, "Segoe UI", Roboto, sans-serif;
24+ background: var(--bg);
25+ color: var(--ink);
26+ font-size: 17px;
27+ line-height: 1.45;
28+}
29+
30+.topbar {
31+ background: var(--primary);
32+ padding: .7rem 1rem;
33+ position: sticky;
34+ top: 0;
35+}
36+.brand {
37+ color: var(--primary-ink);
38+ text-decoration: none;
39+ font-weight: 700;
40+ font-size: 1.05rem;
41+}
42+
43+.page {
44+ max-width: 560px;
45+ margin: 0 auto;
46+ padding: 1rem 1rem 3rem;
47+}
48+
49+h1 { font-size: 1.4rem; margin: .5rem 0 1rem; }
50+h2 { font-size: 1.1rem; margin: 1.6rem 0 .6rem; }
51+
52+/* Boutons — cibles tactiles généreuses. */
53+.btn {
54+ display: inline-block;
55+ padding: .65rem 1rem;
56+ min-height: 44px;
57+ border: 1px solid var(--border);
58+ border-radius: var(--radius);
59+ background: var(--card);
60+ color: var(--ink);
61+ text-decoration: none;
62+ text-align: center;
63+ font-size: 1rem;
64+ cursor: pointer;
65+}
66+.btn:active { transform: translateY(1px); }
67+.btn-primary { background: var(--primary); border-color: var(--primary); color: var(--primary-ink); }
68+.btn-danger { background: var(--danger); border-color: var(--danger); color: #fff; }
69+.btn-big { display: block; width: 100%; padding: 1rem; font-size: 1.15rem; }
70+.btn-small { padding: .4rem .7rem; min-height: 36px; font-size: .9rem; }
71+
72+.menu { margin-top: 2rem; }
73+
74+.stack { display: flex; flex-direction: column; gap: .5rem; }
75+.stack label { font-weight: 600; margin-top: .6rem; }
76+.stack .btn { margin-top: 1rem; }
77+
78+input, select {
79+ width: 100%;
80+ padding: .65rem .8rem;
81+ font-size: 1.05rem;
82+ border: 1px solid var(--border);
83+ border-radius: var(--radius);
84+ background: var(--card);
85+ color: var(--ink);
86+}
87+input:focus, select:focus { outline: 2px solid var(--accent); outline-offset: 1px; }
88+
89+.presets { display: flex; gap: .5rem; flex-wrap: wrap; }
90+.btn-preset { flex: 1 1 auto; }
91+
92+.flash {
93+ padding: .6rem .9rem;
94+ border-radius: var(--radius);
95+ margin: .5rem 0;
96+}
97+.flash-ok { background: var(--ok-bg); color: var(--ok-ink); }
98+.flash-error { background: var(--err-bg); color: var(--err-ink); }
99+
100+/* Liste de mes temps */
101+.entries { list-style: none; margin: 0; padding: 0; }
102+.entries li + li { margin-top: .5rem; }
103+.entry {
104+ display: grid;
105+ grid-template-columns: auto 1fr auto;
106+ gap: .8rem;
107+ align-items: center;
108+ padding: .8rem 1rem;
109+ background: var(--card);
110+ border: 1px solid var(--border);
111+ border-radius: var(--radius);
112+ text-decoration: none;
113+ color: var(--ink);
114+}
115+.entry-date { color: var(--muted); font-variant-numeric: tabular-nums; }
116+.entry-task { font-weight: 600; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
117+.entry-duration { font-variant-numeric: tabular-nums; }
118+
119+.pager { display: flex; justify-content: space-between; gap: .5rem; margin: 1rem 0; }
120+
121+/* Stats */
122+.chart-block {
123+ background: var(--card);
124+ border: 1px solid var(--border);
125+ border-radius: var(--radius);
126+ padding: .8rem;
127+ margin-bottom: 1.2rem;
128+}
129+.chart-head {
130+ display: flex;
131+ align-items: center;
132+ justify-content: space-between;
133+ gap: .5rem;
134+ margin-bottom: .4rem;
135+}
136+.chart-head h2 { margin: 0; font-size: 1rem; text-align: center; flex: 1; }
137+.btn-nav { min-width: 44px; }
138+.btn-nav-placeholder { min-width: 44px; display: inline-block; }
139+
140+.chart { display: block; }
141+.gridline { stroke: var(--border); stroke-width: 1; }
142+.axis { stroke: var(--muted); stroke-width: 1; }
143+.tick { font-size: 10px; fill: var(--muted); }
144+.bar-label { font-size: 11px; fill: var(--ink); font-weight: 600; }
145+.total-label { font-size: 10px; fill: var(--muted); }
146+.empty-label { font-size: 12px; fill: var(--muted); }
147+
148+.legend {
149+ list-style: none;
150+ margin: 0 0 1rem;
151+ padding: 0;
152+ display: flex;
153+ flex-wrap: wrap;
154+ gap: .4rem .9rem;
155+ font-size: .9rem;
156+}
157+.legend li { display: flex; align-items: center; gap: .35rem; }
158+.swatch {
159+ width: 12px; height: 12px;
160+ border-radius: 3px;
161+ display: inline-block;
162+}
163+
164+/* Paramètres */
165+.task-list { list-style: none; margin: 0; padding: 0; }
166+.task {
167+ display: flex;
168+ gap: .5rem;
169+ align-items: center;
170+ padding: .5rem 0;
171+ border-bottom: 1px solid var(--border);
172+}
173+.task-rename { display: flex; gap: .5rem; flex: 1; }
174+.task-archived input { color: var(--muted); }
175+.task-add { display: flex; gap: .5rem; margin-top: .8rem; }
176+.hint { color: var(--muted); font-size: .85rem; }
177+.danger-zone { margin-top: 2rem; }
added migrations/0001_init.sql +30 -0
new file mode 100644
@@ -0,0 +1,30 @@
1+-- IDs : UUIDv7 générés côté application (Rust), pas de DEFAULT en base.
2+CREATE TABLE users (
3+ id UUID PRIMARY KEY,
4+ username TEXT NOT NULL UNIQUE,
5+ password_hash TEXT NOT NULL, -- PHC string ($argon2id$...)
6+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
7+);
8+
9+CREATE TABLE tasks (
10+ id UUID PRIMARY KEY,
11+ name TEXT NOT NULL UNIQUE,
12+ archived BOOLEAN NOT NULL DEFAULT FALSE,
13+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
14+);
15+
16+CREATE TABLE time_entries (
17+ id UUID PRIMARY KEY,
18+ user_id UUID NOT NULL REFERENCES users(id),
19+ task_id UUID NOT NULL REFERENCES tasks(id),
20+ minutes INTEGER NOT NULL CHECK (minutes > 0 AND minutes <= 1440),
21+ entry_date DATE NOT NULL,
22+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
23+);
24+CREATE INDEX idx_entries_date ON time_entries (entry_date);
25+CREATE INDEX idx_entries_user_recent ON time_entries (user_id, entry_date DESC, id DESC);
26+
27+CREATE TABLE revoked_tokens (
28+ revocation_id BYTEA PRIMARY KEY,
29+ revoked_at TIMESTAMPTZ NOT NULL DEFAULT now()
30+);
new file mode 100644
@@ -0,0 +1,30 @@
1+-- IDs : UUIDv7 générés côté application (Rust), pas de DEFAULT en base.
2+CREATE TABLE users (
3+ id UUID PRIMARY KEY,
4+ username TEXT NOT NULL UNIQUE,
5+ password_hash TEXT NOT NULL, -- PHC string ($argon2id$...)
6+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
7+);
8+
9+CREATE TABLE tasks (
10+ id UUID PRIMARY KEY,
11+ name TEXT NOT NULL UNIQUE,
12+ archived BOOLEAN NOT NULL DEFAULT FALSE,
13+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
14+);
15+
16+CREATE TABLE time_entries (
17+ id UUID PRIMARY KEY,
18+ user_id UUID NOT NULL REFERENCES users(id),
19+ task_id UUID NOT NULL REFERENCES tasks(id),
20+ minutes INTEGER NOT NULL CHECK (minutes > 0 AND minutes <= 1440),
21+ entry_date DATE NOT NULL,
22+ created_at TIMESTAMPTZ NOT NULL DEFAULT now()
23+);
24+CREATE INDEX idx_entries_date ON time_entries (entry_date);
25+CREATE INDEX idx_entries_user_recent ON time_entries (user_id, entry_date DESC, id DESC);
26+
27+CREATE TABLE revoked_tokens (
28+ revocation_id BYTEA PRIMARY KEY,
29+ revoked_at TIMESTAMPTZ NOT NULL DEFAULT now()
30+);
added src/auth/csrf.rs +31 -0
new file mode 100644
@@ -0,0 +1,31 @@
1+use hmac::{Hmac, Mac};
2+use sha2::Sha256;
3+use subtle::ConstantTimeEq;
4+
5+/// Token CSRF stateless : hex(HMAC-SHA256(csrf_key, revocation_id de la session)).
6+/// Lié à la session (un attaquant cross-origin ne peut pas le calculer),
7+/// aucun stockage, survit aux redémarrages.
8+pub fn token_for(csrf_key: &[u8; 32], session_rev_id: &[u8]) -> String {
9+ let mut mac = Hmac::<Sha256>::new_from_slice(csrf_key).expect("HMAC accepte toute taille");
10+ mac.update(session_rev_id);
11+ hex::encode(mac.finalize().into_bytes())
12+}
13+
14+pub fn verify(csrf_key: &[u8; 32], session_rev_id: &[u8], submitted: &str) -> bool {
15+ let expected = token_for(csrf_key, session_rev_id);
16+ expected.as_bytes().ct_eq(submitted.as_bytes()).into()
17+}
18+
19+#[cfg(test)]
20+mod tests {
21+ use super::*;
22+
23+ #[test]
24+ fn roundtrip_and_mismatch() {
25+ let key = [7u8; 32];
26+ let t = token_for(&key, b"rev-id");
27+ assert!(verify(&key, b"rev-id", &t));
28+ assert!(!verify(&key, b"other", &t));
29+ assert!(!verify(&key, b"rev-id", "deadbeef"));
30+ }
31+}
new file mode 100644
@@ -0,0 +1,31 @@
1+use hmac::{Hmac, Mac};
2+use sha2::Sha256;
3+use subtle::ConstantTimeEq;
4+
5+/// Token CSRF stateless : hex(HMAC-SHA256(csrf_key, revocation_id de la session)).
6+/// Lié à la session (un attaquant cross-origin ne peut pas le calculer),
7+/// aucun stockage, survit aux redémarrages.
8+pub fn token_for(csrf_key: &[u8; 32], session_rev_id: &[u8]) -> String {
9+ let mut mac = Hmac::<Sha256>::new_from_slice(csrf_key).expect("HMAC accepte toute taille");
10+ mac.update(session_rev_id);
11+ hex::encode(mac.finalize().into_bytes())
12+}
13+
14+pub fn verify(csrf_key: &[u8; 32], session_rev_id: &[u8], submitted: &str) -> bool {
15+ let expected = token_for(csrf_key, session_rev_id);
16+ expected.as_bytes().ct_eq(submitted.as_bytes()).into()
17+}
18+
19+#[cfg(test)]
20+mod tests {
21+ use super::*;
22+
23+ #[test]
24+ fn roundtrip_and_mismatch() {
25+ let key = [7u8; 32];
26+ let t = token_for(&key, b"rev-id");
27+ assert!(verify(&key, b"rev-id", &t));
28+ assert!(!verify(&key, b"other", &t));
29+ assert!(!verify(&key, b"rev-id", "deadbeef"));
30+ }
31+}
added src/auth/extractor.rs +74 -0
new file mode 100644
@@ -0,0 +1,74 @@
1+use axum::extract::FromRequestParts;
2+use axum::http::header::SET_COOKIE;
3+use axum::http::request::Parts;
4+use axum::response::{IntoResponse, Redirect, Response};
5+use axum_extra::extract::CookieJar;
6+use uuid::Uuid;
7+
8+use super::{SESSION_COOKIE, token};
9+use crate::state::AppState;
10+
11+/// Utilisateur authentifié, extrait du biscuit en cookie.
12+pub struct AuthUser {
13+ pub id: Uuid,
14+ /// Tous les identifiants de révocation du token (pour logout).
15+ pub rev_ids: Vec<Vec<u8>>,
16+}
17+
18+impl AuthUser {
19+ /// Identifiant de révocation du bloc d'autorité — sert de base au token CSRF.
20+ pub fn session_rev_id(&self) -> &[u8] {
21+ self.rev_ids.first().map(Vec::as_slice).unwrap_or(&[])
22+ }
23+
24+ pub fn csrf_token(&self, state: &AppState) -> String {
25+ super::csrf::token_for(&state.csrf_key, self.session_rev_id())
26+ }
27+
28+ pub fn check_csrf(&self, state: &AppState, submitted: &str) -> bool {
29+ super::csrf::verify(&state.csrf_key, self.session_rev_id(), submitted)
30+ }
31+}
32+
33+/// Rejet : suppression du cookie + redirection vers /login.
34+pub struct AuthRedirect;
35+
36+impl IntoResponse for AuthRedirect {
37+ fn into_response(self) -> Response {
38+ let clear = format!("{SESSION_COOKIE}=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax");
39+ ([(SET_COOKIE, clear)], Redirect::to("/login")).into_response()
40+ }
41+}
42+
43+fn authenticate(parts: &mut Parts, state: &AppState) -> Option<AuthUser> {
44+ let jar = CookieJar::from_headers(&parts.headers);
45+ let cookie = jar.get(SESSION_COOKIE)?;
46+ let verified = token::verify(cookie.value(), state.public).ok()?;
47+ if state.revocations.contains_any(&verified.revocation_ids) {
48+ return None;
49+ }
50+ Some(AuthUser {
51+ id: verified.user_id,
52+ rev_ids: verified.revocation_ids,
53+ })
54+}
55+
56+impl FromRequestParts<AppState> for AuthUser {
57+ type Rejection = AuthRedirect;
58+
59+ async fn from_request_parts(parts: &mut Parts, state: &AppState) -> Result<Self, AuthRedirect> {
60+ authenticate(parts, state).ok_or(AuthRedirect)
61+ }
62+}
63+
64+/// Permet `Option<AuthUser>` dans les handlers (page de login).
65+impl axum::extract::OptionalFromRequestParts<AppState> for AuthUser {
66+ type Rejection = std::convert::Infallible;
67+
68+ async fn from_request_parts(
69+ parts: &mut Parts,
70+ state: &AppState,
71+ ) -> Result<Option<Self>, Self::Rejection> {
72+ Ok(authenticate(parts, state))
73+ }
74+}
new file mode 100644
@@ -0,0 +1,74 @@
1+use axum::extract::FromRequestParts;
2+use axum::http::header::SET_COOKIE;
3+use axum::http::request::Parts;
4+use axum::response::{IntoResponse, Redirect, Response};
5+use axum_extra::extract::CookieJar;
6+use uuid::Uuid;
7+
8+use super::{SESSION_COOKIE, token};
9+use crate::state::AppState;
10+
11+/// Utilisateur authentifié, extrait du biscuit en cookie.
12+pub struct AuthUser {
13+ pub id: Uuid,
14+ /// Tous les identifiants de révocation du token (pour logout).
15+ pub rev_ids: Vec<Vec<u8>>,
16+}
17+
18+impl AuthUser {
19+ /// Identifiant de révocation du bloc d'autorité — sert de base au token CSRF.
20+ pub fn session_rev_id(&self) -> &[u8] {
21+ self.rev_ids.first().map(Vec::as_slice).unwrap_or(&[])
22+ }
23+
24+ pub fn csrf_token(&self, state: &AppState) -> String {
25+ super::csrf::token_for(&state.csrf_key, self.session_rev_id())
26+ }
27+
28+ pub fn check_csrf(&self, state: &AppState, submitted: &str) -> bool {
29+ super::csrf::verify(&state.csrf_key, self.session_rev_id(), submitted)
30+ }
31+}
32+
33+/// Rejet : suppression du cookie + redirection vers /login.
34+pub struct AuthRedirect;
35+
36+impl IntoResponse for AuthRedirect {
37+ fn into_response(self) -> Response {
38+ let clear = format!("{SESSION_COOKIE}=; Path=/; Max-Age=0; HttpOnly; SameSite=Lax");
39+ ([(SET_COOKIE, clear)], Redirect::to("/login")).into_response()
40+ }
41+}
42+
43+fn authenticate(parts: &mut Parts, state: &AppState) -> Option<AuthUser> {
44+ let jar = CookieJar::from_headers(&parts.headers);
45+ let cookie = jar.get(SESSION_COOKIE)?;
46+ let verified = token::verify(cookie.value(), state.public).ok()?;
47+ if state.revocations.contains_any(&verified.revocation_ids) {
48+ return None;
49+ }
50+ Some(AuthUser {
51+ id: verified.user_id,
52+ rev_ids: verified.revocation_ids,
53+ })
54+}
55+
56+impl FromRequestParts<AppState> for AuthUser {
57+ type Rejection = AuthRedirect;
58+
59+ async fn from_request_parts(parts: &mut Parts, state: &AppState) -> Result<Self, AuthRedirect> {
60+ authenticate(parts, state).ok_or(AuthRedirect)
61+ }
62+}
63+
64+/// Permet `Option<AuthUser>` dans les handlers (page de login).
65+impl axum::extract::OptionalFromRequestParts<AppState> for AuthUser {
66+ type Rejection = std::convert::Infallible;
67+
68+ async fn from_request_parts(
69+ parts: &mut Parts,
70+ state: &AppState,
71+ ) -> Result<Option<Self>, Self::Rejection> {
72+ Ok(authenticate(parts, state))
73+ }
74+}
added src/auth/mod.rs +9 -0
new file mode 100644
@@ -0,0 +1,9 @@
1+pub mod csrf;
2+pub mod extractor;
3+pub mod password;
4+pub mod revocation;
5+pub mod token;
6+
7+pub const SESSION_COOKIE: &str = "tt_session";
8+/// ~6 mois.
9+pub const SESSION_SECONDS: i64 = 6 * 30 * 24 * 3600;
new file mode 100644
@@ -0,0 +1,9 @@
1+pub mod csrf;
2+pub mod extractor;
3+pub mod password;
4+pub mod revocation;
5+pub mod token;
6+
7+pub const SESSION_COOKIE: &str = "tt_session";
8+/// ~6 mois.
9+pub const SESSION_SECONDS: i64 = 6 * 30 * 24 * 3600;
added src/auth/password.rs +35 -0
new file mode 100644
@@ -0,0 +1,35 @@
1+use anyhow::{Result, anyhow};
2+use argon2::Argon2;
3+use argon2::password_hash::rand_core::OsRng;
4+use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
5+
6+/// Hash argon2id (paramètres par défaut de la crate), sortie PHC string.
7+pub fn hash(password: &str) -> Result<String> {
8+ let salt = SaltString::generate(&mut OsRng);
9+ Argon2::default()
10+ .hash_password(password.as_bytes(), &salt)
11+ .map(|h| h.to_string())
12+ .map_err(|e| anyhow!("hash mot de passe : {e}"))
13+}
14+
15+pub fn verify(password: &str, phc: &str) -> bool {
16+ let Ok(parsed) = PasswordHash::new(phc) else {
17+ return false;
18+ };
19+ Argon2::default()
20+ .verify_password(password.as_bytes(), &parsed)
21+ .is_ok()
22+}
23+
24+#[cfg(test)]
25+mod tests {
26+ use super::*;
27+
28+ #[test]
29+ fn roundtrip() {
30+ let h = hash("s3cret").unwrap();
31+ assert!(h.starts_with("$argon2id$"));
32+ assert!(verify("s3cret", &h));
33+ assert!(!verify("wrong", &h));
34+ }
35+}
new file mode 100644
@@ -0,0 +1,35 @@
1+use anyhow::{Result, anyhow};
2+use argon2::Argon2;
3+use argon2::password_hash::rand_core::OsRng;
4+use argon2::password_hash::{PasswordHash, PasswordHasher, PasswordVerifier, SaltString};
5+
6+/// Hash argon2id (paramètres par défaut de la crate), sortie PHC string.
7+pub fn hash(password: &str) -> Result<String> {
8+ let salt = SaltString::generate(&mut OsRng);
9+ Argon2::default()
10+ .hash_password(password.as_bytes(), &salt)
11+ .map(|h| h.to_string())
12+ .map_err(|e| anyhow!("hash mot de passe : {e}"))
13+}
14+
15+pub fn verify(password: &str, phc: &str) -> bool {
16+ let Ok(parsed) = PasswordHash::new(phc) else {
17+ return false;
18+ };
19+ Argon2::default()
20+ .verify_password(password.as_bytes(), &parsed)
21+ .is_ok()
22+}
23+
24+#[cfg(test)]
25+mod tests {
26+ use super::*;
27+
28+ #[test]
29+ fn roundtrip() {
30+ let h = hash("s3cret").unwrap();
31+ assert!(h.starts_with("$argon2id$"));
32+ assert!(verify("s3cret", &h));
33+ assert!(!verify("wrong", &h));
34+ }
35+}
added src/auth/revocation.rs +59 -0
new file mode 100644
@@ -0,0 +1,59 @@
1+use std::collections::HashSet;
2+use std::sync::{Arc, RwLock};
3+use std::time::Duration;
4+
5+use anyhow::Result;
6+use sqlx::PgPool;
7+
8+/// Cache mémoire de la liste de révocation (bytes bruts, jamais base64).
9+#[derive(Clone, Default)]
10+pub struct RevocationCache(Arc<RwLock<HashSet<Vec<u8>>>>);
11+
12+impl RevocationCache {
13+ pub fn contains_any(&self, ids: &[Vec<u8>]) -> bool {
14+ let guard = self.0.read().expect("revocation cache lock poisoned");
15+ ids.iter().any(|id| guard.contains(id))
16+ }
17+
18+ pub fn insert_all(&self, ids: &[Vec<u8>]) {
19+ let mut guard = self.0.write().expect("revocation cache lock poisoned");
20+ for id in ids {
21+ guard.insert(id.clone());
22+ }
23+ }
24+
25+ pub fn replace(&self, set: HashSet<Vec<u8>>) {
26+ *self.0.write().expect("revocation cache lock poisoned") = set;
27+ }
28+}
29+
30+/// Révoque en base + effet immédiat sur le cache local.
31+pub async fn revoke(pool: &PgPool, cache: &RevocationCache, ids: &[Vec<u8>]) -> Result<()> {
32+ for id in ids {
33+ sqlx::query("INSERT INTO revoked_tokens (revocation_id) VALUES ($1) ON CONFLICT DO NOTHING")
34+ .bind(id)
35+ .execute(pool)
36+ .await?;
37+ }
38+ cache.insert_all(ids);
39+ Ok(())
40+}
41+
42+pub async fn reload(pool: &PgPool, cache: &RevocationCache) -> Result<()> {
43+ let rows: Vec<(Vec<u8>,)> = sqlx::query_as("SELECT revocation_id FROM revoked_tokens")
44+ .fetch_all(pool)
45+ .await?;
46+ cache.replace(rows.into_iter().map(|(id,)| id).collect());
47+ Ok(())
48+}
49+
50+/// Tâche de fond : recharge complète toutes les 30 s (échelle : petit groupe).
51+pub async fn refresher(pool: PgPool, cache: RevocationCache) {
52+ let mut interval = tokio::time::interval(Duration::from_secs(30));
53+ loop {
54+ interval.tick().await;
55+ if let Err(e) = reload(&pool, &cache).await {
56+ tracing::warn!(error = %e, "rechargement de la liste de révocation échoué");
57+ }
58+ }
59+}
new file mode 100644
@@ -0,0 +1,59 @@
1+use std::collections::HashSet;
2+use std::sync::{Arc, RwLock};
3+use std::time::Duration;
4+
5+use anyhow::Result;
6+use sqlx::PgPool;
7+
8+/// Cache mémoire de la liste de révocation (bytes bruts, jamais base64).
9+#[derive(Clone, Default)]
10+pub struct RevocationCache(Arc<RwLock<HashSet<Vec<u8>>>>);
11+
12+impl RevocationCache {
13+ pub fn contains_any(&self, ids: &[Vec<u8>]) -> bool {
14+ let guard = self.0.read().expect("revocation cache lock poisoned");
15+ ids.iter().any(|id| guard.contains(id))
16+ }
17+
18+ pub fn insert_all(&self, ids: &[Vec<u8>]) {
19+ let mut guard = self.0.write().expect("revocation cache lock poisoned");
20+ for id in ids {
21+ guard.insert(id.clone());
22+ }
23+ }
24+
25+ pub fn replace(&self, set: HashSet<Vec<u8>>) {
26+ *self.0.write().expect("revocation cache lock poisoned") = set;
27+ }
28+}
29+
30+/// Révoque en base + effet immédiat sur le cache local.
31+pub async fn revoke(pool: &PgPool, cache: &RevocationCache, ids: &[Vec<u8>]) -> Result<()> {
32+ for id in ids {
33+ sqlx::query("INSERT INTO revoked_tokens (revocation_id) VALUES ($1) ON CONFLICT DO NOTHING")
34+ .bind(id)
35+ .execute(pool)
36+ .await?;
37+ }
38+ cache.insert_all(ids);
39+ Ok(())
40+}
41+
42+pub async fn reload(pool: &PgPool, cache: &RevocationCache) -> Result<()> {
43+ let rows: Vec<(Vec<u8>,)> = sqlx::query_as("SELECT revocation_id FROM revoked_tokens")
44+ .fetch_all(pool)
45+ .await?;
46+ cache.replace(rows.into_iter().map(|(id,)| id).collect());
47+ Ok(())
48+}
49+
50+/// Tâche de fond : recharge complète toutes les 30 s (échelle : petit groupe).
51+pub async fn refresher(pool: PgPool, cache: RevocationCache) {
52+ let mut interval = tokio::time::interval(Duration::from_secs(30));
53+ loop {
54+ interval.tick().await;
55+ if let Err(e) = reload(&pool, &cache).await {
56+ tracing::warn!(error = %e, "rechargement de la liste de révocation échoué");
57+ }
58+ }
59+}
added src/auth/token.rs +95 -0
new file mode 100644
@@ -0,0 +1,95 @@
1+use std::collections::HashMap;
2+use std::time::{Duration, SystemTime, UNIX_EPOCH};
3+
4+use anyhow::{Context, Result, anyhow};
5+use biscuit_auth::builder::{AuthorizerBuilder, Term};
6+use biscuit_auth::datalog::RunLimits;
7+use biscuit_auth::{Biscuit, KeyPair, PublicKey};
8+use uuid::Uuid;
9+
10+use super::SESSION_SECONDS;
11+
12+pub struct VerifiedToken {
13+ pub user_id: Uuid,
14+ /// Identifiants de révocation de tous les blocs (bytes bruts).
15+ pub revocation_ids: Vec<Vec<u8>>,
16+}
17+
18+/// Forge un biscuit `user(<uuid>)` + check d'expiration à ~6 mois, encodé base64.
19+pub fn mint(root: &KeyPair, user_id: Uuid) -> Result<String> {
20+ let expiry = SystemTime::now() + Duration::from_secs(SESSION_SECONDS as u64);
21+ let expiry_secs = expiry
22+ .duration_since(UNIX_EPOCH)
23+ .context("horloge système antérieure à 1970")?
24+ .as_secs();
25+ let builder = Biscuit::builder().code_with_params(
26+ r#"user({id}); check if time($t), $t < {exp};"#,
27+ HashMap::from([
28+ ("id".to_string(), Term::Str(user_id.to_string())),
29+ ("exp".to_string(), Term::Date(expiry_secs)),
30+ ]),
31+ HashMap::new(),
32+ )?;
33+ let biscuit = builder.build(root)?;
34+ Ok(biscuit.to_base64()?)
35+}
36+
37+/// Vérifie signature + expiration, extrait l'id utilisateur et les
38+/// identifiants de révocation. Ne consulte PAS la liste de révocation
39+/// (fait par l'extracteur, contre le cache).
40+pub fn verify(token_b64: &str, public: PublicKey) -> Result<VerifiedToken> {
41+ let biscuit = Biscuit::from_base64(token_b64, public)?;
42+
43+ let mut authorizer = AuthorizerBuilder::new()
44+ .time()
45+ .set_limits(RunLimits {
46+ max_time: Duration::from_millis(100),
47+ ..Default::default()
48+ })
49+ .code("allow if user($u);")?
50+ .build(&biscuit)?;
51+ authorizer.authorize()?;
52+
53+ let ids: Vec<(String,)> = authorizer.query("data($id) <- user($id)")?;
54+ let (id_str,) = ids.first().ok_or_else(|| anyhow!("fact user absent"))?;
55+ let user_id = Uuid::parse_str(id_str).context("fact user n'est pas un UUID")?;
56+
57+ Ok(VerifiedToken {
58+ user_id,
59+ revocation_ids: biscuit.revocation_identifiers(),
60+ })
61+}
62+
63+#[cfg(test)]
64+mod tests {
65+ use super::*;
66+
67+ #[test]
68+ fn mint_and_verify() {
69+ let kp = KeyPair::new();
70+ let uid = Uuid::now_v7();
71+ let token = mint(&kp, uid).unwrap();
72+ let v = verify(&token, kp.public()).unwrap();
73+ assert_eq!(v.user_id, uid);
74+ assert!(!v.revocation_ids.is_empty());
75+ }
76+
77+ #[test]
78+ fn rejects_wrong_key() {
79+ let kp = KeyPair::new();
80+ let other = KeyPair::new();
81+ let token = mint(&kp, Uuid::now_v7()).unwrap();
82+ assert!(verify(&token, other.public()).is_err());
83+ }
84+
85+ #[test]
86+ fn rejects_tampered() {
87+ let kp = KeyPair::new();
88+ let token = mint(&kp, Uuid::now_v7()).unwrap();
89+ let mut bytes = token.into_bytes();
90+ let last = bytes.len() - 1;
91+ bytes[last] = if bytes[last] == b'A' { b'B' } else { b'A' };
92+ let tampered = String::from_utf8(bytes).unwrap();
93+ assert!(verify(&tampered, kp.public()).is_err());
94+ }
95+}
new file mode 100644
@@ -0,0 +1,95 @@
1+use std::collections::HashMap;
2+use std::time::{Duration, SystemTime, UNIX_EPOCH};
3+
4+use anyhow::{Context, Result, anyhow};
5+use biscuit_auth::builder::{AuthorizerBuilder, Term};
6+use biscuit_auth::datalog::RunLimits;
7+use biscuit_auth::{Biscuit, KeyPair, PublicKey};
8+use uuid::Uuid;
9+
10+use super::SESSION_SECONDS;
11+
12+pub struct VerifiedToken {
13+ pub user_id: Uuid,
14+ /// Identifiants de révocation de tous les blocs (bytes bruts).
15+ pub revocation_ids: Vec<Vec<u8>>,
16+}
17+
18+/// Forge un biscuit `user(<uuid>)` + check d'expiration à ~6 mois, encodé base64.
19+pub fn mint(root: &KeyPair, user_id: Uuid) -> Result<String> {
20+ let expiry = SystemTime::now() + Duration::from_secs(SESSION_SECONDS as u64);
21+ let expiry_secs = expiry
22+ .duration_since(UNIX_EPOCH)
23+ .context("horloge système antérieure à 1970")?
24+ .as_secs();
25+ let builder = Biscuit::builder().code_with_params(
26+ r#"user({id}); check if time($t), $t < {exp};"#,
27+ HashMap::from([
28+ ("id".to_string(), Term::Str(user_id.to_string())),
29+ ("exp".to_string(), Term::Date(expiry_secs)),
30+ ]),
31+ HashMap::new(),
32+ )?;
33+ let biscuit = builder.build(root)?;
34+ Ok(biscuit.to_base64()?)
35+}
36+
37+/// Vérifie signature + expiration, extrait l'id utilisateur et les
38+/// identifiants de révocation. Ne consulte PAS la liste de révocation
39+/// (fait par l'extracteur, contre le cache).
40+pub fn verify(token_b64: &str, public: PublicKey) -> Result<VerifiedToken> {
41+ let biscuit = Biscuit::from_base64(token_b64, public)?;
42+
43+ let mut authorizer = AuthorizerBuilder::new()
44+ .time()
45+ .set_limits(RunLimits {
46+ max_time: Duration::from_millis(100),
47+ ..Default::default()
48+ })
49+ .code("allow if user($u);")?
50+ .build(&biscuit)?;
51+ authorizer.authorize()?;
52+
53+ let ids: Vec<(String,)> = authorizer.query("data($id) <- user($id)")?;
54+ let (id_str,) = ids.first().ok_or_else(|| anyhow!("fact user absent"))?;
55+ let user_id = Uuid::parse_str(id_str).context("fact user n'est pas un UUID")?;
56+
57+ Ok(VerifiedToken {
58+ user_id,
59+ revocation_ids: biscuit.revocation_identifiers(),
60+ })
61+}
62+
63+#[cfg(test)]
64+mod tests {
65+ use super::*;
66+
67+ #[test]
68+ fn mint_and_verify() {
69+ let kp = KeyPair::new();
70+ let uid = Uuid::now_v7();
71+ let token = mint(&kp, uid).unwrap();
72+ let v = verify(&token, kp.public()).unwrap();
73+ assert_eq!(v.user_id, uid);
74+ assert!(!v.revocation_ids.is_empty());
75+ }
76+
77+ #[test]
78+ fn rejects_wrong_key() {
79+ let kp = KeyPair::new();
80+ let other = KeyPair::new();
81+ let token = mint(&kp, Uuid::now_v7()).unwrap();
82+ assert!(verify(&token, other.public()).is_err());
83+ }
84+
85+ #[test]
86+ fn rejects_tampered() {
87+ let kp = KeyPair::new();
88+ let token = mint(&kp, Uuid::now_v7()).unwrap();
89+ let mut bytes = token.into_bytes();
90+ let last = bytes.len() - 1;
91+ bytes[last] = if bytes[last] == b'A' { b'B' } else { b'A' };
92+ let tampered = String::from_utf8(bytes).unwrap();
93+ assert!(verify(&tampered, kp.public()).is_err());
94+ }
95+}
added src/charts.rs +267 -0
new file mode 100644
@@ -0,0 +1,267 @@
1+use std::collections::BTreeMap;
2+
3+use uuid::Uuid;
4+
5+use crate::db::stats::StatRow;
6+use crate::db::users::UserRow;
7+
8+/// Palette fixe, contrastée, lisible sur fond clair.
9+pub const PALETTE: [&str; 12] = [
10+ "#4e79a7", "#f28e2b", "#e15759", "#76b7b2", "#59a14f", "#edc948", "#b07aa1", "#ff9da7",
11+ "#9c755f", "#bab0ac", "#3b6ea5", "#8cd17d",
12+];
13+
14+/// Couleur stable dans le temps : hash de l'UUID de la tâche → index de palette.
15+pub fn task_color(task_id: Uuid) -> &'static str {
16+ let h = task_id
17+ .as_bytes()
18+ .iter()
19+ .fold(0usize, |acc, &b| acc.wrapping_mul(31).wrapping_add(b as usize));
20+ PALETTE[h % PALETTE.len()]
21+}
22+
23+/// « 45 min », « 2 h », « 12 h 30 »
24+pub fn fmt_minutes(total: i64) -> String {
25+ let (h, m) = (total / 60, total % 60);
26+ match (h, m) {
27+ (0, m) => format!("{m} min"),
28+ (h, 0) => format!("{h} h"),
29+ (h, m) => format!("{h} h {m:02}"),
30+ }
31+}
32+
33+// Géométrie du SVG (viewBox 0 0 400 260, en dur dans le template) — le template est muet.
34+const LEFT: f32 = 44.0;
35+const RIGHT: f32 = 392.0;
36+const TOP: f32 = 24.0;
37+const BOTTOM: f32 = 232.0;
38+const N_TICKS: i64 = 4;
39+
40+pub struct ChartView {
41+ pub title: String,
42+ pub empty: bool,
43+ pub bars: Vec<BarView>,
44+ pub y_ticks: Vec<Tick>,
45+ pub baseline: f32,
46+}
47+
48+pub struct BarView {
49+ pub user_label: String,
50+ pub total_label: String,
51+ pub label_x: f32,
52+ pub total_y: f32,
53+ pub segments: Vec<SegView>,
54+}
55+
56+pub struct SegView {
57+ pub x: f32,
58+ pub y: f32,
59+ pub w: f32,
60+ pub h: f32,
61+ pub color: &'static str,
62+ pub title: String,
63+}
64+
65+pub struct Tick {
66+ pub y: f32,
67+ pub label: String,
68+}
69+
70+pub struct LegendItem {
71+ pub name: String,
72+ pub color: &'static str,
73+}
74+
75+/// Échelle Y : plus petit pas « rond » (en minutes) tel que max ≤ 4 pas.
76+fn nice_step(max: i64) -> i64 {
77+ const STEPS: [i64; 10] = [15, 30, 60, 120, 240, 480, 960, 1920, 3840, 7680];
78+ for s in STEPS {
79+ if max <= N_TICKS * s {
80+ return s;
81+ }
82+ }
83+ (max / N_TICKS).max(1)
84+}
85+
86+impl ChartView {
87+ /// `users` : tous les utilisateurs (une barre chacun, même à zéro),
88+ /// `rows` : totaux user × tâche de la période.
89+ pub fn build(title: String, users: &[UserRow], rows: &[StatRow]) -> Self {
90+ // user_id -> [(task_id, task_name, total)] trié par task_id (UUIDv7 ≈ ordre
91+ // de création) : ordre d'empilement stable entre les trois graphiques.
92+ let mut by_user: BTreeMap<Uuid, Vec<&StatRow>> = BTreeMap::new();
93+ for r in rows {
94+ by_user.entry(r.user_id).or_default().push(r);
95+ }
96+ for segs in by_user.values_mut() {
97+ segs.sort_by_key(|r| r.task_id);
98+ }
99+
100+ let max_total = users
101+ .iter()
102+ .map(|u| {
103+ by_user
104+ .get(&u.id)
105+ .map(|segs| segs.iter().map(|r| r.total).sum::<i64>())
106+ .unwrap_or(0)
107+ })
108+ .max()
109+ .unwrap_or(0);
110+ let empty = max_total == 0;
111+ let step = nice_step(max_total.max(1));
112+ let y_max = (N_TICKS * step) as f32;
113+ let scale = (BOTTOM - TOP) / y_max;
114+
115+ let y_ticks = (0..=N_TICKS)
116+ .map(|i| Tick {
117+ y: BOTTOM - (i * step) as f32 * scale,
118+ label: fmt_minutes(i * step),
119+ })
120+ .collect();
121+
122+ let n = users.len().max(1) as f32;
123+ let slot = (RIGHT - LEFT) / n;
124+ let bar_w = (slot * 0.6).min(80.0);
125+
126+ let bars = users
127+ .iter()
128+ .enumerate()
129+ .map(|(i, u)| {
130+ let center = LEFT + slot * (i as f32 + 0.5);
131+ let x = center - bar_w / 2.0;
132+ let mut cumul = 0i64;
133+ let mut segments = Vec::new();
134+ if let Some(segs) = by_user.get(&u.id) {
135+ for r in segs {
136+ let y_bottom = BOTTOM - cumul as f32 * scale;
137+ cumul += r.total;
138+ let y_top = BOTTOM - cumul as f32 * scale;
139+ segments.push(SegView {
140+ x,
141+ y: y_top,
142+ w: bar_w,
143+ h: y_bottom - y_top,
144+ color: task_color(r.task_id),
145+ title: format!("{} — {}", r.task_name, fmt_minutes(r.total)),
146+ });
147+ }
148+ }
149+ BarView {
150+ user_label: u.username.clone(),
151+ total_label: if cumul > 0 {
152+ fmt_minutes(cumul)
153+ } else {
154+ String::new()
155+ },
156+ label_x: center,
157+ total_y: BOTTOM - cumul as f32 * scale - 5.0,
158+ segments,
159+ }
160+ })
161+ .collect();
162+
163+ Self {
164+ title,
165+ empty,
166+ bars,
167+ y_ticks,
168+ baseline: BOTTOM,
169+ }
170+ }
171+
172+ pub fn left(&self) -> f32 {
173+ LEFT
174+ }
175+ pub fn right(&self) -> f32 {
176+ RIGHT
177+ }
178+}
179+
180+/// Légende commune : union des tâches présentes, triée par nom.
181+pub fn legend(row_sets: &[&[StatRow]]) -> Vec<LegendItem> {
182+ let mut seen: BTreeMap<String, &'static str> = BTreeMap::new();
183+ for rows in row_sets {
184+ for r in *rows {
185+ seen.entry(r.task_name.clone())
186+ .or_insert_with(|| task_color(r.task_id));
187+ }
188+ }
189+ seen.into_iter()
190+ .map(|(name, color)| LegendItem { name, color })
191+ .collect()
192+}
193+
194+#[cfg(test)]
195+mod tests {
196+ use super::*;
197+
198+ fn user(id: u128, name: &str) -> UserRow {
199+ UserRow {
200+ id: Uuid::from_u128(id),
201+ username: name.into(),
202+ }
203+ }
204+
205+ fn row(uid: u128, tid: u128, task: &str, total: i64) -> StatRow {
206+ StatRow {
207+ user_id: Uuid::from_u128(uid),
208+ task_id: Uuid::from_u128(tid),
209+ task_name: task.into(),
210+ total,
211+ }
212+ }
213+
214+ #[test]
215+ fn fmt() {
216+ assert_eq!(fmt_minutes(45), "45 min");
217+ assert_eq!(fmt_minutes(120), "2 h");
218+ assert_eq!(fmt_minutes(750), "12 h 30");
219+ }
220+
221+ #[test]
222+ fn geometry_sums_to_totals() {
223+ let users = [user(1, "alice"), user(2, "bob")];
224+ let rows = [
225+ row(1, 10, "dev", 90),
226+ row(1, 11, "réunions", 30),
227+ row(2, 10, "dev", 240),
228+ ];
229+ let chart = ChartView::build("t".into(), &users, &rows);
230+ assert_eq!(chart.bars.len(), 2);
231+
232+ // La hauteur cumulée des segments correspond au total, à l'échelle près.
233+ let alice = &chart.bars[0];
234+ let bob = &chart.bars[1];
235+ let h_alice: f32 = alice.segments.iter().map(|s| s.h).sum();
236+ let h_bob: f32 = bob.segments.iter().map(|s| s.h).sum();
237+ assert!((h_alice / h_bob - 120.0 / 240.0).abs() < 1e-3);
238+ assert_eq!(alice.total_label, "2 h");
239+ assert_eq!(bob.total_label, "4 h");
240+
241+ // Empilement : segments contigus, du bas vers le haut.
242+ assert!((alice.segments[0].y - (alice.segments[1].y + alice.segments[1].h)).abs() < 1e-3);
243+ }
244+
245+ #[test]
246+ fn zero_user_has_no_segments() {
247+ let users = [user(1, "alice"), user(2, "bob")];
248+ let rows = [row(1, 10, "dev", 60)];
249+ let chart = ChartView::build("t".into(), &users, &rows);
250+ assert!(chart.bars[1].segments.is_empty());
251+ assert_eq!(chart.bars[1].total_label, "");
252+ assert!(!chart.empty);
253+ }
254+
255+ #[test]
256+ fn stable_color() {
257+ let id = Uuid::from_u128(42);
258+ assert_eq!(task_color(id), task_color(id));
259+ }
260+
261+ #[test]
262+ fn nice_steps() {
263+ assert_eq!(nice_step(1), 15); // ≤ 1 h → pas de 15 min
264+ assert_eq!(nice_step(240), 60);
265+ assert_eq!(nice_step(241), 120);
266+ }
267+}
new file mode 100644
@@ -0,0 +1,267 @@
1+use std::collections::BTreeMap;
2+
3+use uuid::Uuid;
4+
5+use crate::db::stats::StatRow;
6+use crate::db::users::UserRow;
7+
8+/// Palette fixe, contrastée, lisible sur fond clair.
9+pub const PALETTE: [&str; 12] = [
10+ "#4e79a7", "#f28e2b", "#e15759", "#76b7b2", "#59a14f", "#edc948", "#b07aa1", "#ff9da7",
11+ "#9c755f", "#bab0ac", "#3b6ea5", "#8cd17d",
12+];
13+
14+/// Couleur stable dans le temps : hash de l'UUID de la tâche → index de palette.
15+pub fn task_color(task_id: Uuid) -> &'static str {
16+ let h = task_id
17+ .as_bytes()
18+ .iter()
19+ .fold(0usize, |acc, &b| acc.wrapping_mul(31).wrapping_add(b as usize));
20+ PALETTE[h % PALETTE.len()]
21+}
22+
23+/// « 45 min », « 2 h », « 12 h 30 »
24+pub fn fmt_minutes(total: i64) -> String {
25+ let (h, m) = (total / 60, total % 60);
26+ match (h, m) {
27+ (0, m) => format!("{m} min"),
28+ (h, 0) => format!("{h} h"),
29+ (h, m) => format!("{h} h {m:02}"),
30+ }
31+}
32+
33+// Géométrie du SVG (viewBox 0 0 400 260, en dur dans le template) — le template est muet.
34+const LEFT: f32 = 44.0;
35+const RIGHT: f32 = 392.0;
36+const TOP: f32 = 24.0;
37+const BOTTOM: f32 = 232.0;
38+const N_TICKS: i64 = 4;
39+
40+pub struct ChartView {
41+ pub title: String,
42+ pub empty: bool,
43+ pub bars: Vec<BarView>,
44+ pub y_ticks: Vec<Tick>,
45+ pub baseline: f32,
46+}
47+
48+pub struct BarView {
49+ pub user_label: String,
50+ pub total_label: String,
51+ pub label_x: f32,
52+ pub total_y: f32,
53+ pub segments: Vec<SegView>,
54+}
55+
56+pub struct SegView {
57+ pub x: f32,
58+ pub y: f32,
59+ pub w: f32,
60+ pub h: f32,
61+ pub color: &'static str,
62+ pub title: String,
63+}
64+
65+pub struct Tick {
66+ pub y: f32,
67+ pub label: String,
68+}
69+
70+pub struct LegendItem {
71+ pub name: String,
72+ pub color: &'static str,
73+}
74+
75+/// Échelle Y : plus petit pas « rond » (en minutes) tel que max ≤ 4 pas.
76+fn nice_step(max: i64) -> i64 {
77+ const STEPS: [i64; 10] = [15, 30, 60, 120, 240, 480, 960, 1920, 3840, 7680];
78+ for s in STEPS {
79+ if max <= N_TICKS * s {
80+ return s;
81+ }
82+ }
83+ (max / N_TICKS).max(1)
84+}
85+
86+impl ChartView {
87+ /// `users` : tous les utilisateurs (une barre chacun, même à zéro),
88+ /// `rows` : totaux user × tâche de la période.
89+ pub fn build(title: String, users: &[UserRow], rows: &[StatRow]) -> Self {
90+ // user_id -> [(task_id, task_name, total)] trié par task_id (UUIDv7 ≈ ordre
91+ // de création) : ordre d'empilement stable entre les trois graphiques.
92+ let mut by_user: BTreeMap<Uuid, Vec<&StatRow>> = BTreeMap::new();
93+ for r in rows {
94+ by_user.entry(r.user_id).or_default().push(r);
95+ }
96+ for segs in by_user.values_mut() {
97+ segs.sort_by_key(|r| r.task_id);
98+ }
99+
100+ let max_total = users
101+ .iter()
102+ .map(|u| {
103+ by_user
104+ .get(&u.id)
105+ .map(|segs| segs.iter().map(|r| r.total).sum::<i64>())
106+ .unwrap_or(0)
107+ })
108+ .max()
109+ .unwrap_or(0);
110+ let empty = max_total == 0;
111+ let step = nice_step(max_total.max(1));
112+ let y_max = (N_TICKS * step) as f32;
113+ let scale = (BOTTOM - TOP) / y_max;
114+
115+ let y_ticks = (0..=N_TICKS)
116+ .map(|i| Tick {
117+ y: BOTTOM - (i * step) as f32 * scale,
118+ label: fmt_minutes(i * step),
119+ })
120+ .collect();
121+
122+ let n = users.len().max(1) as f32;
123+ let slot = (RIGHT - LEFT) / n;
124+ let bar_w = (slot * 0.6).min(80.0);
125+
126+ let bars = users
127+ .iter()
128+ .enumerate()
129+ .map(|(i, u)| {
130+ let center = LEFT + slot * (i as f32 + 0.5);
131+ let x = center - bar_w / 2.0;
132+ let mut cumul = 0i64;
133+ let mut segments = Vec::new();
134+ if let Some(segs) = by_user.get(&u.id) {
135+ for r in segs {
136+ let y_bottom = BOTTOM - cumul as f32 * scale;
137+ cumul += r.total;
138+ let y_top = BOTTOM - cumul as f32 * scale;
139+ segments.push(SegView {
140+ x,
141+ y: y_top,
142+ w: bar_w,
143+ h: y_bottom - y_top,
144+ color: task_color(r.task_id),
145+ title: format!("{} — {}", r.task_name, fmt_minutes(r.total)),
146+ });
147+ }
148+ }
149+ BarView {
150+ user_label: u.username.clone(),
151+ total_label: if cumul > 0 {
152+ fmt_minutes(cumul)
153+ } else {
154+ String::new()
155+ },
156+ label_x: center,
157+ total_y: BOTTOM - cumul as f32 * scale - 5.0,
158+ segments,
159+ }
160+ })
161+ .collect();
162+
163+ Self {
164+ title,
165+ empty,
166+ bars,
167+ y_ticks,
168+ baseline: BOTTOM,
169+ }
170+ }
171+
172+ pub fn left(&self) -> f32 {
173+ LEFT
174+ }
175+ pub fn right(&self) -> f32 {
176+ RIGHT
177+ }
178+}
179+
180+/// Légende commune : union des tâches présentes, triée par nom.
181+pub fn legend(row_sets: &[&[StatRow]]) -> Vec<LegendItem> {
182+ let mut seen: BTreeMap<String, &'static str> = BTreeMap::new();
183+ for rows in row_sets {
184+ for r in *rows {
185+ seen.entry(r.task_name.clone())
186+ .or_insert_with(|| task_color(r.task_id));
187+ }
188+ }
189+ seen.into_iter()
190+ .map(|(name, color)| LegendItem { name, color })
191+ .collect()
192+}
193+
194+#[cfg(test)]
195+mod tests {
196+ use super::*;
197+
198+ fn user(id: u128, name: &str) -> UserRow {
199+ UserRow {
200+ id: Uuid::from_u128(id),
201+ username: name.into(),
202+ }
203+ }
204+
205+ fn row(uid: u128, tid: u128, task: &str, total: i64) -> StatRow {
206+ StatRow {
207+ user_id: Uuid::from_u128(uid),
208+ task_id: Uuid::from_u128(tid),
209+ task_name: task.into(),
210+ total,
211+ }
212+ }
213+
214+ #[test]
215+ fn fmt() {
216+ assert_eq!(fmt_minutes(45), "45 min");
217+ assert_eq!(fmt_minutes(120), "2 h");
218+ assert_eq!(fmt_minutes(750), "12 h 30");
219+ }
220+
221+ #[test]
222+ fn geometry_sums_to_totals() {
223+ let users = [user(1, "alice"), user(2, "bob")];
224+ let rows = [
225+ row(1, 10, "dev", 90),
226+ row(1, 11, "réunions", 30),
227+ row(2, 10, "dev", 240),
228+ ];
229+ let chart = ChartView::build("t".into(), &users, &rows);
230+ assert_eq!(chart.bars.len(), 2);
231+
232+ // La hauteur cumulée des segments correspond au total, à l'échelle près.
233+ let alice = &chart.bars[0];
234+ let bob = &chart.bars[1];
235+ let h_alice: f32 = alice.segments.iter().map(|s| s.h).sum();
236+ let h_bob: f32 = bob.segments.iter().map(|s| s.h).sum();
237+ assert!((h_alice / h_bob - 120.0 / 240.0).abs() < 1e-3);
238+ assert_eq!(alice.total_label, "2 h");
239+ assert_eq!(bob.total_label, "4 h");
240+
241+ // Empilement : segments contigus, du bas vers le haut.
242+ assert!((alice.segments[0].y - (alice.segments[1].y + alice.segments[1].h)).abs() < 1e-3);
243+ }
244+
245+ #[test]
246+ fn zero_user_has_no_segments() {
247+ let users = [user(1, "alice"), user(2, "bob")];
248+ let rows = [row(1, 10, "dev", 60)];
249+ let chart = ChartView::build("t".into(), &users, &rows);
250+ assert!(chart.bars[1].segments.is_empty());
251+ assert_eq!(chart.bars[1].total_label, "");
252+ assert!(!chart.empty);
253+ }
254+
255+ #[test]
256+ fn stable_color() {
257+ let id = Uuid::from_u128(42);
258+ assert_eq!(task_color(id), task_color(id));
259+ }
260+
261+ #[test]
262+ fn nice_steps() {
263+ assert_eq!(nice_step(1), 15); // ≤ 1 h → pas de 15 min
264+ assert_eq!(nice_step(240), 60);
265+ assert_eq!(nice_step(241), 120);
266+ }
267+}
added src/cli.rs +56 -0
new file mode 100644
@@ -0,0 +1,56 @@
1+use anyhow::{Context, Result, bail};
2+use biscuit_auth::KeyPair;
3+use sqlx::postgres::PgPoolOptions;
4+
5+use crate::auth::password;
6+use crate::config;
7+use crate::db::users;
8+
9+/// Génère un keypair racine et l'affiche (à faire une fois, en local).
10+pub fn keygen() {
11+ let kp = KeyPair::new();
12+ println!("BISCUIT_PRIVATE_KEY={}", hex::encode(kp.private().to_bytes()));
13+ println!("# clé publique (information) : {}", kp.public().to_bytes_hex());
14+}
15+
16+/// `time-time user add <username>` — création de compte côté serveur.
17+pub async fn user_add(username: &str, password_arg: Option<String>) -> Result<()> {
18+ let username = username.trim();
19+ if username.is_empty() {
20+ bail!("le nom d'utilisateur ne peut pas être vide");
21+ }
22+
23+ let password = match password_arg {
24+ Some(p) => p,
25+ None => {
26+ let p1 = rpassword::prompt_password("Mot de passe : ")?;
27+ let p2 = rpassword::prompt_password("Confirmation : ")?;
28+ if p1 != p2 {
29+ bail!("les mots de passe ne correspondent pas");
30+ }
31+ p1
32+ }
33+ };
34+ if password.len() < 8 {
35+ bail!("le mot de passe doit faire au moins 8 caractères");
36+ }
37+
38+ let pool = PgPoolOptions::new()
39+ .max_connections(1)
40+ .connect(&config::database_url()?)
41+ .await
42+ .context("connexion à la base")?;
43+ sqlx::migrate!().run(&pool).await?;
44+
45+ let hash = password::hash(&password)?;
46+ match users::create(&pool, username, &hash).await {
47+ Ok(id) => {
48+ println!("Utilisateur « {username} » créé ({id}).");
49+ Ok(())
50+ }
51+ Err(sqlx::Error::Database(db)) if db.is_unique_violation() => {
52+ bail!("l'utilisateur « {username} » existe déjà")
53+ }
54+ Err(e) => Err(e.into()),
55+ }
56+}
new file mode 100644
@@ -0,0 +1,56 @@
1+use anyhow::{Context, Result, bail};
2+use biscuit_auth::KeyPair;
3+use sqlx::postgres::PgPoolOptions;
4+
5+use crate::auth::password;
6+use crate::config;
7+use crate::db::users;
8+
9+/// Génère un keypair racine et l'affiche (à faire une fois, en local).
10+pub fn keygen() {
11+ let kp = KeyPair::new();
12+ println!("BISCUIT_PRIVATE_KEY={}", hex::encode(kp.private().to_bytes()));
13+ println!("# clé publique (information) : {}", kp.public().to_bytes_hex());
14+}
15+
16+/// `time-time user add <username>` — création de compte côté serveur.
17+pub async fn user_add(username: &str, password_arg: Option<String>) -> Result<()> {
18+ let username = username.trim();
19+ if username.is_empty() {
20+ bail!("le nom d'utilisateur ne peut pas être vide");
21+ }
22+
23+ let password = match password_arg {
24+ Some(p) => p,
25+ None => {
26+ let p1 = rpassword::prompt_password("Mot de passe : ")?;
27+ let p2 = rpassword::prompt_password("Confirmation : ")?;
28+ if p1 != p2 {
29+ bail!("les mots de passe ne correspondent pas");
30+ }
31+ p1
32+ }
33+ };
34+ if password.len() < 8 {
35+ bail!("le mot de passe doit faire au moins 8 caractères");
36+ }
37+
38+ let pool = PgPoolOptions::new()
39+ .max_connections(1)
40+ .connect(&config::database_url()?)
41+ .await
42+ .context("connexion à la base")?;
43+ sqlx::migrate!().run(&pool).await?;
44+
45+ let hash = password::hash(&password)?;
46+ match users::create(&pool, username, &hash).await {
47+ Ok(id) => {
48+ println!("Utilisateur « {username} » créé ({id}).");
49+ Ok(())
50+ }
51+ Err(sqlx::Error::Database(db)) if db.is_unique_violation() => {
52+ bail!("l'utilisateur « {username} » existe déjà")
53+ }
54+ Err(e) => Err(e.into()),
55+ }
56+}
added src/config.rs +53 -0
new file mode 100644
@@ -0,0 +1,53 @@
1+use anyhow::{Context, Result};
2+
3+/// URL de la base : POSTGRESQL_ADDON_URI (Clever Cloud) sinon DATABASE_URL,
4+/// avec sslmode=require imposé si absent.
5+pub fn database_url() -> Result<String> {
6+ let url = std::env::var("POSTGRESQL_ADDON_URI")
7+ .or_else(|_| std::env::var("DATABASE_URL"))
8+ .context("POSTGRESQL_ADDON_URI ou DATABASE_URL doit être définie")?;
9+ Ok(ensure_sslmode_require(&url))
10+}
11+
12+fn ensure_sslmode_require(url: &str) -> String {
13+ if url.contains("sslmode=") {
14+ url.to_string()
15+ } else if url.contains('?') {
16+ format!("{url}&sslmode=require")
17+ } else {
18+ format!("{url}?sslmode=require")
19+ }
20+}
21+
22+pub fn port() -> u16 {
23+ std::env::var("PORT")
24+ .ok()
25+ .and_then(|p| p.parse().ok())
26+ .unwrap_or(8080)
27+}
28+
29+pub fn biscuit_private_key_hex() -> Result<String> {
30+ std::env::var("BISCUIT_PRIVATE_KEY")
31+ .context("BISCUIT_PRIVATE_KEY doit être définie (générer avec `time-time keygen`)")
32+}
33+
34+#[cfg(test)]
35+mod tests {
36+ use super::*;
37+
38+ #[test]
39+ fn sslmode_appended() {
40+ assert_eq!(
41+ ensure_sslmode_require("postgres://u:p@h:5432/db"),
42+ "postgres://u:p@h:5432/db?sslmode=require"
43+ );
44+ assert_eq!(
45+ ensure_sslmode_require("postgres://u:p@h:5432/db?foo=1"),
46+ "postgres://u:p@h:5432/db?foo=1&sslmode=require"
47+ );
48+ assert_eq!(
49+ ensure_sslmode_require("postgres://u:p@h:5432/db?sslmode=verify-full"),
50+ "postgres://u:p@h:5432/db?sslmode=verify-full"
51+ );
52+ }
53+}
new file mode 100644
@@ -0,0 +1,53 @@
1+use anyhow::{Context, Result};
2+
3+/// URL de la base : POSTGRESQL_ADDON_URI (Clever Cloud) sinon DATABASE_URL,
4+/// avec sslmode=require imposé si absent.
5+pub fn database_url() -> Result<String> {
6+ let url = std::env::var("POSTGRESQL_ADDON_URI")
7+ .or_else(|_| std::env::var("DATABASE_URL"))
8+ .context("POSTGRESQL_ADDON_URI ou DATABASE_URL doit être définie")?;
9+ Ok(ensure_sslmode_require(&url))
10+}
11+
12+fn ensure_sslmode_require(url: &str) -> String {
13+ if url.contains("sslmode=") {
14+ url.to_string()
15+ } else if url.contains('?') {
16+ format!("{url}&sslmode=require")
17+ } else {
18+ format!("{url}?sslmode=require")
19+ }
20+}
21+
22+pub fn port() -> u16 {
23+ std::env::var("PORT")
24+ .ok()
25+ .and_then(|p| p.parse().ok())
26+ .unwrap_or(8080)
27+}
28+
29+pub fn biscuit_private_key_hex() -> Result<String> {
30+ std::env::var("BISCUIT_PRIVATE_KEY")
31+ .context("BISCUIT_PRIVATE_KEY doit être définie (générer avec `time-time keygen`)")
32+}
33+
34+#[cfg(test)]
35+mod tests {
36+ use super::*;
37+
38+ #[test]
39+ fn sslmode_appended() {
40+ assert_eq!(
41+ ensure_sslmode_require("postgres://u:p@h:5432/db"),
42+ "postgres://u:p@h:5432/db?sslmode=require"
43+ );
44+ assert_eq!(
45+ ensure_sslmode_require("postgres://u:p@h:5432/db?foo=1"),
46+ "postgres://u:p@h:5432/db?foo=1&sslmode=require"
47+ );
48+ assert_eq!(
49+ ensure_sslmode_require("postgres://u:p@h:5432/db?sslmode=verify-full"),
50+ "postgres://u:p@h:5432/db?sslmode=verify-full"
51+ );
52+ }
53+}
added src/db/entries.rs +106 -0
new file mode 100644
@@ -0,0 +1,106 @@
1+use chrono::NaiveDate;
2+use sqlx::PgPool;
3+use uuid::Uuid;
4+
5+pub const PAGE_SIZE: i64 = 20;
6+
7+#[derive(sqlx::FromRow)]
8+pub struct EntryRow {
9+ pub id: Uuid,
10+ pub task_id: Uuid,
11+ pub task_name: String,
12+ pub minutes: i32,
13+ pub entry_date: NaiveDate,
14+}
15+
16+pub async fn insert(
17+ pool: &PgPool,
18+ user_id: Uuid,
19+ task_id: Uuid,
20+ minutes: i32,
21+ entry_date: NaiveDate,
22+) -> sqlx::Result<Uuid> {
23+ let id = Uuid::now_v7();
24+ sqlx::query(
25+ "INSERT INTO time_entries (id, user_id, task_id, minutes, entry_date)
26+ VALUES ($1, $2, $3, $4, $5)",
27+ )
28+ .bind(id)
29+ .bind(user_id)
30+ .bind(task_id)
31+ .bind(minutes)
32+ .bind(entry_date)
33+ .execute(pool)
34+ .await?;
35+ Ok(id)
36+}
37+
38+/// Page de MES temps, du plus récent au plus ancien.
39+/// Retourne (entrées, y a-t-il une page suivante) — on lit PAGE_SIZE + 1 lignes.
40+pub async fn list_page(
41+ pool: &PgPool,
42+ user_id: Uuid,
43+ page: i64,
44+) -> sqlx::Result<(Vec<EntryRow>, bool)> {
45+ let offset = (page - 1) * PAGE_SIZE;
46+ let mut rows: Vec<EntryRow> = sqlx::query_as(
47+ "SELECT e.id, e.task_id, t.name AS task_name, e.minutes, e.entry_date
48+ FROM time_entries e JOIN tasks t ON t.id = e.task_id
49+ WHERE e.user_id = $1
50+ ORDER BY e.entry_date DESC, e.id DESC
51+ LIMIT $2 OFFSET $3",
52+ )
53+ .bind(user_id)
54+ .bind(PAGE_SIZE + 1)
55+ .bind(offset)
56+ .fetch_all(pool)
57+ .await?;
58+ let has_next = rows.len() as i64 > PAGE_SIZE;
59+ rows.truncate(PAGE_SIZE as usize);
60+ Ok((rows, has_next))
61+}
62+
63+/// Une entrée m'appartenant (édition) — None si absente ou pas à moi.
64+pub async fn get_owned(pool: &PgPool, id: Uuid, user_id: Uuid) -> sqlx::Result<Option<EntryRow>> {
65+ sqlx::query_as(
66+ "SELECT e.id, e.task_id, t.name AS task_name, e.minutes, e.entry_date
67+ FROM time_entries e JOIN tasks t ON t.id = e.task_id
68+ WHERE e.id = $1 AND e.user_id = $2",
69+ )
70+ .bind(id)
71+ .bind(user_id)
72+ .fetch_optional(pool)
73+ .await
74+}
75+
76+/// Ownership vérifié en SQL : 0 ligne touchée = pas à moi / inexistant.
77+pub async fn update_owned(
78+ pool: &PgPool,
79+ id: Uuid,
80+ user_id: Uuid,
81+ task_id: Uuid,
82+ minutes: i32,
83+ entry_date: NaiveDate,
84+) -> sqlx::Result<u64> {
85+ let res = sqlx::query(
86+ "UPDATE time_entries SET task_id = $1, minutes = $2, entry_date = $3
87+ WHERE id = $4 AND user_id = $5",
88+ )
89+ .bind(task_id)
90+ .bind(minutes)
91+ .bind(entry_date)
92+ .bind(id)
93+ .bind(user_id)
94+ .execute(pool)
95+ .await?;
96+ Ok(res.rows_affected())
97+}
98+
99+pub async fn delete_owned(pool: &PgPool, id: Uuid, user_id: Uuid) -> sqlx::Result<u64> {
100+ let res = sqlx::query("DELETE FROM time_entries WHERE id = $1 AND user_id = $2")
101+ .bind(id)
102+ .bind(user_id)
103+ .execute(pool)
104+ .await?;
105+ Ok(res.rows_affected())
106+}
new file mode 100644
@@ -0,0 +1,106 @@
1+use chrono::NaiveDate;
2+use sqlx::PgPool;
3+use uuid::Uuid;
4+
5+pub const PAGE_SIZE: i64 = 20;
6+
7+#[derive(sqlx::FromRow)]
8+pub struct EntryRow {
9+ pub id: Uuid,
10+ pub task_id: Uuid,
11+ pub task_name: String,
12+ pub minutes: i32,
13+ pub entry_date: NaiveDate,
14+}
15+
16+pub async fn insert(
17+ pool: &PgPool,
18+ user_id: Uuid,
19+ task_id: Uuid,
20+ minutes: i32,
21+ entry_date: NaiveDate,
22+) -> sqlx::Result<Uuid> {
23+ let id = Uuid::now_v7();
24+ sqlx::query(
25+ "INSERT INTO time_entries (id, user_id, task_id, minutes, entry_date)
26+ VALUES ($1, $2, $3, $4, $5)",
27+ )
28+ .bind(id)
29+ .bind(user_id)
30+ .bind(task_id)
31+ .bind(minutes)
32+ .bind(entry_date)
33+ .execute(pool)
34+ .await?;
35+ Ok(id)
36+}
37+
38+/// Page de MES temps, du plus récent au plus ancien.
39+/// Retourne (entrées, y a-t-il une page suivante) — on lit PAGE_SIZE + 1 lignes.
40+pub async fn list_page(
41+ pool: &PgPool,
42+ user_id: Uuid,
43+ page: i64,
44+) -> sqlx::Result<(Vec<EntryRow>, bool)> {
45+ let offset = (page - 1) * PAGE_SIZE;
46+ let mut rows: Vec<EntryRow> = sqlx::query_as(
47+ "SELECT e.id, e.task_id, t.name AS task_name, e.minutes, e.entry_date
48+ FROM time_entries e JOIN tasks t ON t.id = e.task_id
49+ WHERE e.user_id = $1
50+ ORDER BY e.entry_date DESC, e.id DESC
51+ LIMIT $2 OFFSET $3",
52+ )
53+ .bind(user_id)
54+ .bind(PAGE_SIZE + 1)
55+ .bind(offset)
56+ .fetch_all(pool)
57+ .await?;
58+ let has_next = rows.len() as i64 > PAGE_SIZE;
59+ rows.truncate(PAGE_SIZE as usize);
60+ Ok((rows, has_next))
61+}
62+
63+/// Une entrée m'appartenant (édition) — None si absente ou pas à moi.
64+pub async fn get_owned(pool: &PgPool, id: Uuid, user_id: Uuid) -> sqlx::Result<Option<EntryRow>> {
65+ sqlx::query_as(
66+ "SELECT e.id, e.task_id, t.name AS task_name, e.minutes, e.entry_date
67+ FROM time_entries e JOIN tasks t ON t.id = e.task_id
68+ WHERE e.id = $1 AND e.user_id = $2",
69+ )
70+ .bind(id)
71+ .bind(user_id)
72+ .fetch_optional(pool)
73+ .await
74+}
75+
76+/// Ownership vérifié en SQL : 0 ligne touchée = pas à moi / inexistant.
77+pub async fn update_owned(
78+ pool: &PgPool,
79+ id: Uuid,
80+ user_id: Uuid,
81+ task_id: Uuid,
82+ minutes: i32,
83+ entry_date: NaiveDate,
84+) -> sqlx::Result<u64> {
85+ let res = sqlx::query(
86+ "UPDATE time_entries SET task_id = $1, minutes = $2, entry_date = $3
87+ WHERE id = $4 AND user_id = $5",
88+ )
89+ .bind(task_id)
90+ .bind(minutes)
91+ .bind(entry_date)
92+ .bind(id)
93+ .bind(user_id)
94+ .execute(pool)
95+ .await?;
96+ Ok(res.rows_affected())
97+}
98+
99+pub async fn delete_owned(pool: &PgPool, id: Uuid, user_id: Uuid) -> sqlx::Result<u64> {
100+ let res = sqlx::query("DELETE FROM time_entries WHERE id = $1 AND user_id = $2")
101+ .bind(id)
102+ .bind(user_id)
103+ .execute(pool)
104+ .await?;
105+ Ok(res.rows_affected())
106+}
added src/db/mod.rs +4 -0
new file mode 100644
@@ -0,0 +1,4 @@
1+pub mod entries;
2+pub mod stats;
3+pub mod tasks;
4+pub mod users;
new file mode 100644
@@ -0,0 +1,4 @@
1+pub mod entries;
2+pub mod stats;
3+pub mod tasks;
4+pub mod users;
added src/db/stats.rs +33 -0
new file mode 100644
@@ -0,0 +1,33 @@
1+use chrono::NaiveDate;
2+use sqlx::PgPool;
3+use uuid::Uuid;
4+
5+#[derive(sqlx::FromRow)]
6+pub struct StatRow {
7+ pub user_id: Uuid,
8+ pub task_id: Uuid,
9+ pub task_name: String,
10+ pub total: i64,
11+}
12+
13+/// Totaux par utilisateur × tâche sur [from, to) ; bornes NULL = depuis le début.
14+/// Les tâches archivées restent incluses.
15+pub async fn totals(
16+ pool: &PgPool,
17+ from: Option<NaiveDate>,
18+ to: Option<NaiveDate>,
19+) -> sqlx::Result<Vec<StatRow>> {
20+ sqlx::query_as(
21+ "SELECT e.user_id, e.task_id, t.name AS task_name,
22+ SUM(e.minutes)::BIGINT AS total
23+ FROM time_entries e
24+ JOIN tasks t ON t.id = e.task_id
25+ WHERE ($1::date IS NULL OR e.entry_date >= $1)
26+ AND ($2::date IS NULL OR e.entry_date < $2)
27+ GROUP BY e.user_id, e.task_id, t.name",
28+ )
29+ .bind(from)
30+ .bind(to)
31+ .fetch_all(pool)
32+ .await
33+}
new file mode 100644
@@ -0,0 +1,33 @@
1+use chrono::NaiveDate;
2+use sqlx::PgPool;
3+use uuid::Uuid;
4+
5+#[derive(sqlx::FromRow)]
6+pub struct StatRow {
7+ pub user_id: Uuid,
8+ pub task_id: Uuid,
9+ pub task_name: String,
10+ pub total: i64,
11+}
12+
13+/// Totaux par utilisateur × tâche sur [from, to) ; bornes NULL = depuis le début.
14+/// Les tâches archivées restent incluses.
15+pub async fn totals(
16+ pool: &PgPool,
17+ from: Option<NaiveDate>,
18+ to: Option<NaiveDate>,
19+) -> sqlx::Result<Vec<StatRow>> {
20+ sqlx::query_as(
21+ "SELECT e.user_id, e.task_id, t.name AS task_name,
22+ SUM(e.minutes)::BIGINT AS total
23+ FROM time_entries e
24+ JOIN tasks t ON t.id = e.task_id
25+ WHERE ($1::date IS NULL OR e.entry_date >= $1)
26+ AND ($2::date IS NULL OR e.entry_date < $2)
27+ GROUP BY e.user_id, e.task_id, t.name",
28+ )
29+ .bind(from)
30+ .bind(to)
31+ .fetch_all(pool)
32+ .await
33+}
added src/db/tasks.rs +50 -0
new file mode 100644
@@ -0,0 +1,50 @@
1+use sqlx::PgPool;
2+use uuid::Uuid;
3+
4+#[derive(sqlx::FromRow)]
5+pub struct TaskRow {
6+ pub id: Uuid,
7+ pub name: String,
8+ pub archived: bool,
9+}
10+
11+/// Tâches proposées dans le formulaire d'ajout (non archivées).
12+pub async fn list_active(pool: &PgPool) -> sqlx::Result<Vec<TaskRow>> {
13+ sqlx::query_as("SELECT id, name, archived FROM tasks WHERE NOT archived ORDER BY name")
14+ .fetch_all(pool)
15+ .await
16+}
17+
18+/// Toutes les tâches (écran Paramètres).
19+pub async fn list_all(pool: &PgPool) -> sqlx::Result<Vec<TaskRow>> {
20+ sqlx::query_as("SELECT id, name, archived FROM tasks ORDER BY archived, name")
21+ .fetch_all(pool)
22+ .await
23+}
24+
25+pub async fn create(pool: &PgPool, name: &str) -> sqlx::Result<Uuid> {
26+ let id = Uuid::now_v7();
27+ sqlx::query("INSERT INTO tasks (id, name) VALUES ($1, $2)")
28+ .bind(id)
29+ .bind(name)
30+ .execute(pool)
31+ .await?;
32+ Ok(id)
33+}
34+
35+pub async fn rename(pool: &PgPool, id: Uuid, name: &str) -> sqlx::Result<u64> {
36+ let res = sqlx::query("UPDATE tasks SET name = $1 WHERE id = $2")
37+ .bind(name)
38+ .bind(id)
39+ .execute(pool)
40+ .await?;
41+ Ok(res.rows_affected())
42+}
43+
44+pub async fn toggle_archived(pool: &PgPool, id: Uuid) -> sqlx::Result<u64> {
45+ let res = sqlx::query("UPDATE tasks SET archived = NOT archived WHERE id = $1")
46+ .bind(id)
47+ .execute(pool)
48+ .await?;
49+ Ok(res.rows_affected())
50+}
new file mode 100644
@@ -0,0 +1,50 @@
1+use sqlx::PgPool;
2+use uuid::Uuid;
3+
4+#[derive(sqlx::FromRow)]
5+pub struct TaskRow {
6+ pub id: Uuid,
7+ pub name: String,
8+ pub archived: bool,
9+}
10+
11+/// Tâches proposées dans le formulaire d'ajout (non archivées).
12+pub async fn list_active(pool: &PgPool) -> sqlx::Result<Vec<TaskRow>> {
13+ sqlx::query_as("SELECT id, name, archived FROM tasks WHERE NOT archived ORDER BY name")
14+ .fetch_all(pool)
15+ .await
16+}
17+
18+/// Toutes les tâches (écran Paramètres).
19+pub async fn list_all(pool: &PgPool) -> sqlx::Result<Vec<TaskRow>> {
20+ sqlx::query_as("SELECT id, name, archived FROM tasks ORDER BY archived, name")
21+ .fetch_all(pool)
22+ .await
23+}
24+
25+pub async fn create(pool: &PgPool, name: &str) -> sqlx::Result<Uuid> {
26+ let id = Uuid::now_v7();
27+ sqlx::query("INSERT INTO tasks (id, name) VALUES ($1, $2)")
28+ .bind(id)
29+ .bind(name)
30+ .execute(pool)
31+ .await?;
32+ Ok(id)
33+}
34+
35+pub async fn rename(pool: &PgPool, id: Uuid, name: &str) -> sqlx::Result<u64> {
36+ let res = sqlx::query("UPDATE tasks SET name = $1 WHERE id = $2")
37+ .bind(name)
38+ .bind(id)
39+ .execute(pool)
40+ .await?;
41+ Ok(res.rows_affected())
42+}
43+
44+pub async fn toggle_archived(pool: &PgPool, id: Uuid) -> sqlx::Result<u64> {
45+ let res = sqlx::query("UPDATE tasks SET archived = NOT archived WHERE id = $1")
46+ .bind(id)
47+ .execute(pool)
48+ .await?;
49+ Ok(res.rows_affected())
50+}
added src/db/users.rs +55 -0
new file mode 100644
@@ -0,0 +1,55 @@
1+use sqlx::PgPool;
2+use uuid::Uuid;
3+
4+#[derive(sqlx::FromRow)]
5+pub struct UserRow {
6+ pub id: Uuid,
7+ pub username: String,
8+}
9+
10+#[derive(sqlx::FromRow)]
11+pub struct Credentials {
12+ pub id: Uuid,
13+ pub password_hash: String,
14+}
15+
16+pub async fn create(pool: &PgPool, username: &str, password_hash: &str) -> sqlx::Result<Uuid> {
17+ let id = Uuid::now_v7();
18+ sqlx::query("INSERT INTO users (id, username, password_hash) VALUES ($1, $2, $3)")
19+ .bind(id)
20+ .bind(username)
21+ .bind(password_hash)
22+ .execute(pool)
23+ .await?;
24+ Ok(id)
25+}
26+
27+pub async fn find_credentials(pool: &PgPool, username: &str) -> sqlx::Result<Option<Credentials>> {
28+ sqlx::query_as("SELECT id, password_hash FROM users WHERE username = $1")
29+ .bind(username)
30+ .fetch_optional(pool)
31+ .await
32+}
33+
34+pub async fn password_hash(pool: &PgPool, id: Uuid) -> sqlx::Result<Option<String>> {
35+ let row: Option<(String,)> = sqlx::query_as("SELECT password_hash FROM users WHERE id = $1")
36+ .bind(id)
37+ .fetch_optional(pool)
38+ .await?;
39+ Ok(row.map(|(h,)| h))
40+}
41+
42+pub async fn update_password(pool: &PgPool, id: Uuid, password_hash: &str) -> sqlx::Result<()> {
43+ sqlx::query("UPDATE users SET password_hash = $1 WHERE id = $2")
44+ .bind(password_hash)
45+ .bind(id)
46+ .execute(pool)
47+ .await?;
48+ Ok(())
49+}
50+
51+pub async fn list_all(pool: &PgPool) -> sqlx::Result<Vec<UserRow>> {
52+ sqlx::query_as("SELECT id, username FROM users ORDER BY username")
53+ .fetch_all(pool)
54+ .await
55+}
new file mode 100644
@@ -0,0 +1,55 @@
1+use sqlx::PgPool;
2+use uuid::Uuid;
3+
4+#[derive(sqlx::FromRow)]
5+pub struct UserRow {
6+ pub id: Uuid,
7+ pub username: String,
8+}
9+
10+#[derive(sqlx::FromRow)]
11+pub struct Credentials {
12+ pub id: Uuid,
13+ pub password_hash: String,
14+}
15+
16+pub async fn create(pool: &PgPool, username: &str, password_hash: &str) -> sqlx::Result<Uuid> {
17+ let id = Uuid::now_v7();
18+ sqlx::query("INSERT INTO users (id, username, password_hash) VALUES ($1, $2, $3)")
19+ .bind(id)
20+ .bind(username)
21+ .bind(password_hash)
22+ .execute(pool)
23+ .await?;
24+ Ok(id)
25+}
26+
27+pub async fn find_credentials(pool: &PgPool, username: &str) -> sqlx::Result<Option<Credentials>> {
28+ sqlx::query_as("SELECT id, password_hash FROM users WHERE username = $1")
29+ .bind(username)
30+ .fetch_optional(pool)
31+ .await
32+}
33+
34+pub async fn password_hash(pool: &PgPool, id: Uuid) -> sqlx::Result<Option<String>> {
35+ let row: Option<(String,)> = sqlx::query_as("SELECT password_hash FROM users WHERE id = $1")
36+ .bind(id)
37+ .fetch_optional(pool)
38+ .await?;
39+ Ok(row.map(|(h,)| h))
40+}
41+
42+pub async fn update_password(pool: &PgPool, id: Uuid, password_hash: &str) -> sqlx::Result<()> {
43+ sqlx::query("UPDATE users SET password_hash = $1 WHERE id = $2")
44+ .bind(password_hash)
45+ .bind(id)
46+ .execute(pool)
47+ .await?;
48+ Ok(())
49+}
50+
51+pub async fn list_all(pool: &PgPool) -> sqlx::Result<Vec<UserRow>> {
52+ sqlx::query_as("SELECT id, username FROM users ORDER BY username")
53+ .fetch_all(pool)
54+ .await
55+}
added src/error.rs +48 -0
new file mode 100644
@@ -0,0 +1,48 @@
1+use axum::http::StatusCode;
2+use axum::response::{Html, IntoResponse, Response};
3+
4+#[derive(Debug, thiserror::Error)]
5+pub enum AppError {
6+ #[error("erreur base de données : {0}")]
7+ Db(#[from] sqlx::Error),
8+ #[error("erreur template : {0}")]
9+ Template(#[from] askama::Error),
10+ #[error("introuvable")]
11+ NotFound,
12+ #[error("formulaire invalide : {0}")]
13+ BadRequest(String),
14+ #[error("jeton CSRF invalide")]
15+ Csrf,
16+ #[error("{0}")]
17+ Internal(#[from] anyhow::Error),
18+}
19+
20+impl IntoResponse for AppError {
21+ fn into_response(self) -> Response {
22+ let (status, message) = match &self {
23+ AppError::NotFound => (StatusCode::NOT_FOUND, "Page introuvable.".to_string()),
24+ AppError::BadRequest(m) => (StatusCode::BAD_REQUEST, m.clone()),
25+ AppError::Csrf => (
26+ StatusCode::FORBIDDEN,
27+ "Formulaire expiré, veuillez réessayer.".to_string(),
28+ ),
29+ _ => {
30+ tracing::error!(error = %self, "erreur interne");
31+ (
32+ StatusCode::INTERNAL_SERVER_ERROR,
33+ "Erreur interne.".to_string(),
34+ )
35+ }
36+ };
37+ let body = format!(
38+ r#"<!doctype html><html lang="fr"><head><meta charset="utf-8">
39+<meta name="viewport" content="width=device-width, initial-scale=1">
40+<link rel="stylesheet" href="/static/style.css"><title>{status}</title></head>
41+<body><main class="page"><h1>{code}</h1><p>{message}</p>
42+<p><a class="btn" href="/">← Accueil</a></p></main></body></html>"#,
43+ status = status.as_u16(),
44+ code = status.as_u16(),
45+ );
46+ (status, Html(body)).into_response()
47+ }
48+}
new file mode 100644
@@ -0,0 +1,48 @@
1+use axum::http::StatusCode;
2+use axum::response::{Html, IntoResponse, Response};
3+
4+#[derive(Debug, thiserror::Error)]
5+pub enum AppError {
6+ #[error("erreur base de données : {0}")]
7+ Db(#[from] sqlx::Error),
8+ #[error("erreur template : {0}")]
9+ Template(#[from] askama::Error),
10+ #[error("introuvable")]
11+ NotFound,
12+ #[error("formulaire invalide : {0}")]
13+ BadRequest(String),
14+ #[error("jeton CSRF invalide")]
15+ Csrf,
16+ #[error("{0}")]
17+ Internal(#[from] anyhow::Error),
18+}
19+
20+impl IntoResponse for AppError {
21+ fn into_response(self) -> Response {
22+ let (status, message) = match &self {
23+ AppError::NotFound => (StatusCode::NOT_FOUND, "Page introuvable.".to_string()),
24+ AppError::BadRequest(m) => (StatusCode::BAD_REQUEST, m.clone()),
25+ AppError::Csrf => (
26+ StatusCode::FORBIDDEN,
27+ "Formulaire expiré, veuillez réessayer.".to_string(),
28+ ),
29+ _ => {
30+ tracing::error!(error = %self, "erreur interne");
31+ (
32+ StatusCode::INTERNAL_SERVER_ERROR,
33+ "Erreur interne.".to_string(),
34+ )
35+ }
36+ };
37+ let body = format!(
38+ r#"<!doctype html><html lang="fr"><head><meta charset="utf-8">
39+<meta name="viewport" content="width=device-width, initial-scale=1">
40+<link rel="stylesheet" href="/static/style.css"><title>{status}</title></head>
41+<body><main class="page"><h1>{code}</h1><p>{message}</p>
42+<p><a class="btn" href="/">← Accueil</a></p></main></body></html>"#,
43+ status = status.as_u16(),
44+ code = status.as_u16(),
45+ );
46+ (status, Html(body)).into_response()
47+ }
48+}
added src/main.rs +102 -0
new file mode 100644
@@ -0,0 +1,102 @@
1+mod auth;
2+mod charts;
3+mod cli;
4+mod config;
5+mod db;
6+mod error;
7+mod period;
8+mod render;
9+mod routes;
10+mod state;
11+
12+use anyhow::{Context, Result};
13+use clap::{Parser, Subcommand};
14+use sqlx::postgres::PgPoolOptions;
15+
16+use crate::state::AppState;
17+
18+#[derive(Parser)]
19+#[command(name = "time-time", about = "Comptage de temps passé sur des tâches")]
20+struct Cli {
21+ #[command(subcommand)]
22+ command: Option<Command>,
23+}
24+
25+#[derive(Subcommand)]
26+enum Command {
27+ /// Lance le serveur web (défaut)
28+ Serve,
29+ /// Gestion des utilisateurs
30+ User {
31+ #[command(subcommand)]
32+ command: UserCommand,
33+ },
34+ /// Génère la paire de clés Biscuit racine
35+ Keygen,
36+}
37+
38+#[derive(Subcommand)]
39+enum UserCommand {
40+ /// Crée un utilisateur
41+ Add {
42+ username: String,
43+ #[arg(long)]
44+ password: Option<String>,
45+ },
46+}
47+
48+#[tokio::main]
49+async fn main() -> Result<()> {
50+ let cli = Cli::parse();
51+ match cli.command {
52+ None | Some(Command::Serve) => serve().await,
53+ Some(Command::Keygen) => {
54+ cli::keygen();
55+ Ok(())
56+ }
57+ Some(Command::User {
58+ command: UserCommand::Add { username, password },
59+ }) => cli::user_add(&username, password).await,
60+ }
61+}
62+
63+async fn serve() -> Result<()> {
64+ tracing_subscriber::fmt()
65+ .with_env_filter(
66+ tracing_subscriber::EnvFilter::try_from_default_env()
67+ .unwrap_or_else(|_| "info".into()),
68+ )
69+ .init();
70+
71+ let pool = PgPoolOptions::new()
72+ .max_connections(5)
73+ .connect(&config::database_url()?)
74+ .await
75+ .context("connexion à la base")?;
76+ sqlx::migrate!().run(&pool).await.context("migrations")?;
77+
78+ let state = AppState::new(pool.clone(), &config::biscuit_private_key_hex()?)?;
79+
80+ // Cache de révocation : chargement initial puis rafraîchissement périodique.
81+ auth::revocation::reload(&pool, &state.revocations)
82+ .await
83+ .context("chargement de la liste de révocation")?;
84+ tokio::spawn(auth::revocation::refresher(
85+ pool.clone(),
86+ state.revocations.clone(),
87+ ));
88+
89+ let app = routes::router(state);
90+ let addr = format!("0.0.0.0:{}", config::port());
91+ let listener = tokio::net::TcpListener::bind(&addr)
92+ .await
93+ .with_context(|| format!("bind {addr}"))?;
94+ tracing::info!("écoute sur http://{addr}");
95+
96+ axum::serve(listener, app)
97+ .with_graceful_shutdown(async {
98+ let _ = tokio::signal::ctrl_c().await;
99+ })
100+ .await?;
101+ Ok(())
102+}
new file mode 100644
@@ -0,0 +1,102 @@
1+mod auth;
2+mod charts;
3+mod cli;
4+mod config;
5+mod db;
6+mod error;
7+mod period;
8+mod render;
9+mod routes;
10+mod state;
11+
12+use anyhow::{Context, Result};
13+use clap::{Parser, Subcommand};
14+use sqlx::postgres::PgPoolOptions;
15+
16+use crate::state::AppState;
17+
18+#[derive(Parser)]
19+#[command(name = "time-time", about = "Comptage de temps passé sur des tâches")]
20+struct Cli {
21+ #[command(subcommand)]
22+ command: Option<Command>,
23+}
24+
25+#[derive(Subcommand)]
26+enum Command {
27+ /// Lance le serveur web (défaut)
28+ Serve,
29+ /// Gestion des utilisateurs
30+ User {
31+ #[command(subcommand)]
32+ command: UserCommand,
33+ },
34+ /// Génère la paire de clés Biscuit racine
35+ Keygen,
36+}
37+
38+#[derive(Subcommand)]
39+enum UserCommand {
40+ /// Crée un utilisateur
41+ Add {
42+ username: String,
43+ #[arg(long)]
44+ password: Option<String>,
45+ },
46+}
47+
48+#[tokio::main]
49+async fn main() -> Result<()> {
50+ let cli = Cli::parse();
51+ match cli.command {
52+ None | Some(Command::Serve) => serve().await,
53+ Some(Command::Keygen) => {
54+ cli::keygen();
55+ Ok(())
56+ }
57+ Some(Command::User {
58+ command: UserCommand::Add { username, password },
59+ }) => cli::user_add(&username, password).await,
60+ }
61+}
62+
63+async fn serve() -> Result<()> {
64+ tracing_subscriber::fmt()
65+ .with_env_filter(
66+ tracing_subscriber::EnvFilter::try_from_default_env()
67+ .unwrap_or_else(|_| "info".into()),
68+ )
69+ .init();
70+
71+ let pool = PgPoolOptions::new()
72+ .max_connections(5)
73+ .connect(&config::database_url()?)
74+ .await
75+ .context("connexion à la base")?;
76+ sqlx::migrate!().run(&pool).await.context("migrations")?;
77+
78+ let state = AppState::new(pool.clone(), &config::biscuit_private_key_hex()?)?;
79+
80+ // Cache de révocation : chargement initial puis rafraîchissement périodique.
81+ auth::revocation::reload(&pool, &state.revocations)
82+ .await
83+ .context("chargement de la liste de révocation")?;
84+ tokio::spawn(auth::revocation::refresher(
85+ pool.clone(),
86+ state.revocations.clone(),
87+ ));
88+
89+ let app = routes::router(state);
90+ let addr = format!("0.0.0.0:{}", config::port());
91+ let listener = tokio::net::TcpListener::bind(&addr)
92+ .await
93+ .with_context(|| format!("bind {addr}"))?;
94+ tracing::info!("écoute sur http://{addr}");
95+
96+ axum::serve(listener, app)
97+ .with_graceful_shutdown(async {
98+ let _ = tokio::signal::ctrl_c().await;
99+ })
100+ .await?;
101+ Ok(())
102+}
added src/period.rs +239 -0
new file mode 100644
@@ -0,0 +1,239 @@
1+use std::fmt;
2+
3+use chrono::{Datelike, Duration, NaiveDate, Utc, Weekday};
4+use chrono_tz::Europe::Paris;
5+
6+/// Aujourd'hui en Europe/Paris — seule intrusion de la timezone dans l'appli
7+/// (les entrées sont stockées en DATE).
8+pub fn paris_today() -> NaiveDate {
9+ Utc::now().with_timezone(&Paris).date_naive()
10+}
11+
12+const MONTH_NAMES: [&str; 12] = [
13+ "janvier",
14+ "février",
15+ "mars",
16+ "avril",
17+ "mai",
18+ "juin",
19+ "juillet",
20+ "août",
21+ "septembre",
22+ "octobre",
23+ "novembre",
24+ "décembre",
25+];
26+
27+/// Semaine ISO (lundi → dimanche), format query `2026-W12`.
28+#[derive(Clone, Copy, PartialEq, Eq, Debug)]
29+pub struct Week {
30+ pub year: i32,
31+ pub week: u32,
32+}
33+
34+impl Week {
35+ pub fn parse(s: &str) -> Option<Self> {
36+ let (y, w) = s.split_once("-W")?;
37+ let week = Self {
38+ year: y.parse().ok()?,
39+ week: w.parse().ok()?,
40+ };
41+ // Valide (la semaine 53 n'existe pas toutes les années).
42+ NaiveDate::from_isoywd_opt(week.year, week.week, Weekday::Mon)?;
43+ Some(week)
44+ }
45+
46+ pub fn of(date: NaiveDate) -> Self {
47+ let iso = date.iso_week();
48+ Self {
49+ year: iso.year(),
50+ week: iso.week(),
51+ }
52+ }
53+
54+ pub fn current() -> Self {
55+ Self::of(paris_today())
56+ }
57+
58+ /// [lundi, lundi suivant)
59+ pub fn bounds(&self) -> (NaiveDate, NaiveDate) {
60+ let start = NaiveDate::from_isoywd_opt(self.year, self.week, Weekday::Mon)
61+ .expect("semaine validée au parse");
62+ (start, start + Duration::days(7))
63+ }
64+
65+ pub fn prev(&self) -> Self {
66+ Self::of(self.bounds().0 - Duration::days(7))
67+ }
68+
69+ pub fn next(&self) -> Self {
70+ Self::of(self.bounds().0 + Duration::days(7))
71+ }
72+
73+ /// « Semaine 12 · 16/03 → 22/03 »
74+ pub fn label(&self) -> String {
75+ let (start, end) = self.bounds();
76+ let end = end - Duration::days(1);
77+ format!(
78+ "Semaine {} · {:02}/{:02} → {:02}/{:02}",
79+ self.week,
80+ start.day(),
81+ start.month(),
82+ end.day(),
83+ end.month()
84+ )
85+ }
86+}
87+
88+impl fmt::Display for Week {
89+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90+ write!(f, "{}-W{:02}", self.year, self.week)
91+ }
92+}
93+
94+/// Mois civil, format query `2026-03`.
95+#[derive(Clone, Copy, PartialEq, Eq, Debug)]
96+pub struct Month {
97+ pub year: i32,
98+ pub month: u32,
99+}
100+
101+impl Month {
102+ pub fn parse(s: &str) -> Option<Self> {
103+ let (y, m) = s.split_once('-')?;
104+ let month = Self {
105+ year: y.parse().ok()?,
106+ month: m.parse().ok()?,
107+ };
108+ (1..=12).contains(&month.month).then_some(month)
109+ }
110+
111+ pub fn of(date: NaiveDate) -> Self {
112+ Self {
113+ year: date.year(),
114+ month: date.month(),
115+ }
116+ }
117+
118+ pub fn current() -> Self {
119+ Self::of(paris_today())
120+ }
121+
122+ /// [1er du mois, 1er du mois suivant)
123+ pub fn bounds(&self) -> (NaiveDate, NaiveDate) {
124+ let start =
125+ NaiveDate::from_ymd_opt(self.year, self.month, 1).expect("mois validé au parse");
126+ let end = if self.month == 12 {
127+ NaiveDate::from_ymd_opt(self.year + 1, 1, 1)
128+ } else {
129+ NaiveDate::from_ymd_opt(self.year, self.month + 1, 1)
130+ }
131+ .expect("1er du mois toujours valide");
132+ (start, end)
133+ }
134+
135+ pub fn prev(&self) -> Self {
136+ if self.month == 1 {
137+ Self {
138+ year: self.year - 1,
139+ month: 12,
140+ }
141+ } else {
142+ Self {
143+ year: self.year,
144+ month: self.month - 1,
145+ }
146+ }
147+ }
148+
149+ pub fn next(&self) -> Self {
150+ if self.month == 12 {
151+ Self {
152+ year: self.year + 1,
153+ month: 1,
154+ }
155+ } else {
156+ Self {
157+ year: self.year,
158+ month: self.month + 1,
159+ }
160+ }
161+ }
162+
163+ /// « mars 2026 »
164+ pub fn label(&self) -> String {
165+ format!("{} {}", MONTH_NAMES[(self.month - 1) as usize], self.year)
166+ }
167+}
168+
169+impl fmt::Display for Month {
170+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171+ write!(f, "{}-{:02}", self.year, self.month)
172+ }
173+}
174+
175+#[cfg(test)]
176+mod tests {
177+ use super::*;
178+
179+ fn d(y: i32, m: u32, d: u32) -> NaiveDate {
180+ NaiveDate::from_ymd_opt(y, m, d).unwrap()
181+ }
182+
183+ #[test]
184+ fn week_parse_and_display() {
185+ let w = Week::parse("2026-W12").unwrap();
186+ assert_eq!((w.year, w.week), (2026, 12));
187+ assert_eq!(w.to_string(), "2026-W12");
188+ assert!(Week::parse("2026-W54").is_none());
189+ assert!(Week::parse("n'importe quoi").is_none());
190+ // 2026 n'a que 53 semaines ISO ? Non : 2026 en a 53 ; 2025 en a 52.
191+ assert!(Week::parse("2025-W53").is_none());
192+ assert!(Week::parse("2026-W53").is_some());
193+ }
194+
195+ #[test]
196+ fn week_bounds_monday_based() {
197+ let (start, end) = Week::parse("2026-W12").unwrap().bounds();
198+ assert_eq!(start, d(2026, 3, 16));
199+ assert_eq!(start.weekday(), Weekday::Mon);
200+ assert_eq!(end, d(2026, 3, 23));
201+ }
202+
203+ #[test]
204+ fn week_year_rollover() {
205+ // Le 1er janvier 2026 (jeudi) est en semaine ISO 2026-W01,
206+ // qui commence le lundi 29 décembre 2025.
207+ let w = Week::of(d(2026, 1, 1));
208+ assert_eq!((w.year, w.week), (2026, 1));
209+ assert_eq!(w.bounds().0, d(2025, 12, 29));
210+ // prev traverse l'année.
211+ let p = w.prev();
212+ assert_eq!((p.year, p.week), (2025, 52));
213+ assert_eq!(p.next(), w);
214+ }
215+
216+ #[test]
217+ fn month_parse_bounds_rollover() {
218+ let m = Month::parse("2026-03").unwrap();
219+ assert_eq!(m.bounds(), (d(2026, 3, 1), d(2026, 4, 1)));
220+ assert_eq!(m.to_string(), "2026-03");
221+ assert!(Month::parse("2026-13").is_none());
222+
223+ let dec = Month::parse("2025-12").unwrap();
224+ assert_eq!(dec.bounds().1, d(2026, 1, 1));
225+ assert_eq!(dec.next(), Month::parse("2026-01").unwrap());
226+ assert_eq!(Month::parse("2026-01").unwrap().prev(), dec);
227+ }
228+
229+ #[test]
230+ fn labels() {
231+ assert_eq!(Month::parse("2026-03").unwrap().label(), "mars 2026");
232+ assert!(
233+ Week::parse("2026-W12")
234+ .unwrap()
235+ .label()
236+ .starts_with("Semaine 12")
237+ );
238+ }
239+}
new file mode 100644
@@ -0,0 +1,239 @@
1+use std::fmt;
2+
3+use chrono::{Datelike, Duration, NaiveDate, Utc, Weekday};
4+use chrono_tz::Europe::Paris;
5+
6+/// Aujourd'hui en Europe/Paris — seule intrusion de la timezone dans l'appli
7+/// (les entrées sont stockées en DATE).
8+pub fn paris_today() -> NaiveDate {
9+ Utc::now().with_timezone(&Paris).date_naive()
10+}
11+
12+const MONTH_NAMES: [&str; 12] = [
13+ "janvier",
14+ "février",
15+ "mars",
16+ "avril",
17+ "mai",
18+ "juin",
19+ "juillet",
20+ "août",
21+ "septembre",
22+ "octobre",
23+ "novembre",
24+ "décembre",
25+];
26+
27+/// Semaine ISO (lundi → dimanche), format query `2026-W12`.
28+#[derive(Clone, Copy, PartialEq, Eq, Debug)]
29+pub struct Week {
30+ pub year: i32,
31+ pub week: u32,
32+}
33+
34+impl Week {
35+ pub fn parse(s: &str) -> Option<Self> {
36+ let (y, w) = s.split_once("-W")?;
37+ let week = Self {
38+ year: y.parse().ok()?,
39+ week: w.parse().ok()?,
40+ };
41+ // Valide (la semaine 53 n'existe pas toutes les années).
42+ NaiveDate::from_isoywd_opt(week.year, week.week, Weekday::Mon)?;
43+ Some(week)
44+ }
45+
46+ pub fn of(date: NaiveDate) -> Self {
47+ let iso = date.iso_week();
48+ Self {
49+ year: iso.year(),
50+ week: iso.week(),
51+ }
52+ }
53+
54+ pub fn current() -> Self {
55+ Self::of(paris_today())
56+ }
57+
58+ /// [lundi, lundi suivant)
59+ pub fn bounds(&self) -> (NaiveDate, NaiveDate) {
60+ let start = NaiveDate::from_isoywd_opt(self.year, self.week, Weekday::Mon)
61+ .expect("semaine validée au parse");
62+ (start, start + Duration::days(7))
63+ }
64+
65+ pub fn prev(&self) -> Self {
66+ Self::of(self.bounds().0 - Duration::days(7))
67+ }
68+
69+ pub fn next(&self) -> Self {
70+ Self::of(self.bounds().0 + Duration::days(7))
71+ }
72+
73+ /// « Semaine 12 · 16/03 → 22/03 »
74+ pub fn label(&self) -> String {
75+ let (start, end) = self.bounds();
76+ let end = end - Duration::days(1);
77+ format!(
78+ "Semaine {} · {:02}/{:02} → {:02}/{:02}",
79+ self.week,
80+ start.day(),
81+ start.month(),
82+ end.day(),
83+ end.month()
84+ )
85+ }
86+}
87+
88+impl fmt::Display for Week {
89+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90+ write!(f, "{}-W{:02}", self.year, self.week)
91+ }
92+}
93+
94+/// Mois civil, format query `2026-03`.
95+#[derive(Clone, Copy, PartialEq, Eq, Debug)]
96+pub struct Month {
97+ pub year: i32,
98+ pub month: u32,
99+}
100+
101+impl Month {
102+ pub fn parse(s: &str) -> Option<Self> {
103+ let (y, m) = s.split_once('-')?;
104+ let month = Self {
105+ year: y.parse().ok()?,
106+ month: m.parse().ok()?,
107+ };
108+ (1..=12).contains(&month.month).then_some(month)
109+ }
110+
111+ pub fn of(date: NaiveDate) -> Self {
112+ Self {
113+ year: date.year(),
114+ month: date.month(),
115+ }
116+ }
117+
118+ pub fn current() -> Self {
119+ Self::of(paris_today())
120+ }
121+
122+ /// [1er du mois, 1er du mois suivant)
123+ pub fn bounds(&self) -> (NaiveDate, NaiveDate) {
124+ let start =
125+ NaiveDate::from_ymd_opt(self.year, self.month, 1).expect("mois validé au parse");
126+ let end = if self.month == 12 {
127+ NaiveDate::from_ymd_opt(self.year + 1, 1, 1)
128+ } else {
129+ NaiveDate::from_ymd_opt(self.year, self.month + 1, 1)
130+ }
131+ .expect("1er du mois toujours valide");
132+ (start, end)
133+ }
134+
135+ pub fn prev(&self) -> Self {
136+ if self.month == 1 {
137+ Self {
138+ year: self.year - 1,
139+ month: 12,
140+ }
141+ } else {
142+ Self {
143+ year: self.year,
144+ month: self.month - 1,
145+ }
146+ }
147+ }
148+
149+ pub fn next(&self) -> Self {
150+ if self.month == 12 {
151+ Self {
152+ year: self.year + 1,
153+ month: 1,
154+ }
155+ } else {
156+ Self {
157+ year: self.year,
158+ month: self.month + 1,
159+ }
160+ }
161+ }
162+
163+ /// « mars 2026 »
164+ pub fn label(&self) -> String {
165+ format!("{} {}", MONTH_NAMES[(self.month - 1) as usize], self.year)
166+ }
167+}
168+
169+impl fmt::Display for Month {
170+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171+ write!(f, "{}-{:02}", self.year, self.month)
172+ }
173+}
174+
175+#[cfg(test)]
176+mod tests {
177+ use super::*;
178+
179+ fn d(y: i32, m: u32, d: u32) -> NaiveDate {
180+ NaiveDate::from_ymd_opt(y, m, d).unwrap()
181+ }
182+
183+ #[test]
184+ fn week_parse_and_display() {
185+ let w = Week::parse("2026-W12").unwrap();
186+ assert_eq!((w.year, w.week), (2026, 12));
187+ assert_eq!(w.to_string(), "2026-W12");
188+ assert!(Week::parse("2026-W54").is_none());
189+ assert!(Week::parse("n'importe quoi").is_none());
190+ // 2026 n'a que 53 semaines ISO ? Non : 2026 en a 53 ; 2025 en a 52.
191+ assert!(Week::parse("2025-W53").is_none());
192+ assert!(Week::parse("2026-W53").is_some());
193+ }
194+
195+ #[test]
196+ fn week_bounds_monday_based() {
197+ let (start, end) = Week::parse("2026-W12").unwrap().bounds();
198+ assert_eq!(start, d(2026, 3, 16));
199+ assert_eq!(start.weekday(), Weekday::Mon);
200+ assert_eq!(end, d(2026, 3, 23));
201+ }
202+
203+ #[test]
204+ fn week_year_rollover() {
205+ // Le 1er janvier 2026 (jeudi) est en semaine ISO 2026-W01,
206+ // qui commence le lundi 29 décembre 2025.
207+ let w = Week::of(d(2026, 1, 1));
208+ assert_eq!((w.year, w.week), (2026, 1));
209+ assert_eq!(w.bounds().0, d(2025, 12, 29));
210+ // prev traverse l'année.
211+ let p = w.prev();
212+ assert_eq!((p.year, p.week), (2025, 52));
213+ assert_eq!(p.next(), w);
214+ }
215+
216+ #[test]
217+ fn month_parse_bounds_rollover() {
218+ let m = Month::parse("2026-03").unwrap();
219+ assert_eq!(m.bounds(), (d(2026, 3, 1), d(2026, 4, 1)));
220+ assert_eq!(m.to_string(), "2026-03");
221+ assert!(Month::parse("2026-13").is_none());
222+
223+ let dec = Month::parse("2025-12").unwrap();
224+ assert_eq!(dec.bounds().1, d(2026, 1, 1));
225+ assert_eq!(dec.next(), Month::parse("2026-01").unwrap());
226+ assert_eq!(Month::parse("2026-01").unwrap().prev(), dec);
227+ }
228+
229+ #[test]
230+ fn labels() {
231+ assert_eq!(Month::parse("2026-03").unwrap().label(), "mars 2026");
232+ assert!(
233+ Week::parse("2026-W12")
234+ .unwrap()
235+ .label()
236+ .starts_with("Semaine 12")
237+ );
238+ }
239+}
added src/render.rs +17 -0
new file mode 100644
@@ -0,0 +1,17 @@
1+use askama::Template;
2+use axum::response::{Html, IntoResponse, Response};
3+
4+use crate::error::AppError;
5+
6+/// Wrapper local IntoResponse pour les templates askama
7+/// (askama_axum est abandonné depuis askama 0.13).
8+pub struct HtmlTemplate<T>(pub T);
9+
10+impl<T: Template> IntoResponse for HtmlTemplate<T> {
11+ fn into_response(self) -> Response {
12+ match self.0.render() {
13+ Ok(html) => Html(html).into_response(),
14+ Err(e) => AppError::Template(e).into_response(),
15+ }
16+ }
17+}
new file mode 100644
@@ -0,0 +1,17 @@
1+use askama::Template;
2+use axum::response::{Html, IntoResponse, Response};
3+
4+use crate::error::AppError;
5+
6+/// Wrapper local IntoResponse pour les templates askama
7+/// (askama_axum est abandonné depuis askama 0.13).
8+pub struct HtmlTemplate<T>(pub T);
9+
10+impl<T: Template> IntoResponse for HtmlTemplate<T> {
11+ fn into_response(self) -> Response {
12+ match self.0.render() {
13+ Ok(html) => Html(html).into_response(),
14+ Err(e) => AppError::Template(e).into_response(),
15+ }
16+ }
17+}
added src/routes/entries.rs +254 -0
new file mode 100644
@@ -0,0 +1,254 @@
1+use askama::Template;
2+use axum::Form;
3+use axum::extract::{Path, Query, State};
4+use axum::response::Redirect;
5+use chrono::NaiveDate;
6+use serde::Deserialize;
7+use uuid::Uuid;
8+
9+use crate::auth::extractor::AuthUser;
10+use crate::charts::fmt_minutes;
11+use crate::db::entries::{self, EntryRow};
12+use crate::db::tasks::{self, TaskRow};
13+use crate::error::AppError;
14+use crate::period::paris_today;
15+use crate::render::HtmlTemplate;
16+use crate::state::AppState;
17+
18+pub struct Preset {
19+ pub label: &'static str,
20+ pub href: String,
21+}
22+
23+/// Option du select des tâches, avec sélection précalculée côté Rust.
24+pub struct TaskOption {
25+ pub id: Uuid,
26+ pub name: String,
27+ pub archived: bool,
28+ pub selected: bool,
29+}
30+
31+fn task_options(tasks: Vec<TaskRow>, selected: Option<Uuid>) -> Vec<TaskOption> {
32+ tasks
33+ .into_iter()
34+ .map(|t| TaskOption {
35+ selected: selected == Some(t.id),
36+ id: t.id,
37+ name: t.name,
38+ archived: t.archived,
39+ })
40+ .collect()
41+}
42+
43+#[derive(Template)]
44+#[template(path = "entry_form.html")]
45+pub struct EntryFormPage {
46+ /// Titre + URL cible : partagé entre ajout et édition.
47+ pub title: &'static str,
48+ pub action: String,
49+ pub csrf: String,
50+ pub tasks: Vec<TaskOption>,
51+ pub minutes: Option<i32>,
52+ pub date: NaiveDate,
53+ pub presets: Vec<Preset>,
54+ pub saved: bool,
55+ /// Some(id) en édition : affiche le bouton Supprimer.
56+ pub delete_action: Option<String>,
57+}
58+
59+#[derive(Deserialize)]
60+pub struct NewQuery {
61+ minutes: Option<i32>,
62+ tache: Option<Uuid>,
63+ date: Option<NaiveDate>,
64+ ok: Option<u8>,
65+}
66+
67+fn presets(date: NaiveDate, tache: Option<Uuid>) -> Vec<Preset> {
68+ [15, 30, 45, 60]
69+ .into_iter()
70+ .map(|m| {
71+ let mut href = format!("/temps/nouveau?minutes={m}&date={date}");
72+ if let Some(t) = tache {
73+ href.push_str(&format!("&tache={t}"));
74+ }
75+ Preset {
76+ label: match m {
77+ 15 => "15 min",
78+ 30 => "30 min",
79+ 45 => "45 min",
80+ _ => "1 h",
81+ },
82+ href,
83+ }
84+ })
85+ .collect()
86+}
87+
88+pub async fn entry_new(
89+ State(state): State<AppState>,
90+ auth: AuthUser,
91+ Query(q): Query<NewQuery>,
92+) -> Result<HtmlTemplate<EntryFormPage>, AppError> {
93+ let date = q.date.unwrap_or_else(paris_today);
94+ Ok(HtmlTemplate(EntryFormPage {
95+ title: "Ajouter un temps",
96+ action: "/temps".to_string(),
97+ csrf: auth.csrf_token(&state),
98+ tasks: task_options(tasks::list_active(&state.pool).await?, q.tache),
99+ minutes: q.minutes,
100+ date,
101+ presets: presets(date, q.tache),
102+ saved: q.ok.is_some(),
103+ delete_action: None,
104+ }))
105+}
106+
107+#[derive(Deserialize)]
108+pub struct EntryForm {
109+ minutes: i32,
110+ tache: Uuid,
111+ date: NaiveDate,
112+ csrf: String,
113+}
114+
115+fn validate(form: &EntryForm) -> Result<(), AppError> {
116+ if !(1..=1440).contains(&form.minutes) {
117+ return Err(AppError::BadRequest(
118+ "La durée doit être comprise entre 1 et 1440 minutes.".into(),
119+ ));
120+ }
121+ Ok(())
122+}
123+
124+pub async fn entry_create(
125+ State(state): State<AppState>,
126+ auth: AuthUser,
127+ Form(form): Form<EntryForm>,
128+) -> Result<Redirect, AppError> {
129+ if !auth.check_csrf(&state, &form.csrf) {
130+ return Err(AppError::Csrf);
131+ }
132+ validate(&form)?;
133+ entries::insert(&state.pool, auth.id, form.tache, form.minutes, form.date).await?;
134+ Ok(Redirect::to("/temps/nouveau?ok=1"))
135+}
136+
137+#[derive(Template)]
138+#[template(path = "entries_list.html")]
139+pub struct EntriesListPage {
140+ pub entries: Vec<EntryView>,
141+ pub page: i64,
142+ pub has_next: bool,
143+}
144+
145+pub struct EntryView {
146+ pub id: Uuid,
147+ pub task_name: String,
148+ pub duration: String,
149+ pub date: String,
150+}
151+
152+#[derive(Deserialize)]
153+pub struct PageQuery {
154+ page: Option<i64>,
155+}
156+
157+pub async fn entries_list(
158+ State(state): State<AppState>,
159+ auth: AuthUser,
160+ Query(q): Query<PageQuery>,
161+) -> Result<HtmlTemplate<EntriesListPage>, AppError> {
162+ let page = q.page.unwrap_or(1).max(1);
163+ let (rows, has_next) = entries::list_page(&state.pool, auth.id, page).await?;
164+ let entries = rows.into_iter().map(entry_view).collect();
165+ Ok(HtmlTemplate(EntriesListPage {
166+ entries,
167+ page,
168+ has_next,
169+ }))
170+}
171+
172+fn entry_view(e: EntryRow) -> EntryView {
173+ EntryView {
174+ id: e.id,
175+ task_name: e.task_name,
176+ duration: fmt_minutes(e.minutes as i64),
177+ date: e.entry_date.format("%d/%m/%Y").to_string(),
178+ }
179+}
180+
181+pub async fn entry_edit(
182+ State(state): State<AppState>,
183+ auth: AuthUser,
184+ Path(id): Path<Uuid>,
185+) -> Result<HtmlTemplate<EntryFormPage>, AppError> {
186+ let entry = entries::get_owned(&state.pool, id, auth.id)
187+ .await?
188+ .ok_or(AppError::NotFound)?;
189+ // En édition on liste aussi la tâche de l'entrée même si archivée depuis.
190+ let mut task_list = tasks::list_active(&state.pool).await?;
191+ if !task_list.iter().any(|t| t.id == entry.task_id) {
192+ task_list.push(TaskRow {
193+ id: entry.task_id,
194+ name: entry.task_name.clone(),
195+ archived: true,
196+ });
197+ }
198+ Ok(HtmlTemplate(EntryFormPage {
199+ title: "Modifier un temps",
200+ action: format!("/temps/{id}"),
201+ csrf: auth.csrf_token(&state),
202+ tasks: task_options(task_list, Some(entry.task_id)),
203+ minutes: Some(entry.minutes),
204+ date: entry.entry_date,
205+ presets: Vec::new(),
206+ saved: false,
207+ delete_action: Some(format!("/temps/{id}/supprimer")),
208+ }))
209+}
210+
211+pub async fn entry_update(
212+ State(state): State<AppState>,
213+ auth: AuthUser,
214+ Path(id): Path<Uuid>,
215+ Form(form): Form<EntryForm>,
216+) -> Result<Redirect, AppError> {
217+ if !auth.check_csrf(&state, &form.csrf) {
218+ return Err(AppError::Csrf);
219+ }
220+ validate(&form)?;
221+ let n = entries::update_owned(
222+ &state.pool,
223+ id,
224+ auth.id,
225+ form.tache,
226+ form.minutes,
227+ form.date,
228+ )
229+ .await?;
230+ if n == 0 {
231+ return Err(AppError::NotFound);
232+ }
233+ Ok(Redirect::to("/temps"))
234+}
235+
236+#[derive(Deserialize)]
237+pub struct CsrfOnly {
238+ csrf: String,
239+}
240+
241+pub async fn entry_delete(
242+ State(state): State<AppState>,
243+ auth: AuthUser,
244+ Path(id): Path<Uuid>,
245+ Form(form): Form<CsrfOnly>,
246+) -> Result<Redirect, AppError> {
247+ if !auth.check_csrf(&state, &form.csrf) {
248+ return Err(AppError::Csrf);
249+ }
250+ if entries::delete_owned(&state.pool, id, auth.id).await? == 0 {
251+ return Err(AppError::NotFound);
252+ }
253+ Ok(Redirect::to("/temps"))
254+}
new file mode 100644
@@ -0,0 +1,254 @@
1+use askama::Template;
2+use axum::Form;
3+use axum::extract::{Path, Query, State};
4+use axum::response::Redirect;
5+use chrono::NaiveDate;
6+use serde::Deserialize;
7+use uuid::Uuid;
8+
9+use crate::auth::extractor::AuthUser;
10+use crate::charts::fmt_minutes;
11+use crate::db::entries::{self, EntryRow};
12+use crate::db::tasks::{self, TaskRow};
13+use crate::error::AppError;
14+use crate::period::paris_today;
15+use crate::render::HtmlTemplate;
16+use crate::state::AppState;
17+
18+pub struct Preset {
19+ pub label: &'static str,
20+ pub href: String,
21+}
22+
23+/// Option du select des tâches, avec sélection précalculée côté Rust.
24+pub struct TaskOption {
25+ pub id: Uuid,
26+ pub name: String,
27+ pub archived: bool,
28+ pub selected: bool,
29+}
30+
31+fn task_options(tasks: Vec<TaskRow>, selected: Option<Uuid>) -> Vec<TaskOption> {
32+ tasks
33+ .into_iter()
34+ .map(|t| TaskOption {
35+ selected: selected == Some(t.id),
36+ id: t.id,
37+ name: t.name,
38+ archived: t.archived,
39+ })
40+ .collect()
41+}
42+
43+#[derive(Template)]
44+#[template(path = "entry_form.html")]
45+pub struct EntryFormPage {
46+ /// Titre + URL cible : partagé entre ajout et édition.
47+ pub title: &'static str,
48+ pub action: String,
49+ pub csrf: String,
50+ pub tasks: Vec<TaskOption>,
51+ pub minutes: Option<i32>,
52+ pub date: NaiveDate,
53+ pub presets: Vec<Preset>,
54+ pub saved: bool,
55+ /// Some(id) en édition : affiche le bouton Supprimer.
56+ pub delete_action: Option<String>,
57+}
58+
59+#[derive(Deserialize)]
60+pub struct NewQuery {
61+ minutes: Option<i32>,
62+ tache: Option<Uuid>,
63+ date: Option<NaiveDate>,
64+ ok: Option<u8>,
65+}
66+
67+fn presets(date: NaiveDate, tache: Option<Uuid>) -> Vec<Preset> {
68+ [15, 30, 45, 60]
69+ .into_iter()
70+ .map(|m| {
71+ let mut href = format!("/temps/nouveau?minutes={m}&date={date}");
72+ if let Some(t) = tache {
73+ href.push_str(&format!("&tache={t}"));
74+ }
75+ Preset {
76+ label: match m {
77+ 15 => "15 min",
78+ 30 => "30 min",
79+ 45 => "45 min",
80+ _ => "1 h",
81+ },
82+ href,
83+ }
84+ })
85+ .collect()
86+}
87+
88+pub async fn entry_new(
89+ State(state): State<AppState>,
90+ auth: AuthUser,
91+ Query(q): Query<NewQuery>,
92+) -> Result<HtmlTemplate<EntryFormPage>, AppError> {
93+ let date = q.date.unwrap_or_else(paris_today);
94+ Ok(HtmlTemplate(EntryFormPage {
95+ title: "Ajouter un temps",
96+ action: "/temps".to_string(),
97+ csrf: auth.csrf_token(&state),
98+ tasks: task_options(tasks::list_active(&state.pool).await?, q.tache),
99+ minutes: q.minutes,
100+ date,
101+ presets: presets(date, q.tache),
102+ saved: q.ok.is_some(),
103+ delete_action: None,
104+ }))
105+}
106+
107+#[derive(Deserialize)]
108+pub struct EntryForm {
109+ minutes: i32,
110+ tache: Uuid,
111+ date: NaiveDate,
112+ csrf: String,
113+}
114+
115+fn validate(form: &EntryForm) -> Result<(), AppError> {
116+ if !(1..=1440).contains(&form.minutes) {
117+ return Err(AppError::BadRequest(
118+ "La durée doit être comprise entre 1 et 1440 minutes.".into(),
119+ ));
120+ }
121+ Ok(())
122+}
123+
124+pub async fn entry_create(
125+ State(state): State<AppState>,
126+ auth: AuthUser,
127+ Form(form): Form<EntryForm>,
128+) -> Result<Redirect, AppError> {
129+ if !auth.check_csrf(&state, &form.csrf) {
130+ return Err(AppError::Csrf);
131+ }
132+ validate(&form)?;
133+ entries::insert(&state.pool, auth.id, form.tache, form.minutes, form.date).await?;
134+ Ok(Redirect::to("/temps/nouveau?ok=1"))
135+}
136+
137+#[derive(Template)]
138+#[template(path = "entries_list.html")]
139+pub struct EntriesListPage {
140+ pub entries: Vec<EntryView>,
141+ pub page: i64,
142+ pub has_next: bool,
143+}
144+
145+pub struct EntryView {
146+ pub id: Uuid,
147+ pub task_name: String,
148+ pub duration: String,
149+ pub date: String,
150+}
151+
152+#[derive(Deserialize)]
153+pub struct PageQuery {
154+ page: Option<i64>,
155+}
156+
157+pub async fn entries_list(
158+ State(state): State<AppState>,
159+ auth: AuthUser,
160+ Query(q): Query<PageQuery>,
161+) -> Result<HtmlTemplate<EntriesListPage>, AppError> {
162+ let page = q.page.unwrap_or(1).max(1);
163+ let (rows, has_next) = entries::list_page(&state.pool, auth.id, page).await?;
164+ let entries = rows.into_iter().map(entry_view).collect();
165+ Ok(HtmlTemplate(EntriesListPage {
166+ entries,
167+ page,
168+ has_next,
169+ }))
170+}
171+
172+fn entry_view(e: EntryRow) -> EntryView {
173+ EntryView {
174+ id: e.id,
175+ task_name: e.task_name,
176+ duration: fmt_minutes(e.minutes as i64),
177+ date: e.entry_date.format("%d/%m/%Y").to_string(),
178+ }
179+}
180+
181+pub async fn entry_edit(
182+ State(state): State<AppState>,
183+ auth: AuthUser,
184+ Path(id): Path<Uuid>,
185+) -> Result<HtmlTemplate<EntryFormPage>, AppError> {
186+ let entry = entries::get_owned(&state.pool, id, auth.id)
187+ .await?
188+ .ok_or(AppError::NotFound)?;
189+ // En édition on liste aussi la tâche de l'entrée même si archivée depuis.
190+ let mut task_list = tasks::list_active(&state.pool).await?;
191+ if !task_list.iter().any(|t| t.id == entry.task_id) {
192+ task_list.push(TaskRow {
193+ id: entry.task_id,
194+ name: entry.task_name.clone(),
195+ archived: true,
196+ });
197+ }
198+ Ok(HtmlTemplate(EntryFormPage {
199+ title: "Modifier un temps",
200+ action: format!("/temps/{id}"),
201+ csrf: auth.csrf_token(&state),
202+ tasks: task_options(task_list, Some(entry.task_id)),
203+ minutes: Some(entry.minutes),
204+ date: entry.entry_date,
205+ presets: Vec::new(),
206+ saved: false,
207+ delete_action: Some(format!("/temps/{id}/supprimer")),
208+ }))
209+}
210+
211+pub async fn entry_update(
212+ State(state): State<AppState>,
213+ auth: AuthUser,
214+ Path(id): Path<Uuid>,
215+ Form(form): Form<EntryForm>,
216+) -> Result<Redirect, AppError> {
217+ if !auth.check_csrf(&state, &form.csrf) {
218+ return Err(AppError::Csrf);
219+ }
220+ validate(&form)?;
221+ let n = entries::update_owned(
222+ &state.pool,
223+ id,
224+ auth.id,
225+ form.tache,
226+ form.minutes,
227+ form.date,
228+ )
229+ .await?;
230+ if n == 0 {
231+ return Err(AppError::NotFound);
232+ }
233+ Ok(Redirect::to("/temps"))
234+}
235+
236+#[derive(Deserialize)]
237+pub struct CsrfOnly {
238+ csrf: String,
239+}
240+
241+pub async fn entry_delete(
242+ State(state): State<AppState>,
243+ auth: AuthUser,
244+ Path(id): Path<Uuid>,
245+ Form(form): Form<CsrfOnly>,
246+) -> Result<Redirect, AppError> {
247+ if !auth.check_csrf(&state, &form.csrf) {
248+ return Err(AppError::Csrf);
249+ }
250+ if entries::delete_owned(&state.pool, id, auth.id).await? == 0 {
251+ return Err(AppError::NotFound);
252+ }
253+ Ok(Redirect::to("/temps"))
254+}
added src/routes/home.rs +12 -0
new file mode 100644
@@ -0,0 +1,12 @@
1+use askama::Template;
2+
3+use crate::auth::extractor::AuthUser;
4+use crate::render::HtmlTemplate;
5+
6+#[derive(Template)]
7+#[template(path = "home.html")]
8+pub struct HomePage;
9+
10+pub async fn home(_auth: AuthUser) -> HtmlTemplate<HomePage> {
11+ HtmlTemplate(HomePage)
12+}
new file mode 100644
@@ -0,0 +1,12 @@
1+use askama::Template;
2+
3+use crate::auth::extractor::AuthUser;
4+use crate::render::HtmlTemplate;
5+
6+#[derive(Template)]
7+#[template(path = "home.html")]
8+pub struct HomePage;
9+
10+pub async fn home(_auth: AuthUser) -> HtmlTemplate<HomePage> {
11+ HtmlTemplate(HomePage)
12+}
added src/routes/login.rs +92 -0
new file mode 100644
@@ -0,0 +1,92 @@
1+use askama::Template;
2+use axum::extract::{Query, State};
3+use axum::response::{IntoResponse, Redirect, Response};
4+use axum::Form;
5+use axum_extra::extract::CookieJar;
6+use axum_extra::extract::cookie::{Cookie, SameSite};
7+use serde::Deserialize;
8+
9+use crate::auth::extractor::AuthUser;
10+use crate::auth::{SESSION_COOKIE, SESSION_SECONDS, revocation, token};
11+use crate::db::users;
12+use crate::error::AppError;
13+use crate::render::HtmlTemplate;
14+use crate::state::AppState;
15+
16+#[derive(Template)]
17+#[template(path = "login.html")]
18+pub struct LoginPage {
19+ pub error: bool,
20+}
21+
22+#[derive(Deserialize)]
23+pub struct LoginQuery {
24+ #[serde(default)]
25+ err: Option<u8>,
26+}
27+
28+pub async fn login_page(user: Option<AuthUser>, Query(q): Query<LoginQuery>) -> Response {
29+ if user.is_some() {
30+ return Redirect::to("/").into_response();
31+ }
32+ HtmlTemplate(LoginPage {
33+ error: q.err.is_some(),
34+ })
35+ .into_response()
36+}
37+
38+#[derive(Deserialize)]
39+pub struct LoginForm {
40+ username: String,
41+ password: String,
42+}
43+
44+pub async fn login_submit(
45+ State(state): State<AppState>,
46+ jar: CookieJar,
47+ Form(form): Form<LoginForm>,
48+) -> Result<Response, AppError> {
49+ let creds = users::find_credentials(&state.pool, form.username.trim()).await?;
50+ let Some(creds) = creds else {
51+ // Vérification factice pour ne pas révéler l'existence du compte via le timing.
52+ let _ = crate::auth::password::verify(&form.password, &DUMMY_HASH);
53+ return Ok(Redirect::to("/login?err=1").into_response());
54+ };
55+ if !crate::auth::password::verify(&form.password, &creds.password_hash) {
56+ return Ok(Redirect::to("/login?err=1").into_response());
57+ }
58+
59+ let token = token::mint(&state.root, creds.id).map_err(AppError::Internal)?;
60+ let cookie = Cookie::build((SESSION_COOKIE, token))
61+ .path("/")
62+ .http_only(true)
63+ .secure(true)
64+ .same_site(SameSite::Lax)
65+ .max_age(time::Duration::seconds(SESSION_SECONDS));
66+ Ok((jar.add(cookie), Redirect::to("/")).into_response())
67+}
68+
69+/// Hash argon2id factice — uniquement pour égaliser le temps de réponse.
70+static DUMMY_HASH: std::sync::LazyLock<String> =
71+ std::sync::LazyLock::new(|| crate::auth::password::hash("dummy").expect("hash factice"));
72+
73+#[derive(Deserialize)]
74+pub struct LogoutForm {
75+ csrf: String,
76+}
77+
78+pub async fn logout(
79+ State(state): State<AppState>,
80+ auth: AuthUser,
81+ jar: CookieJar,
82+ Form(form): Form<LogoutForm>,
83+) -> Result<Response, AppError> {
84+ if !auth.check_csrf(&state, &form.csrf) {
85+ return Err(AppError::Csrf);
86+ }
87+ revocation::revoke(&state.pool, &state.revocations, &auth.rev_ids)
88+ .await
89+ .map_err(AppError::Internal)?;
90+ let jar = jar.remove(Cookie::build((SESSION_COOKIE, "")).path("/"));
91+ Ok((jar, Redirect::to("/login")).into_response())
92+}
new file mode 100644
@@ -0,0 +1,92 @@
1+use askama::Template;
2+use axum::extract::{Query, State};
3+use axum::response::{IntoResponse, Redirect, Response};
4+use axum::Form;
5+use axum_extra::extract::CookieJar;
6+use axum_extra::extract::cookie::{Cookie, SameSite};
7+use serde::Deserialize;
8+
9+use crate::auth::extractor::AuthUser;
10+use crate::auth::{SESSION_COOKIE, SESSION_SECONDS, revocation, token};
11+use crate::db::users;
12+use crate::error::AppError;
13+use crate::render::HtmlTemplate;
14+use crate::state::AppState;
15+
16+#[derive(Template)]
17+#[template(path = "login.html")]
18+pub struct LoginPage {
19+ pub error: bool,
20+}
21+
22+#[derive(Deserialize)]
23+pub struct LoginQuery {
24+ #[serde(default)]
25+ err: Option<u8>,
26+}
27+
28+pub async fn login_page(user: Option<AuthUser>, Query(q): Query<LoginQuery>) -> Response {
29+ if user.is_some() {
30+ return Redirect::to("/").into_response();
31+ }
32+ HtmlTemplate(LoginPage {
33+ error: q.err.is_some(),
34+ })
35+ .into_response()
36+}
37+
38+#[derive(Deserialize)]
39+pub struct LoginForm {
40+ username: String,
41+ password: String,
42+}
43+
44+pub async fn login_submit(
45+ State(state): State<AppState>,
46+ jar: CookieJar,
47+ Form(form): Form<LoginForm>,
48+) -> Result<Response, AppError> {
49+ let creds = users::find_credentials(&state.pool, form.username.trim()).await?;
50+ let Some(creds) = creds else {
51+ // Vérification factice pour ne pas révéler l'existence du compte via le timing.
52+ let _ = crate::auth::password::verify(&form.password, &DUMMY_HASH);
53+ return Ok(Redirect::to("/login?err=1").into_response());
54+ };
55+ if !crate::auth::password::verify(&form.password, &creds.password_hash) {
56+ return Ok(Redirect::to("/login?err=1").into_response());
57+ }
58+
59+ let token = token::mint(&state.root, creds.id).map_err(AppError::Internal)?;
60+ let cookie = Cookie::build((SESSION_COOKIE, token))
61+ .path("/")
62+ .http_only(true)
63+ .secure(true)
64+ .same_site(SameSite::Lax)
65+ .max_age(time::Duration::seconds(SESSION_SECONDS));
66+ Ok((jar.add(cookie), Redirect::to("/")).into_response())
67+}
68+
69+/// Hash argon2id factice — uniquement pour égaliser le temps de réponse.
70+static DUMMY_HASH: std::sync::LazyLock<String> =
71+ std::sync::LazyLock::new(|| crate::auth::password::hash("dummy").expect("hash factice"));
72+
73+#[derive(Deserialize)]
74+pub struct LogoutForm {
75+ csrf: String,
76+}
77+
78+pub async fn logout(
79+ State(state): State<AppState>,
80+ auth: AuthUser,
81+ jar: CookieJar,
82+ Form(form): Form<LogoutForm>,
83+) -> Result<Response, AppError> {
84+ if !auth.check_csrf(&state, &form.csrf) {
85+ return Err(AppError::Csrf);
86+ }
87+ revocation::revoke(&state.pool, &state.revocations, &auth.rev_ids)
88+ .await
89+ .map_err(AppError::Internal)?;
90+ let jar = jar.remove(Cookie::build((SESSION_COOKIE, "")).path("/"));
91+ Ok((jar, Redirect::to("/login")).into_response())
92+}
added src/routes/mod.rs +47 -0
new file mode 100644
@@ -0,0 +1,47 @@
1+mod entries;
2+mod home;
3+mod login;
4+mod settings;
5+mod statics;
6+mod stats;
7+
8+use axum::Router;
9+use axum::routing::{get, post};
10+
11+use crate::state::AppState;
12+
13+pub fn router(state: AppState) -> Router {
14+ Router::new()
15+ .route("/", get(home::home))
16+ .route("/login", get(login::login_page).post(login::login_submit))
17+ .route("/logout", post(login::logout))
18+ .route(
19+ "/temps",
20+ get(entries::entries_list).post(entries::entry_create),
21+ )
22+ .route("/temps/nouveau", get(entries::entry_new))
23+ .route("/temps/{id}/modifier", get(entries::entry_edit))
24+ .route("/temps/{id}", post(entries::entry_update))
25+ .route("/temps/{id}/supprimer", post(entries::entry_delete))
26+ .route("/stats", get(stats::stats_page))
27+ .route("/parametres", get(settings::settings_page))
28+ .route("/parametres/taches", post(settings::task_create))
29+ .route("/parametres/taches/{id}", post(settings::task_rename))
30+ .route(
31+ "/parametres/taches/{id}/archiver",
32+ post(settings::task_toggle_archive),
33+ )
34+ .route(
35+ "/parametres/mot-de-passe",
36+ post(settings::password_change),
37+ )
38+ .route("/static/style.css", get(statics::css))
39+ .route("/manifest.json", get(statics::manifest))
40+ .route("/icon-512.png", get(statics::icon))
41+ .fallback(fallback_404)
42+ .with_state(state)
43+}
44+
45+async fn fallback_404() -> crate::error::AppError {
46+ crate::error::AppError::NotFound
47+}
new file mode 100644
@@ -0,0 +1,47 @@
1+mod entries;
2+mod home;
3+mod login;
4+mod settings;
5+mod statics;
6+mod stats;
7+
8+use axum::Router;
9+use axum::routing::{get, post};
10+
11+use crate::state::AppState;
12+
13+pub fn router(state: AppState) -> Router {
14+ Router::new()
15+ .route("/", get(home::home))
16+ .route("/login", get(login::login_page).post(login::login_submit))
17+ .route("/logout", post(login::logout))
18+ .route(
19+ "/temps",
20+ get(entries::entries_list).post(entries::entry_create),
21+ )
22+ .route("/temps/nouveau", get(entries::entry_new))
23+ .route("/temps/{id}/modifier", get(entries::entry_edit))
24+ .route("/temps/{id}", post(entries::entry_update))
25+ .route("/temps/{id}/supprimer", post(entries::entry_delete))
26+ .route("/stats", get(stats::stats_page))
27+ .route("/parametres", get(settings::settings_page))
28+ .route("/parametres/taches", post(settings::task_create))
29+ .route("/parametres/taches/{id}", post(settings::task_rename))
30+ .route(
31+ "/parametres/taches/{id}/archiver",
32+ post(settings::task_toggle_archive),
33+ )
34+ .route(
35+ "/parametres/mot-de-passe",
36+ post(settings::password_change),
37+ )
38+ .route("/static/style.css", get(statics::css))
39+ .route("/manifest.json", get(statics::manifest))
40+ .route("/icon-512.png", get(statics::icon))
41+ .fallback(fallback_404)
42+ .with_state(state)
43+}
44+
45+async fn fallback_404() -> crate::error::AppError {
46+ crate::error::AppError::NotFound
47+}
added src/routes/settings.rs +158 -0
new file mode 100644
@@ -0,0 +1,158 @@
1+use askama::Template;
2+use axum::Form;
3+use axum::extract::{Path, Query, State};
4+use axum::response::Redirect;
5+use serde::Deserialize;
6+use uuid::Uuid;
7+
8+use crate::auth::extractor::AuthUser;
9+use crate::auth::password;
10+use crate::db::{tasks, users};
11+use crate::db::tasks::TaskRow;
12+use crate::error::AppError;
13+use crate::render::HtmlTemplate;
14+use crate::state::AppState;
15+
16+#[derive(Template)]
17+#[template(path = "settings.html")]
18+pub struct SettingsPage {
19+ pub tasks: Vec<TaskRow>,
20+ pub csrf: String,
21+ pub notice: Option<&'static str>,
22+ pub error: Option<&'static str>,
23+}
24+
25+#[derive(Deserialize)]
26+pub struct SettingsQuery {
27+ ok: Option<String>,
28+ err: Option<String>,
29+}
30+
31+pub async fn settings_page(
32+ State(state): State<AppState>,
33+ auth: AuthUser,
34+ Query(q): Query<SettingsQuery>,
35+) -> Result<HtmlTemplate<SettingsPage>, AppError> {
36+ let notice = match q.ok.as_deref() {
37+ Some("mdp") => Some("Mot de passe modifié."),
38+ Some("tache") => Some("Tâche enregistrée."),
39+ _ => None,
40+ };
41+ let error = match q.err.as_deref() {
42+ Some("mdp") => Some("Ancien mot de passe incorrect."),
43+ Some("confirm") => Some("La confirmation ne correspond pas."),
44+ Some("court") => Some("Le nouveau mot de passe doit faire au moins 8 caractères."),
45+ Some("doublon") => Some("Une tâche porte déjà ce nom."),
46+ Some("vide") => Some("Le nom de la tâche ne peut pas être vide."),
47+ _ => None,
48+ };
49+ Ok(HtmlTemplate(SettingsPage {
50+ tasks: tasks::list_all(&state.pool).await?,
51+ csrf: auth.csrf_token(&state),
52+ notice,
53+ error,
54+ }))
55+}
56+
57+#[derive(Deserialize)]
58+pub struct TaskForm {
59+ name: String,
60+ csrf: String,
61+}
62+
63+fn is_unique_violation(e: &sqlx::Error) -> bool {
64+ matches!(e, sqlx::Error::Database(db) if db.is_unique_violation())
65+}
66+
67+pub async fn task_create(
68+ State(state): State<AppState>,
69+ auth: AuthUser,
70+ Form(form): Form<TaskForm>,
71+) -> Result<Redirect, AppError> {
72+ if !auth.check_csrf(&state, &form.csrf) {
73+ return Err(AppError::Csrf);
74+ }
75+ let name = form.name.trim();
76+ if name.is_empty() {
77+ return Ok(Redirect::to("/parametres?err=vide"));
78+ }
79+ match tasks::create(&state.pool, name).await {
80+ Ok(_) => Ok(Redirect::to("/parametres?ok=tache")),
81+ Err(e) if is_unique_violation(&e) => Ok(Redirect::to("/parametres?err=doublon")),
82+ Err(e) => Err(e.into()),
83+ }
84+}
85+
86+pub async fn task_rename(
87+ State(state): State<AppState>,
88+ auth: AuthUser,
89+ Path(id): Path<Uuid>,
90+ Form(form): Form<TaskForm>,
91+) -> Result<Redirect, AppError> {
92+ if !auth.check_csrf(&state, &form.csrf) {
93+ return Err(AppError::Csrf);
94+ }
95+ let name = form.name.trim();
96+ if name.is_empty() {
97+ return Ok(Redirect::to("/parametres?err=vide"));
98+ }
99+ match tasks::rename(&state.pool, id, name).await {
100+ Ok(0) => Err(AppError::NotFound),
101+ Ok(_) => Ok(Redirect::to("/parametres?ok=tache")),
102+ Err(e) if is_unique_violation(&e) => Ok(Redirect::to("/parametres?err=doublon")),
103+ Err(e) => Err(e.into()),
104+ }
105+}
106+
107+#[derive(Deserialize)]
108+pub struct CsrfOnly {
109+ csrf: String,
110+}
111+
112+pub async fn task_toggle_archive(
113+ State(state): State<AppState>,
114+ auth: AuthUser,
115+ Path(id): Path<Uuid>,
116+ Form(form): Form<CsrfOnly>,
117+) -> Result<Redirect, AppError> {
118+ if !auth.check_csrf(&state, &form.csrf) {
119+ return Err(AppError::Csrf);
120+ }
121+ if tasks::toggle_archived(&state.pool, id).await? == 0 {
122+ return Err(AppError::NotFound);
123+ }
124+ Ok(Redirect::to("/parametres?ok=tache"))
125+}
126+
127+#[derive(Deserialize)]
128+pub struct PasswordForm {
129+ ancien: String,
130+ nouveau: String,
131+ confirmation: String,
132+ csrf: String,
133+}
134+
135+pub async fn password_change(
136+ State(state): State<AppState>,
137+ auth: AuthUser,
138+ Form(form): Form<PasswordForm>,
139+) -> Result<Redirect, AppError> {
140+ if !auth.check_csrf(&state, &form.csrf) {
141+ return Err(AppError::Csrf);
142+ }
143+ if form.nouveau != form.confirmation {
144+ return Ok(Redirect::to("/parametres?err=confirm"));
145+ }
146+ if form.nouveau.len() < 8 {
147+ return Ok(Redirect::to("/parametres?err=court"));
148+ }
149+ let current = users::password_hash(&state.pool, auth.id)
150+ .await?
151+ .ok_or(AppError::NotFound)?;
152+ if !password::verify(&form.ancien, &current) {
153+ return Ok(Redirect::to("/parametres?err=mdp"));
154+ }
155+ let new_hash = password::hash(&form.nouveau).map_err(AppError::Internal)?;
156+ users::update_password(&state.pool, auth.id, &new_hash).await?;
157+ Ok(Redirect::to("/parametres?ok=mdp"))
158+}
new file mode 100644
@@ -0,0 +1,158 @@
1+use askama::Template;
2+use axum::Form;
3+use axum::extract::{Path, Query, State};
4+use axum::response::Redirect;
5+use serde::Deserialize;
6+use uuid::Uuid;
7+
8+use crate::auth::extractor::AuthUser;
9+use crate::auth::password;
10+use crate::db::{tasks, users};
11+use crate::db::tasks::TaskRow;
12+use crate::error::AppError;
13+use crate::render::HtmlTemplate;
14+use crate::state::AppState;
15+
16+#[derive(Template)]
17+#[template(path = "settings.html")]
18+pub struct SettingsPage {
19+ pub tasks: Vec<TaskRow>,
20+ pub csrf: String,
21+ pub notice: Option<&'static str>,
22+ pub error: Option<&'static str>,
23+}
24+
25+#[derive(Deserialize)]
26+pub struct SettingsQuery {
27+ ok: Option<String>,
28+ err: Option<String>,
29+}
30+
31+pub async fn settings_page(
32+ State(state): State<AppState>,
33+ auth: AuthUser,
34+ Query(q): Query<SettingsQuery>,
35+) -> Result<HtmlTemplate<SettingsPage>, AppError> {
36+ let notice = match q.ok.as_deref() {
37+ Some("mdp") => Some("Mot de passe modifié."),
38+ Some("tache") => Some("Tâche enregistrée."),
39+ _ => None,
40+ };
41+ let error = match q.err.as_deref() {
42+ Some("mdp") => Some("Ancien mot de passe incorrect."),
43+ Some("confirm") => Some("La confirmation ne correspond pas."),
44+ Some("court") => Some("Le nouveau mot de passe doit faire au moins 8 caractères."),
45+ Some("doublon") => Some("Une tâche porte déjà ce nom."),
46+ Some("vide") => Some("Le nom de la tâche ne peut pas être vide."),
47+ _ => None,
48+ };
49+ Ok(HtmlTemplate(SettingsPage {
50+ tasks: tasks::list_all(&state.pool).await?,
51+ csrf: auth.csrf_token(&state),
52+ notice,
53+ error,
54+ }))
55+}
56+
57+#[derive(Deserialize)]
58+pub struct TaskForm {
59+ name: String,
60+ csrf: String,
61+}
62+
63+fn is_unique_violation(e: &sqlx::Error) -> bool {
64+ matches!(e, sqlx::Error::Database(db) if db.is_unique_violation())
65+}
66+
67+pub async fn task_create(
68+ State(state): State<AppState>,
69+ auth: AuthUser,
70+ Form(form): Form<TaskForm>,
71+) -> Result<Redirect, AppError> {
72+ if !auth.check_csrf(&state, &form.csrf) {
73+ return Err(AppError::Csrf);
74+ }
75+ let name = form.name.trim();
76+ if name.is_empty() {
77+ return Ok(Redirect::to("/parametres?err=vide"));
78+ }
79+ match tasks::create(&state.pool, name).await {
80+ Ok(_) => Ok(Redirect::to("/parametres?ok=tache")),
81+ Err(e) if is_unique_violation(&e) => Ok(Redirect::to("/parametres?err=doublon")),
82+ Err(e) => Err(e.into()),
83+ }
84+}
85+
86+pub async fn task_rename(
87+ State(state): State<AppState>,
88+ auth: AuthUser,
89+ Path(id): Path<Uuid>,
90+ Form(form): Form<TaskForm>,
91+) -> Result<Redirect, AppError> {
92+ if !auth.check_csrf(&state, &form.csrf) {
93+ return Err(AppError::Csrf);
94+ }
95+ let name = form.name.trim();
96+ if name.is_empty() {
97+ return Ok(Redirect::to("/parametres?err=vide"));
98+ }
99+ match tasks::rename(&state.pool, id, name).await {
100+ Ok(0) => Err(AppError::NotFound),
101+ Ok(_) => Ok(Redirect::to("/parametres?ok=tache")),
102+ Err(e) if is_unique_violation(&e) => Ok(Redirect::to("/parametres?err=doublon")),
103+ Err(e) => Err(e.into()),
104+ }
105+}
106+
107+#[derive(Deserialize)]
108+pub struct CsrfOnly {
109+ csrf: String,
110+}
111+
112+pub async fn task_toggle_archive(
113+ State(state): State<AppState>,
114+ auth: AuthUser,
115+ Path(id): Path<Uuid>,
116+ Form(form): Form<CsrfOnly>,
117+) -> Result<Redirect, AppError> {
118+ if !auth.check_csrf(&state, &form.csrf) {
119+ return Err(AppError::Csrf);
120+ }
121+ if tasks::toggle_archived(&state.pool, id).await? == 0 {
122+ return Err(AppError::NotFound);
123+ }
124+ Ok(Redirect::to("/parametres?ok=tache"))
125+}
126+
127+#[derive(Deserialize)]
128+pub struct PasswordForm {
129+ ancien: String,
130+ nouveau: String,
131+ confirmation: String,
132+ csrf: String,
133+}
134+
135+pub async fn password_change(
136+ State(state): State<AppState>,
137+ auth: AuthUser,
138+ Form(form): Form<PasswordForm>,
139+) -> Result<Redirect, AppError> {
140+ if !auth.check_csrf(&state, &form.csrf) {
141+ return Err(AppError::Csrf);
142+ }
143+ if form.nouveau != form.confirmation {
144+ return Ok(Redirect::to("/parametres?err=confirm"));
145+ }
146+ if form.nouveau.len() < 8 {
147+ return Ok(Redirect::to("/parametres?err=court"));
148+ }
149+ let current = users::password_hash(&state.pool, auth.id)
150+ .await?
151+ .ok_or(AppError::NotFound)?;
152+ if !password::verify(&form.ancien, &current) {
153+ return Ok(Redirect::to("/parametres?err=mdp"));
154+ }
155+ let new_hash = password::hash(&form.nouveau).map_err(AppError::Internal)?;
156+ users::update_password(&state.pool, auth.id, &new_hash).await?;
157+ Ok(Redirect::to("/parametres?ok=mdp"))
158+}
added src/routes/statics.rs +28 -0
new file mode 100644
@@ -0,0 +1,28 @@
1+use axum::http::header::{CACHE_CONTROL, CONTENT_TYPE};
2+use axum::response::IntoResponse;
3+
4+const CACHE: &str = "public, max-age=86400";
5+
6+pub async fn css() -> impl IntoResponse {
7+ (
8+ [(CONTENT_TYPE, "text/css; charset=utf-8"), (CACHE_CONTROL, CACHE)],
9+ include_bytes!("../../assets/style.css").as_slice(),
10+ )
11+}
12+
13+pub async fn manifest() -> impl IntoResponse {
14+ (
15+ [
16+ (CONTENT_TYPE, "application/manifest+json"),
17+ (CACHE_CONTROL, CACHE),
18+ ],
19+ include_bytes!("../../assets/manifest.json").as_slice(),
20+ )
21+}
22+
23+pub async fn icon() -> impl IntoResponse {
24+ (
25+ [(CONTENT_TYPE, "image/png"), (CACHE_CONTROL, CACHE)],
26+ include_bytes!("../../assets/icon-512.png").as_slice(),
27+ )
28+}
new file mode 100644
@@ -0,0 +1,28 @@
1+use axum::http::header::{CACHE_CONTROL, CONTENT_TYPE};
2+use axum::response::IntoResponse;
3+
4+const CACHE: &str = "public, max-age=86400";
5+
6+pub async fn css() -> impl IntoResponse {
7+ (
8+ [(CONTENT_TYPE, "text/css; charset=utf-8"), (CACHE_CONTROL, CACHE)],
9+ include_bytes!("../../assets/style.css").as_slice(),
10+ )
11+}
12+
13+pub async fn manifest() -> impl IntoResponse {
14+ (
15+ [
16+ (CONTENT_TYPE, "application/manifest+json"),
17+ (CACHE_CONTROL, CACHE),
18+ ],
19+ include_bytes!("../../assets/manifest.json").as_slice(),
20+ )
21+}
22+
23+pub async fn icon() -> impl IntoResponse {
24+ (
25+ [(CONTENT_TYPE, "image/png"), (CACHE_CONTROL, CACHE)],
26+ include_bytes!("../../assets/icon-512.png").as_slice(),
27+ )
28+}
added src/routes/stats.rs +76 -0
new file mode 100644
@@ -0,0 +1,76 @@
1+use askama::Template;
2+use axum::extract::{Query, State};
3+use serde::Deserialize;
4+
5+use crate::auth::extractor::AuthUser;
6+use crate::charts::{ChartView, LegendItem, legend};
7+use crate::db::{stats, users};
8+use crate::error::AppError;
9+use crate::period::{Month, Week};
10+use crate::render::HtmlTemplate;
11+use crate::state::AppState;
12+
13+#[derive(Template)]
14+#[template(path = "stats.html")]
15+pub struct StatsPage {
16+ pub week_chart: ChartView,
17+ pub month_chart: ChartView,
18+ pub total_chart: ChartView,
19+ pub legend: Vec<LegendItem>,
20+ pub week_prev: String,
21+ pub week_next: Option<String>,
22+ pub month_prev: String,
23+ pub month_next: Option<String>,
24+}
25+
26+#[derive(Deserialize)]
27+pub struct StatsQuery {
28+ semaine: Option<String>,
29+ mois: Option<String>,
30+}
31+
32+fn href(week: Week, month: Month) -> String {
33+ format!("/stats?semaine={week}&mois={month}")
34+}
35+
36+pub async fn stats_page(
37+ State(state): State<AppState>,
38+ _auth: AuthUser,
39+ Query(q): Query<StatsQuery>,
40+) -> Result<HtmlTemplate<StatsPage>, AppError> {
41+ let week = q
42+ .semaine
43+ .as_deref()
44+ .and_then(Week::parse)
45+ .unwrap_or_else(Week::current);
46+ let month = q
47+ .mois
48+ .as_deref()
49+ .and_then(Month::parse)
50+ .unwrap_or_else(Month::current);
51+
52+ let (week_from, week_to) = week.bounds();
53+ let (month_from, month_to) = month.bounds();
54+
55+ let all_users = users::list_all(&state.pool).await?;
56+ let week_rows = stats::totals(&state.pool, Some(week_from), Some(week_to)).await?;
57+ let month_rows = stats::totals(&state.pool, Some(month_from), Some(month_to)).await?;
58+ let total_rows = stats::totals(&state.pool, None, None).await?;
59+
60+ let legend = legend(&[&week_rows, &month_rows, &total_rows]);
61+
62+ // Les flèches « suivant » disparaissent une fois revenu à la période courante.
63+ let week_next = (week != Week::current()).then(|| href(week.next(), month));
64+ let month_next = (month != Month::current()).then(|| href(week, month.next()));
65+
66+ Ok(HtmlTemplate(StatsPage {
67+ week_chart: ChartView::build(week.label(), &all_users, &week_rows),
68+ month_chart: ChartView::build(month.label(), &all_users, &month_rows),
69+ total_chart: ChartView::build("Depuis le début".into(), &all_users, &total_rows),
70+ legend,
71+ week_prev: href(week.prev(), month),
72+ week_next,
73+ month_prev: href(week, month.prev()),
74+ month_next,
75+ }))
76+}
new file mode 100644
@@ -0,0 +1,76 @@
1+use askama::Template;
2+use axum::extract::{Query, State};
3+use serde::Deserialize;
4+
5+use crate::auth::extractor::AuthUser;
6+use crate::charts::{ChartView, LegendItem, legend};
7+use crate::db::{stats, users};
8+use crate::error::AppError;
9+use crate::period::{Month, Week};
10+use crate::render::HtmlTemplate;
11+use crate::state::AppState;
12+
13+#[derive(Template)]
14+#[template(path = "stats.html")]
15+pub struct StatsPage {
16+ pub week_chart: ChartView,
17+ pub month_chart: ChartView,
18+ pub total_chart: ChartView,
19+ pub legend: Vec<LegendItem>,
20+ pub week_prev: String,
21+ pub week_next: Option<String>,
22+ pub month_prev: String,
23+ pub month_next: Option<String>,
24+}
25+
26+#[derive(Deserialize)]
27+pub struct StatsQuery {
28+ semaine: Option<String>,
29+ mois: Option<String>,
30+}
31+
32+fn href(week: Week, month: Month) -> String {
33+ format!("/stats?semaine={week}&mois={month}")
34+}
35+
36+pub async fn stats_page(
37+ State(state): State<AppState>,
38+ _auth: AuthUser,
39+ Query(q): Query<StatsQuery>,
40+) -> Result<HtmlTemplate<StatsPage>, AppError> {
41+ let week = q
42+ .semaine
43+ .as_deref()
44+ .and_then(Week::parse)
45+ .unwrap_or_else(Week::current);
46+ let month = q
47+ .mois
48+ .as_deref()
49+ .and_then(Month::parse)
50+ .unwrap_or_else(Month::current);
51+
52+ let (week_from, week_to) = week.bounds();
53+ let (month_from, month_to) = month.bounds();
54+
55+ let all_users = users::list_all(&state.pool).await?;
56+ let week_rows = stats::totals(&state.pool, Some(week_from), Some(week_to)).await?;
57+ let month_rows = stats::totals(&state.pool, Some(month_from), Some(month_to)).await?;
58+ let total_rows = stats::totals(&state.pool, None, None).await?;
59+
60+ let legend = legend(&[&week_rows, &month_rows, &total_rows]);
61+
62+ // Les flèches « suivant » disparaissent une fois revenu à la période courante.
63+ let week_next = (week != Week::current()).then(|| href(week.next(), month));
64+ let month_next = (month != Month::current()).then(|| href(week, month.next()));
65+
66+ Ok(HtmlTemplate(StatsPage {
67+ week_chart: ChartView::build(week.label(), &all_users, &week_rows),
68+ month_chart: ChartView::build(month.label(), &all_users, &month_rows),
69+ total_chart: ChartView::build("Depuis le début".into(), &all_users, &total_rows),
70+ legend,
71+ week_prev: href(week.prev(), month),
72+ week_next,
73+ month_prev: href(week, month.prev()),
74+ month_next,
75+ }))
76+}
added src/state.rs +39 -0
new file mode 100644
@@ -0,0 +1,39 @@
1+use std::sync::Arc;
2+
3+use anyhow::Result;
4+use biscuit_auth::{KeyPair, PrivateKey, PublicKey, builder::Algorithm};
5+use sha2::{Digest, Sha256};
6+use sqlx::PgPool;
7+
8+use crate::auth::revocation::RevocationCache;
9+
10+#[derive(Clone)]
11+pub struct AppState {
12+ pub pool: PgPool,
13+ pub root: Arc<KeyPair>,
14+ pub public: PublicKey,
15+ pub csrf_key: [u8; 32],
16+ pub revocations: RevocationCache,
17+}
18+
19+impl AppState {
20+ pub fn new(pool: PgPool, private_key_hex: &str) -> Result<Self> {
21+ let private = PrivateKey::from_bytes_hex(private_key_hex, Algorithm::Ed25519)?;
22+ let root = KeyPair::from(&private);
23+ let public = root.public();
24+
25+ // Clé CSRF dérivée de la clé privée : pas d'env var supplémentaire.
26+ let mut hasher = Sha256::new();
27+ hasher.update(b"csrf");
28+ hasher.update(private.to_bytes().as_slice());
29+ let csrf_key: [u8; 32] = hasher.finalize().into();
30+
31+ Ok(Self {
32+ pool,
33+ root: Arc::new(root),
34+ public,
35+ csrf_key,
36+ revocations: RevocationCache::default(),
37+ })
38+ }
39+}
new file mode 100644
@@ -0,0 +1,39 @@
1+use std::sync::Arc;
2+
3+use anyhow::Result;
4+use biscuit_auth::{KeyPair, PrivateKey, PublicKey, builder::Algorithm};
5+use sha2::{Digest, Sha256};
6+use sqlx::PgPool;
7+
8+use crate::auth::revocation::RevocationCache;
9+
10+#[derive(Clone)]
11+pub struct AppState {
12+ pub pool: PgPool,
13+ pub root: Arc<KeyPair>,
14+ pub public: PublicKey,
15+ pub csrf_key: [u8; 32],
16+ pub revocations: RevocationCache,
17+}
18+
19+impl AppState {
20+ pub fn new(pool: PgPool, private_key_hex: &str) -> Result<Self> {
21+ let private = PrivateKey::from_bytes_hex(private_key_hex, Algorithm::Ed25519)?;
22+ let root = KeyPair::from(&private);
23+ let public = root.public();
24+
25+ // Clé CSRF dérivée de la clé privée : pas d'env var supplémentaire.
26+ let mut hasher = Sha256::new();
27+ hasher.update(b"csrf");
28+ hasher.update(private.to_bytes().as_slice());
29+ let csrf_key: [u8; 32] = hasher.finalize().into();
30+
31+ Ok(Self {
32+ pool,
33+ root: Arc::new(root),
34+ public,
35+ csrf_key,
36+ revocations: RevocationCache::default(),
37+ })
38+ }
39+}
added templates/base.html +19 -0
new file mode 100644
@@ -0,0 +1,19 @@
1+<!doctype html>
2+<html lang="fr">
3+<head>
4+<meta charset="utf-8">
5+<meta name="viewport" content="width=device-width, initial-scale=1">
6+<meta name="theme-color" content="#1d3557">
7+<link rel="manifest" href="/manifest.json">
8+<link rel="icon" href="/icon-512.png">
9+<link rel="apple-touch-icon" href="/icon-512.png">
10+<link rel="stylesheet" href="/static/style.css">
11+<title>{% block title %}time-time{% endblock %}</title>
12+</head>
13+<body>
14+<header class="topbar"><a href="/" class="brand">⏱ time-time</a></header>
15+<main class="page">
16+{% block content %}{% endblock %}
17+</main>
18+</body>
19+</html>
new file mode 100644
@@ -0,0 +1,19 @@
1+<!doctype html>
2+<html lang="fr">
3+<head>
4+<meta charset="utf-8">
5+<meta name="viewport" content="width=device-width, initial-scale=1">
6+<meta name="theme-color" content="#1d3557">
7+<link rel="manifest" href="/manifest.json">
8+<link rel="icon" href="/icon-512.png">
9+<link rel="apple-touch-icon" href="/icon-512.png">
10+<link rel="stylesheet" href="/static/style.css">
11+<title>{% block title %}time-time{% endblock %}</title>
12+</head>
13+<body>
14+<header class="topbar"><a href="/" class="brand">⏱ time-time</a></header>
15+<main class="page">
16+{% block content %}{% endblock %}
17+</main>
18+</body>
19+</html>
added templates/chart.svg.html +21 -0
new file mode 100644
@@ -0,0 +1,21 @@
1+<svg viewBox="0 0 400 260" width="100%" class="chart" role="img" aria-label="{{ chart.title }}">
2+ {% for t in chart.y_ticks %}
3+ <line x1="{{ chart.left() }}" x2="{{ chart.right() }}" y1="{{ t.y }}" y2="{{ t.y }}" class="gridline"/>
4+ <text x="{{ chart.left() - 4.0 }}" y="{{ t.y + 3.0 }}" text-anchor="end" class="tick">{{ t.label }}</text>
5+ {% endfor %}
6+ <line x1="{{ chart.left() }}" x2="{{ chart.right() }}" y1="{{ chart.baseline }}" y2="{{ chart.baseline }}" class="axis"/>
7+ {% for bar in chart.bars %}
8+ {% for s in bar.segments %}
9+ <rect x="{{ s.x }}" y="{{ s.y }}" width="{{ s.w }}" height="{{ s.h }}" fill="{{ s.color }}">
10+ <title>{{ s.title }}</title>
11+ </rect>
12+ {% endfor %}
13+ <text x="{{ bar.label_x }}" y="248" text-anchor="middle" class="bar-label">{{ bar.user_label }}</text>
14+ {% if !bar.total_label.is_empty() %}
15+ <text x="{{ bar.label_x }}" y="{{ bar.total_y }}" text-anchor="middle" class="total-label">{{ bar.total_label }}</text>
16+ {% endif %}
17+ {% endfor %}
18+ {% if chart.empty %}
19+ <text x="218" y="130" text-anchor="middle" class="empty-label">Aucun temps sur cette période</text>
20+ {% endif %}
21+</svg>
new file mode 100644
@@ -0,0 +1,21 @@
1+<svg viewBox="0 0 400 260" width="100%" class="chart" role="img" aria-label="{{ chart.title }}">
2+ {% for t in chart.y_ticks %}
3+ <line x1="{{ chart.left() }}" x2="{{ chart.right() }}" y1="{{ t.y }}" y2="{{ t.y }}" class="gridline"/>
4+ <text x="{{ chart.left() - 4.0 }}" y="{{ t.y + 3.0 }}" text-anchor="end" class="tick">{{ t.label }}</text>
5+ {% endfor %}
6+ <line x1="{{ chart.left() }}" x2="{{ chart.right() }}" y1="{{ chart.baseline }}" y2="{{ chart.baseline }}" class="axis"/>
7+ {% for bar in chart.bars %}
8+ {% for s in bar.segments %}
9+ <rect x="{{ s.x }}" y="{{ s.y }}" width="{{ s.w }}" height="{{ s.h }}" fill="{{ s.color }}">
10+ <title>{{ s.title }}</title>
11+ </rect>
12+ {% endfor %}
13+ <text x="{{ bar.label_x }}" y="248" text-anchor="middle" class="bar-label">{{ bar.user_label }}</text>
14+ {% if !bar.total_label.is_empty() %}
15+ <text x="{{ bar.label_x }}" y="{{ bar.total_y }}" text-anchor="middle" class="total-label">{{ bar.total_label }}</text>
16+ {% endif %}
17+ {% endfor %}
18+ {% if chart.empty %}
19+ <text x="218" y="130" text-anchor="middle" class="empty-label">Aucun temps sur cette période</text>
20+ {% endif %}
21+</svg>
added templates/entries_list.html +25 -0
new file mode 100644
@@ -0,0 +1,25 @@
1+{% extends "base.html" %}
2+{% block title %}Mes temps · time-time{% endblock %}
3+{% block content %}
4+<h1>Mes temps</h1>
5+{% if entries.is_empty() %}
6+<p>Aucun temps enregistré{% if page > 1 %} sur cette page{% endif %}.</p>
7+{% else %}
8+<ul class="entries">
9+ {% for e in entries %}
10+ <li>
11+ <a class="entry" href="/temps/{{ e.id }}/modifier">
12+ <span class="entry-date">{{ e.date }}</span>
13+ <span class="entry-task">{{ e.task_name }}</span>
14+ <span class="entry-duration">{{ e.duration }}</span>
15+ </a>
16+ </li>
17+ {% endfor %}
18+</ul>
19+{% endif %}
20+<nav class="pager">
21+ {% if page > 1 %}<a class="btn" href="/temps?page={{ page - 1 }}">← Plus récents</a>{% endif %}
22+ {% if has_next %}<a class="btn" href="/temps?page={{ page + 1 }}">Plus anciens →</a>{% endif %}
23+</nav>
24+<p><a class="btn" href="/stats">← Statistiques</a></p>
25+{% endblock %}
new file mode 100644
@@ -0,0 +1,25 @@
1+{% extends "base.html" %}
2+{% block title %}Mes temps · time-time{% endblock %}
3+{% block content %}
4+<h1>Mes temps</h1>
5+{% if entries.is_empty() %}
6+<p>Aucun temps enregistré{% if page > 1 %} sur cette page{% endif %}.</p>
7+{% else %}
8+<ul class="entries">
9+ {% for e in entries %}
10+ <li>
11+ <a class="entry" href="/temps/{{ e.id }}/modifier">
12+ <span class="entry-date">{{ e.date }}</span>
13+ <span class="entry-task">{{ e.task_name }}</span>
14+ <span class="entry-duration">{{ e.duration }}</span>
15+ </a>
16+ </li>
17+ {% endfor %}
18+</ul>
19+{% endif %}
20+<nav class="pager">
21+ {% if page > 1 %}<a class="btn" href="/temps?page={{ page - 1 }}">← Plus récents</a>{% endif %}
22+ {% if has_next %}<a class="btn" href="/temps?page={{ page + 1 }}">Plus anciens →</a>{% endif %}
23+</nav>
24+<p><a class="btn" href="/stats">← Statistiques</a></p>
25+{% endblock %}
added templates/entry_form.html +43 -0
new file mode 100644
@@ -0,0 +1,43 @@
1+{% extends "base.html" %}
2+{% block title %}{{ title }} · time-time{% endblock %}
3+{% block content %}
4+<h1>{{ title }}</h1>
5+{% if saved %}<p class="flash flash-ok">Temps enregistré ✔</p>{% endif %}
6+{% if tasks.is_empty() %}
7+<p class="flash flash-error">Aucune tâche disponible — créez-en une dans <a href="/parametres">Paramètres</a>.</p>
8+{% else %}
9+<form method="post" action="{{ action }}" class="stack">
10+ <input type="hidden" name="csrf" value="{{ csrf }}">
11+
12+ <label for="minutes">Durée (minutes)</label>
13+ <input id="minutes" name="minutes" type="number" inputmode="numeric"
14+ min="1" max="1440" required
15+ {% if let Some(m) = minutes %}value="{{ m }}"{% endif %}>
16+ {% if !presets.is_empty() %}
17+ <div class="presets">
18+ {% for p in presets %}<a class="btn btn-preset" href="{{ p.href }}">{{ p.label }}</a>{% endfor %}
19+ </div>
20+ {% endif %}
21+
22+ <label for="tache">Tâche</label>
23+ <select id="tache" name="tache" required>
24+ {% for t in tasks %}
25+ <option value="{{ t.id }}"{% if t.selected %} selected{% endif %}>
26+ {{ t.name }}{% if t.archived %} (archivée){% endif %}
27+ </option>
28+ {% endfor %}
29+ </select>
30+
31+ <label for="date">Date</label>
32+ <input id="date" name="date" type="date" value="{{ date }}" required>
33+
34+ <button type="submit" class="btn btn-primary btn-big">Valider</button>
35+</form>
36+{% if let Some(del) = delete_action %}
37+<form method="post" action="{{ del }}" class="stack danger-zone">
38+ <input type="hidden" name="csrf" value="{{ csrf }}">
39+ <button type="submit" class="btn btn-danger">Supprimer ce temps</button>
40+</form>
41+{% endif %}
42+{% endif %}
43+{% endblock %}
new file mode 100644
@@ -0,0 +1,43 @@
1+{% extends "base.html" %}
2+{% block title %}{{ title }} · time-time{% endblock %}
3+{% block content %}
4+<h1>{{ title }}</h1>
5+{% if saved %}<p class="flash flash-ok">Temps enregistré ✔</p>{% endif %}
6+{% if tasks.is_empty() %}
7+<p class="flash flash-error">Aucune tâche disponible — créez-en une dans <a href="/parametres">Paramètres</a>.</p>
8+{% else %}
9+<form method="post" action="{{ action }}" class="stack">
10+ <input type="hidden" name="csrf" value="{{ csrf }}">
11+
12+ <label for="minutes">Durée (minutes)</label>
13+ <input id="minutes" name="minutes" type="number" inputmode="numeric"
14+ min="1" max="1440" required
15+ {% if let Some(m) = minutes %}value="{{ m }}"{% endif %}>
16+ {% if !presets.is_empty() %}
17+ <div class="presets">
18+ {% for p in presets %}<a class="btn btn-preset" href="{{ p.href }}">{{ p.label }}</a>{% endfor %}
19+ </div>
20+ {% endif %}
21+
22+ <label for="tache">Tâche</label>
23+ <select id="tache" name="tache" required>
24+ {% for t in tasks %}
25+ <option value="{{ t.id }}"{% if t.selected %} selected{% endif %}>
26+ {{ t.name }}{% if t.archived %} (archivée){% endif %}
27+ </option>
28+ {% endfor %}
29+ </select>
30+
31+ <label for="date">Date</label>
32+ <input id="date" name="date" type="date" value="{{ date }}" required>
33+
34+ <button type="submit" class="btn btn-primary btn-big">Valider</button>
35+</form>
36+{% if let Some(del) = delete_action %}
37+<form method="post" action="{{ del }}" class="stack danger-zone">
38+ <input type="hidden" name="csrf" value="{{ csrf }}">
39+ <button type="submit" class="btn btn-danger">Supprimer ce temps</button>
40+</form>
41+{% endif %}
42+{% endif %}
43+{% endblock %}
added templates/home.html +8 -0
new file mode 100644
@@ -0,0 +1,8 @@
1+{% extends "base.html" %}
2+{% block content %}
3+<nav class="stack menu">
4+ <a class="btn btn-primary btn-big" href="/temps/nouveau">➕ Ajouter un temps</a>
5+ <a class="btn btn-big" href="/stats">📊 Statistiques</a>
6+ <a class="btn btn-big" href="/parametres">⚙️ Paramètres</a>
7+</nav>
8+{% endblock %}
new file mode 100644
@@ -0,0 +1,8 @@
1+{% extends "base.html" %}
2+{% block content %}
3+<nav class="stack menu">
4+ <a class="btn btn-primary btn-big" href="/temps/nouveau">➕ Ajouter un temps</a>
5+ <a class="btn btn-big" href="/stats">📊 Statistiques</a>
6+ <a class="btn btn-big" href="/parametres">⚙️ Paramètres</a>
7+</nav>
8+{% endblock %}
added templates/login.html +14 -0
new file mode 100644
@@ -0,0 +1,14 @@
1+{% extends "base.html" %}
2+{% block title %}Connexion · time-time{% endblock %}
3+{% block content %}
4+<h1>Connexion</h1>
5+{% if error %}<p class="flash flash-error">Identifiants incorrects.</p>{% endif %}
6+<form method="post" action="/login" class="stack">
7+ <label for="username">Nom d'utilisateur</label>
8+ <input id="username" name="username" type="text" autocomplete="username"
9+ autocapitalize="none" autocorrect="off" required autofocus>
10+ <label for="password">Mot de passe</label>
11+ <input id="password" name="password" type="password" autocomplete="current-password" required>
12+ <button type="submit" class="btn btn-primary">Se connecter</button>
13+</form>
14+{% endblock %}
new file mode 100644
@@ -0,0 +1,14 @@
1+{% extends "base.html" %}
2+{% block title %}Connexion · time-time{% endblock %}
3+{% block content %}
4+<h1>Connexion</h1>
5+{% if error %}<p class="flash flash-error">Identifiants incorrects.</p>{% endif %}
6+<form method="post" action="/login" class="stack">
7+ <label for="username">Nom d'utilisateur</label>
8+ <input id="username" name="username" type="text" autocomplete="username"
9+ autocapitalize="none" autocorrect="off" required autofocus>
10+ <label for="password">Mot de passe</label>
11+ <input id="password" name="password" type="password" autocomplete="current-password" required>
12+ <button type="submit" class="btn btn-primary">Se connecter</button>
13+</form>
14+{% endblock %}
added templates/settings.html +50 -0
new file mode 100644
@@ -0,0 +1,50 @@
1+{% extends "base.html" %}
2+{% block title %}Paramètres · time-time{% endblock %}
3+{% block content %}
4+<h1>Paramètres</h1>
5+{% if let Some(n) = notice %}<p class="flash flash-ok">{{ n }}</p>{% endif %}
6+{% if let Some(e) = error %}<p class="flash flash-error">{{ e }}</p>{% endif %}
7+
8+<h2>Tâches</h2>
9+<ul class="task-list">
10+ {% for t in tasks %}
11+ <li class="task {% if t.archived %}task-archived{% endif %}">
12+ <form method="post" action="/parametres/taches/{{ t.id }}" class="task-rename">
13+ <input type="hidden" name="csrf" value="{{ csrf }}">
14+ <input name="name" value="{{ t.name }}" required aria-label="Nom de la tâche">
15+ <button type="submit" class="btn btn-small">Renommer</button>
16+ </form>
17+ <form method="post" action="/parametres/taches/{{ t.id }}/archiver">
18+ <input type="hidden" name="csrf" value="{{ csrf }}">
19+ <button type="submit" class="btn btn-small">
20+ {% if t.archived %}Restaurer{% else %}Archiver{% endif %}
21+ </button>
22+ </form>
23+ </li>
24+ {% endfor %}
25+</ul>
26+<form method="post" action="/parametres/taches" class="task-add">
27+ <input type="hidden" name="csrf" value="{{ csrf }}">
28+ <input name="name" placeholder="Nouvelle tâche" required aria-label="Nouvelle tâche">
29+ <button type="submit" class="btn btn-primary">Créer</button>
30+</form>
31+<p class="hint">Archiver une tâche la retire du formulaire d'ajout ; les temps déjà saisis restent dans les statistiques.</p>
32+
33+<h2>Mot de passe</h2>
34+<form method="post" action="/parametres/mot-de-passe" class="stack">
35+ <input type="hidden" name="csrf" value="{{ csrf }}">
36+ <label for="ancien">Ancien mot de passe</label>
37+ <input id="ancien" name="ancien" type="password" autocomplete="current-password" required>
38+ <label for="nouveau">Nouveau mot de passe</label>
39+ <input id="nouveau" name="nouveau" type="password" autocomplete="new-password" required minlength="8">
40+ <label for="confirmation">Confirmation</label>
41+ <input id="confirmation" name="confirmation" type="password" autocomplete="new-password" required minlength="8">
42+ <button type="submit" class="btn btn-primary">Changer le mot de passe</button>
43+</form>
44+
45+<h2>Session</h2>
46+<form method="post" action="/logout">
47+ <input type="hidden" name="csrf" value="{{ csrf }}">
48+ <button type="submit" class="btn btn-danger">Se déconnecter</button>
49+</form>
50+{% endblock %}
new file mode 100644
@@ -0,0 +1,50 @@
1+{% extends "base.html" %}
2+{% block title %}Paramètres · time-time{% endblock %}
3+{% block content %}
4+<h1>Paramètres</h1>
5+{% if let Some(n) = notice %}<p class="flash flash-ok">{{ n }}</p>{% endif %}
6+{% if let Some(e) = error %}<p class="flash flash-error">{{ e }}</p>{% endif %}
7+
8+<h2>Tâches</h2>
9+<ul class="task-list">
10+ {% for t in tasks %}
11+ <li class="task {% if t.archived %}task-archived{% endif %}">
12+ <form method="post" action="/parametres/taches/{{ t.id }}" class="task-rename">
13+ <input type="hidden" name="csrf" value="{{ csrf }}">
14+ <input name="name" value="{{ t.name }}" required aria-label="Nom de la tâche">
15+ <button type="submit" class="btn btn-small">Renommer</button>
16+ </form>
17+ <form method="post" action="/parametres/taches/{{ t.id }}/archiver">
18+ <input type="hidden" name="csrf" value="{{ csrf }}">
19+ <button type="submit" class="btn btn-small">
20+ {% if t.archived %}Restaurer{% else %}Archiver{% endif %}
21+ </button>
22+ </form>
23+ </li>
24+ {% endfor %}
25+</ul>
26+<form method="post" action="/parametres/taches" class="task-add">
27+ <input type="hidden" name="csrf" value="{{ csrf }}">
28+ <input name="name" placeholder="Nouvelle tâche" required aria-label="Nouvelle tâche">
29+ <button type="submit" class="btn btn-primary">Créer</button>
30+</form>
31+<p class="hint">Archiver une tâche la retire du formulaire d'ajout ; les temps déjà saisis restent dans les statistiques.</p>
32+
33+<h2>Mot de passe</h2>
34+<form method="post" action="/parametres/mot-de-passe" class="stack">
35+ <input type="hidden" name="csrf" value="{{ csrf }}">
36+ <label for="ancien">Ancien mot de passe</label>
37+ <input id="ancien" name="ancien" type="password" autocomplete="current-password" required>
38+ <label for="nouveau">Nouveau mot de passe</label>
39+ <input id="nouveau" name="nouveau" type="password" autocomplete="new-password" required minlength="8">
40+ <label for="confirmation">Confirmation</label>
41+ <input id="confirmation" name="confirmation" type="password" autocomplete="new-password" required minlength="8">
42+ <button type="submit" class="btn btn-primary">Changer le mot de passe</button>
43+</form>
44+
45+<h2>Session</h2>
46+<form method="post" action="/logout">
47+ <input type="hidden" name="csrf" value="{{ csrf }}">
48+ <button type="submit" class="btn btn-danger">Se déconnecter</button>
49+</form>
50+{% endblock %}
added templates/stats.html +42 -0
new file mode 100644
@@ -0,0 +1,42 @@
1+{% extends "base.html" %}
2+{% block title %}Statistiques · time-time{% endblock %}
3+{% block content %}
4+<h1>Statistiques</h1>
5+
6+{% if !legend.is_empty() %}
7+<ul class="legend">
8+ {% for item in legend %}
9+ <li><span class="swatch" style="background:{{ item.color }}"></span>{{ item.name }}</li>
10+ {% endfor %}
11+</ul>
12+{% endif %}
13+
14+<section class="chart-block">
15+ <div class="chart-head">
16+ <a class="btn btn-nav" href="{{ week_prev }}" aria-label="Semaine précédente">←</a>
17+ <h2>{{ week_chart.title }}</h2>
18+ {% if let Some(next) = week_next %}
19+ <a class="btn btn-nav" href="{{ next }}" aria-label="Semaine suivante">→</a>
20+ {% else %}<span class="btn-nav-placeholder"></span>{% endif %}
21+ </div>
22+ {% let chart = week_chart %}{% include "chart.svg.html" %}
23+</section>
24+
25+<section class="chart-block">
26+ <div class="chart-head">
27+ <a class="btn btn-nav" href="{{ month_prev }}" aria-label="Mois précédent">←</a>
28+ <h2>{{ month_chart.title }}</h2>
29+ {% if let Some(next) = month_next %}
30+ <a class="btn btn-nav" href="{{ next }}" aria-label="Mois suivant">→</a>
31+ {% else %}<span class="btn-nav-placeholder"></span>{% endif %}
32+ </div>
33+ {% let chart = month_chart %}{% include "chart.svg.html" %}
34+</section>
35+
36+<section class="chart-block">
37+ <div class="chart-head"><h2>{{ total_chart.title }}</h2></div>
38+ {% let chart = total_chart %}{% include "chart.svg.html" %}
39+</section>
40+
41+<p><a class="btn btn-big" href="/temps">✏️ Modifier / supprimer mes temps</a></p>
42+{% endblock %}
new file mode 100644
@@ -0,0 +1,42 @@
1+{% extends "base.html" %}
2+{% block title %}Statistiques · time-time{% endblock %}
3+{% block content %}
4+<h1>Statistiques</h1>
5+
6+{% if !legend.is_empty() %}
7+<ul class="legend">
8+ {% for item in legend %}
9+ <li><span class="swatch" style="background:{{ item.color }}"></span>{{ item.name }}</li>
10+ {% endfor %}
11+</ul>
12+{% endif %}
13+
14+<section class="chart-block">
15+ <div class="chart-head">
16+ <a class="btn btn-nav" href="{{ week_prev }}" aria-label="Semaine précédente">←</a>
17+ <h2>{{ week_chart.title }}</h2>
18+ {% if let Some(next) = week_next %}
19+ <a class="btn btn-nav" href="{{ next }}" aria-label="Semaine suivante">→</a>
20+ {% else %}<span class="btn-nav-placeholder"></span>{% endif %}
21+ </div>
22+ {% let chart = week_chart %}{% include "chart.svg.html" %}
23+</section>
24+
25+<section class="chart-block">
26+ <div class="chart-head">
27+ <a class="btn btn-nav" href="{{ month_prev }}" aria-label="Mois précédent">←</a>
28+ <h2>{{ month_chart.title }}</h2>
29+ {% if let Some(next) = month_next %}
30+ <a class="btn btn-nav" href="{{ next }}" aria-label="Mois suivant">→</a>
31+ {% else %}<span class="btn-nav-placeholder"></span>{% endif %}
32+ </div>
33+ {% let chart = month_chart %}{% include "chart.svg.html" %}
34+</section>
35+
36+<section class="chart-block">
37+ <div class="chart-head"><h2>{{ total_chart.title }}</h2></div>
38+ {% let chart = total_chart %}{% include "chart.svg.html" %}
39+</section>
40+
41+<p><a class="btn btn-big" href="/temps">✏️ Modifier / supprimer mes temps</a></p>
42+{% endblock %}