feat: socle BDD (tâche 1.9 Phase 1-2) + moteur APT (tâche 2 SJ-0→3) + WIP capabilities/auth/Rust

Checkpoint multi-chantiers (arbre vert : tsc 0 erreur, 70 tests, build OK).
- tâche 1.9 Phase 1 : schéma socle (machine_state/events/reports/raw_artifacts/
  hardware/metrics + colonnes étendues) + wiring refresh/execute. Migration 0002.
- tâche 1.9 Phase 2 : machine_credentials + machine_host_keys (non destructif,
  dual-read + backfill). Migration 0003. Fix séquence journal de migration.
- tâche 2 : SJ-0 (types étendus rétro-compatibles, réducteur Docker, resolveTemplate),
  SJ-1 (update-analyze enrichi), SJ-2 (apply + diff dpkg + timeout inactivité SSH),
  SJ-3 (reboot vérifié boot_id).
- WIP parallèle inclus : /api/capabilities, auth/apiTokens/apiClients, system metrics,
  scaffold app_rust, ajustements frontend.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-05 19:50:25 +02:00
parent 0fbca06d3d
commit 08919752e3
69 changed files with 7785 additions and 102 deletions
+1
View File
@@ -0,0 +1 @@
target/
+728
View File
@@ -0,0 +1,728 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "autocfg"
version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53"
[[package]]
name = "bitflags"
version = "2.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "84d7ced0ae9557296835c32bf1b1e02b44c746701f898460fb000d7eaa84f00a"
[[package]]
name = "cairo-rs"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5cc8d9aa793480744cd9a0524fef1a2e197d9eaa0f739cde19d16aba530dcb95"
dependencies = [
"bitflags",
"cairo-sys-rs",
"glib",
"libc",
]
[[package]]
name = "cairo-sys-rs"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8b4985713047f5faee02b8db6a6ef32bbb50269ff53c1aee716d1d195b76d54"
dependencies = [
"glib-sys",
"libc",
"system-deps",
]
[[package]]
name = "cfg-expr"
version = "0.20.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fb693542bcafa528e198be0ebd9d3632ca5b7c93dbe7237460e199910835997c"
dependencies = [
"smallvec",
"target-lexicon",
]
[[package]]
name = "equivalent"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f"
[[package]]
name = "field-offset"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38e2275cc4e4fc009b0669731a1e5ab7ebf11f469eaede2bab9309a5b4d6057f"
dependencies = [
"memoffset",
"rustc_version",
]
[[package]]
name = "futures-channel"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07bbe89c50d7a535e539b8c17bc0b49bdb77747034daa8087407d655f3f7cc1d"
dependencies = [
"futures-core",
]
[[package]]
name = "futures-core"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e3450815272ef58cec6d564423f6e755e25379b217b0bc688e295ba24df6b1d"
[[package]]
name = "futures-executor"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf29c38818342a3b26b5b923639e7b1f4a61fc5e76102d4b1981c6dc7a7579d"
dependencies = [
"futures-core",
"futures-task",
"futures-util",
]
[[package]]
name = "futures-io"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cecba35d7ad927e23624b22ad55235f2239cfa44fd10428eecbeba6d6a717718"
[[package]]
name = "futures-macro"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e835b70203e41293343137df5c0664546da5745f82ec9b84d40be8336958447b"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "futures-task"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "037711b3d59c33004d3856fbdc83b99d4ff37a24768fa1be9ce3538a1cde4393"
[[package]]
name = "futures-util"
version = "0.3.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "389ca41296e6190b48053de0321d02a77f32f8a5d2461dd38762c0593805c6d6"
dependencies = [
"futures-core",
"futures-macro",
"futures-task",
"pin-project-lite",
"slab",
]
[[package]]
name = "gdk-pixbuf"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25f420376dbee041b2db374ce4573892a36222bb3f6c0c43e24f0d67eae9b646"
dependencies = [
"gdk-pixbuf-sys",
"gio",
"glib",
"libc",
]
[[package]]
name = "gdk-pixbuf-sys"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48f31b37b1fc4b48b54f6b91b7ef04c18e00b4585d98359dd7b998774bbd91fb"
dependencies = [
"gio-sys",
"glib-sys",
"gobject-sys",
"libc",
"system-deps",
]
[[package]]
name = "gdk4"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd42fdbbf48612c6e8f47c65fb92d2e8f39c25aecd6af047e83897c1a22d2a4e"
dependencies = [
"cairo-rs",
"gdk-pixbuf",
"gdk4-sys",
"gio",
"glib",
"libc",
"pango",
]
[[package]]
name = "gdk4-sys"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9d974ac4f15e67472c3a9728daf612590b4a5762a4b33f0edd298df0b80d043c"
dependencies = [
"cairo-sys-rs",
"gdk-pixbuf-sys",
"gio-sys",
"glib-sys",
"gobject-sys",
"libc",
"pango-sys",
"pkg-config",
"system-deps",
]
[[package]]
name = "gio"
version = "0.22.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3848bcba3a35cc0a71df8ba8ecfd799d6bfb862342a53a4a915fb62213aa4e6"
dependencies = [
"futures-channel",
"futures-core",
"futures-io",
"futures-util",
"gio-sys",
"glib",
"libc",
"pin-project-lite",
"smallvec",
]
[[package]]
name = "gio-sys"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64729ba2772c080448f9f966dba8f4456beeb100d8c28a865ef8a0f2ef4987e1"
dependencies = [
"glib-sys",
"gobject-sys",
"libc",
"system-deps",
"windows-sys",
]
[[package]]
name = "glib"
version = "0.22.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c207e04e51605dcf7b2924c41591b3a10e1438eaac5bcf448fb91f325381104a"
dependencies = [
"bitflags",
"futures-channel",
"futures-core",
"futures-executor",
"futures-task",
"futures-util",
"gio-sys",
"glib-macros",
"glib-sys",
"gobject-sys",
"libc",
"memchr",
"smallvec",
]
[[package]]
name = "glib-macros"
version = "0.22.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "506d23499707c7142898429757e8d9a3871d965239a2cb66dfa05052be6d6f19"
dependencies = [
"heck",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "glib-sys"
version = "0.22.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f7fbac234ed5bc2a28359b7bde8e1b9cdf1441cc2d7f068e4824672d7db9445"
dependencies = [
"libc",
"system-deps",
]
[[package]]
name = "gobject-sys"
version = "0.22.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "22a861859b887a79cf461359c192c97a57d8fb0229dd291232e57aa11f6fa72c"
dependencies = [
"glib-sys",
"libc",
"system-deps",
]
[[package]]
name = "graphene-rs"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7d1b7881f96869f49808b6adfe906a93a57a34204952253444d68c3208d71f1"
dependencies = [
"glib",
"graphene-sys",
"libc",
]
[[package]]
name = "graphene-sys"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "517f062f3fd6b7fd3e57a3f038a74b3c23ca32f51199ff028aa704609943f79c"
dependencies = [
"glib-sys",
"libc",
"pkg-config",
"system-deps",
]
[[package]]
name = "gsk4"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "53c912dfcbd28acace5fc99c40bb9f25e1dcb73efb1f2608327f66a99acdcb62"
dependencies = [
"cairo-rs",
"gdk4",
"glib",
"graphene-rs",
"gsk4-sys",
"libc",
"pango",
]
[[package]]
name = "gsk4-sys"
version = "0.11.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7d54bbc7a9d8b6ffe4f0c95eede15ccfb365c8bf521275abe6bcfb57b18fb8a"
dependencies = [
"cairo-sys-rs",
"gdk4-sys",
"glib-sys",
"gobject-sys",
"graphene-sys",
"libc",
"pango-sys",
"system-deps",
]
[[package]]
name = "gtk4"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7181b837f04cbe93f79441475f7a00560a92cba7a72e38cc1a68b6f8b78eaae2"
dependencies = [
"cairo-rs",
"field-offset",
"futures-channel",
"gdk-pixbuf",
"gdk4",
"gio",
"glib",
"graphene-rs",
"gsk4",
"gtk4-macros",
"gtk4-sys",
"libc",
"pango",
]
[[package]]
name = "gtk4-macros"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3581b242ba62fdff122ebb626ea641582ec326031622bd19d60f85029c804a87"
dependencies = [
"proc-macro-crate",
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "gtk4-sys"
version = "0.11.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20ba8e695e2640455561274e65e45f0a151619e450746007667f4b23ceae4e1b"
dependencies = [
"cairo-sys-rs",
"gdk-pixbuf-sys",
"gdk4-sys",
"gio-sys",
"glib-sys",
"gobject-sys",
"graphene-sys",
"gsk4-sys",
"libc",
"pango-sys",
"system-deps",
]
[[package]]
name = "hashbrown"
version = "0.17.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ed5909b6e89a2db4456e54cd5f673791d7eca6732202bbf2a9cc504fe2f9b84a"
[[package]]
name = "heck"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea"
[[package]]
name = "indexmap"
version = "2.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9"
dependencies = [
"equivalent",
"hashbrown",
]
[[package]]
name = "itoa"
version = "1.0.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8f42a60cbdf9a97f5d2305f08a87dc4e09308d1276d28c869c684d7777685682"
[[package]]
name = "libadwaita"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc0da4e27b20d3e71f830e5b0f0188d22c257986bf421c02cfde777fe07932a4"
dependencies = [
"gdk4",
"gio",
"glib",
"gtk4",
"libadwaita-sys",
"libc",
"pango",
]
[[package]]
name = "libadwaita-sys"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "aaee067051c5d3c058d050d167688b80b67de1950cfca77730549aa761fc5d7d"
dependencies = [
"gdk4-sys",
"gio-sys",
"glib-sys",
"gobject-sys",
"gtk4-sys",
"libc",
"pango-sys",
"system-deps",
]
[[package]]
name = "libc"
version = "0.2.186"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66"
[[package]]
name = "memchr"
version = "2.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b947ae49db0d222b1dbc6b113ce7248a3fc3a6ca21b696717bfc000ba4484d8"
[[package]]
name = "memoffset"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "488016bfae457b036d996092f6cb448677611ce4449e970ceaf42695203f218a"
dependencies = [
"autocfg",
]
[[package]]
name = "pango"
version = "0.22.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "251bdc6e6487b811be0e406a21e301e07e45c0aa8fa39e00c0c8e12a91752438"
dependencies = [
"gio",
"glib",
"libc",
"pango-sys",
]
[[package]]
name = "pango-sys"
version = "0.22.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbd111a20ca90fedf03e09c59783c679c00900f1d8491cca5399f5e33609d5d6"
dependencies = [
"glib-sys",
"gobject-sys",
"libc",
"system-deps",
]
[[package]]
name = "pin-project-lite"
version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd"
[[package]]
name = "pkg-config"
version = "0.3.33"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e"
[[package]]
name = "proc-macro-crate"
version = "3.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e67ba7e9b2b56446f1d419b1d807906278ffa1a658a8a5d8a39dcb1f5a78614f"
dependencies = [
"toml_edit",
]
[[package]]
name = "proc-macro2"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fd00f0bb2e90d81d1044c2b32617f68fcb9fa3bb7640c23e9c748e53fb30934"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.45"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41f2619966050689382d2b44f664f4bc593e129785a36d6ee376ddf37259b924"
dependencies = [
"proc-macro2",
]
[[package]]
name = "rustc_version"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92"
dependencies = [
"semver",
]
[[package]]
name = "semver"
version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd"
[[package]]
name = "serde"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e"
dependencies = [
"serde_core",
"serde_derive",
]
[[package]]
name = "serde_core"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.228"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.150"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8014e44b4736ed0538adeecded0fce2a272f22dc9578a7eb6b2d9993c74cfb9"
dependencies = [
"itoa",
"memchr",
"serde",
"serde_core",
"zmij",
]
[[package]]
name = "serde_spanned"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6662b5879511e06e8999a8a235d848113e942c9124f211511b16466ee2995f26"
dependencies = [
"serde_core",
]
[[package]]
name = "slab"
version = "0.4.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c790de23124f9ab44544d7ac05d60440adc586479ce501c1d6d7da3cd8c9cf5"
[[package]]
name = "smallvec"
version = "1.15.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "67b1b7a3b5fe4f1376887184045fcf45c69e92af734b7aaddc05fb777b6fbd03"
[[package]]
name = "syn"
version = "2.0.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e665b8803e7b1d2a727f4023456bbbbe74da67099c585258af0ad9c5013b9b99"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "system-deps"
version = "7.0.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "396a35feb67335377e0251fcbc1092fc85c484bd4e3a7a54319399da127796e7"
dependencies = [
"cfg-expr",
"heck",
"pkg-config",
"toml",
"version-compare",
]
[[package]]
name = "system-update-gnome"
version = "0.1.0"
dependencies = [
"gtk4",
"libadwaita",
"serde",
"serde_json",
]
[[package]]
name = "target-lexicon"
version = "0.13.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "adb6935a6f5c20170eeceb1a3835a49e12e19d792f6dd344ccc76a985ca5a6ca"
[[package]]
name = "toml"
version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81f3d15e84cbcd896376e6730314d59fb5a87f31e4b038454184435cd57defee"
dependencies = [
"indexmap",
"serde_core",
"serde_spanned",
"toml_datetime",
"toml_parser",
"toml_writer",
"winnow",
]
[[package]]
name = "toml_datetime"
version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3165f65f62e28e0115a00b2ebdd37eb6f3b641855f9d636d3cd4103767159ad7"
dependencies = [
"serde_core",
]
[[package]]
name = "toml_edit"
version = "0.25.12+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d2153edc6955a6c354fad8f5efd38b6a8769bdccf9fe50f8e1329f81b0baa5d7"
dependencies = [
"indexmap",
"toml_datetime",
"toml_parser",
"winnow",
]
[[package]]
name = "toml_parser"
version = "1.1.2+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526"
dependencies = [
"winnow",
]
[[package]]
name = "toml_writer"
version = "1.1.1+spec-1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "756daf9b1013ebe47a8776667b466417e2d4c5679d441c26230efd9ef78692db"
[[package]]
name = "unicode-ident"
version = "1.0.24"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75"
[[package]]
name = "version-compare"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03c2856837ef78f57382f06b2b8563a2f512f7185d732608fd9176cb3b8edf0e"
[[package]]
name = "windows-link"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5"
[[package]]
name = "windows-sys"
version = "0.61.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc"
dependencies = [
"windows-link",
]
[[package]]
name = "winnow"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0592e1c9d151f854e6fd382574c3a0855250e1d9b2f99d9281c6e6391af352f1"
dependencies = [
"memchr",
]
[[package]]
name = "zmij"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8848ee67ecc8aedbaf3e4122217aff892639231befc6a1b58d29fff4c2cabaa"
+16
View File
@@ -0,0 +1,16 @@
[package]
name = "system-update-gnome"
version = "0.1.0"
edition = "2024"
description = "Client local Rust/GNOME pour le backend system_update"
license = "UNLICENSED"
[dependencies]
adw = { package = "libadwaita", version = "0.9", features = ["v1_6"], optional = true }
gtk = { package = "gtk4", version = "0.11", features = ["v4_6"], optional = true }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
[features]
default = []
gui = ["dep:adw", "dep:gtk"]
+56
View File
@@ -0,0 +1,56 @@
# system-update-gnome
Scaffold de l'application locale Rust/GNOME pour `system_update`.
Ce sous-dossier est volontairement dédié au développement de l'application Rust :
- backend/webapp : racine du projet, `server/`, `client/`, `shared/` ;
- application Rust/GNOME : `app_rust/system-update-gnome/` ;
- artefacts Cargo : `app_rust/system-update-gnome/target/`, ignoré par Git.
État actuel :
- client CLI minimal ;
- aucune dépendance externe pour garder le premier build vérifiable sans accès réseau ;
- test de connexion HTTP vers `GET /api/capabilities` ;
- stratégie token séparée dans `src/token_store.rs` ;
- première UI GTK/libadwaita derrière la feature Cargo `gui`.
Exemples :
```bash
cargo run -- --server http://127.0.0.1:8787 capabilities
cargo run -- --server http://127.0.0.1:8787 status
cargo run -- --server http://127.0.0.1:8787 metrics
cargo run -- --server http://127.0.0.1:8787 machines
SYSTEM_UPDATE_SERVER=http://127.0.0.1:8787 cargo run -- capabilities
```
Interface graphique :
```bash
cargo run --features gui -- gui
```
Avec un serveur précis :
```bash
cargo run --features gui -- --server http://10.0.1.137:8787 gui
```
Pré-requis pour le futur incrément GTK/libadwaita :
```bash
sudo apt install libgtk-4-dev libadwaita-1-dev
```
Règles :
- l'application ne fait pas de SSH direct ;
- les credentials machines restent côté backend ;
- les actions passent par l'API du serveur ;
- le token local sera stocké via trousseau système dans un incrément suivant.
Voir aussi :
- `docs/token-storage.md`.
@@ -0,0 +1,33 @@
# Stockage du token API
Objectif : l'application locale ne doit pas stocker le token API en clair dans un fichier de configuration.
État actuel du scaffold :
- `--token` : accepté pour test manuel ponctuel ;
- `SYSTEM_UPDATE_TOKEN` : accepté pour développement ;
- trousseau système : prévu, pas encore activé dans `Cargo.toml`.
Identité prévue dans le trousseau :
- service : `system-update` ;
- compte : `api-token`.
Choix technique à finaliser :
- la documentation actuelle de `keyring` indique que Linux dispose de plusieurs magasins possibles, dont keyutils et Secret Service ;
- pour une app GNOME, Secret Service est le choix naturel ;
- l'écosystème `keyring` 4.x sépare davantage les composants de librairie, donc l'ajout Cargo doit être fait après choix précis entre `keyring-core` + store Secret Service ou une version `keyring` 3.x encore centrée librairie.
Références :
- https://docs.rs/keyring/latest/keyring/
- https://docs.rs/crate/keyring/latest
- https://github.com/open-source-cooperative/keyring-rs/wiki/Keyring-Core
Règle de sécurité :
- token jamais loggé ;
- token jamais écrit dans les rapports ;
- token affiché uniquement sous forme de préfixe côté backend ;
- révocation possible côté backend via `api_clients.revoked_at`.
+157
View File
@@ -0,0 +1,157 @@
use std::io::{Read, Write};
use std::net::TcpStream;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct HttpUrl {
pub host: String,
pub port: u16,
pub base_path: String,
}
impl HttpUrl {
pub fn parse(server_url: &str) -> Result<Self, String> {
let without_scheme = server_url
.strip_prefix("http://")
.ok_or_else(|| "ce premier client supporte seulement http://".to_string())?;
let (host_port, base_path) = match without_scheme.split_once('/') {
Some((host_port, path)) => (host_port, format!("/{path}")),
None => (without_scheme, String::new()),
};
let (host, port) = match host_port.rsplit_once(':') {
Some((host, port)) => {
let parsed_port = port
.parse::<u16>()
.map_err(|_| "port serveur invalide".to_string())?;
(host.to_string(), parsed_port)
}
None => (host_port.to_string(), 80),
};
if host.is_empty() {
return Err("hôte serveur manquant".to_string());
}
Ok(Self {
host,
port,
base_path,
})
}
pub fn path(&self, endpoint: &str) -> String {
let base = self.base_path.trim_end_matches('/');
let endpoint = endpoint.trim_start_matches('/');
if base.is_empty() {
format!("/{endpoint}")
} else {
format!("{base}/{endpoint}")
}
}
}
pub struct ApiClient {
server: HttpUrl,
token: Option<String>,
}
impl ApiClient {
pub fn new(server_url: &str, token: Option<String>) -> Result<Self, String> {
Ok(Self {
server: HttpUrl::parse(server_url)?,
token,
})
}
pub fn get_capabilities(&self) -> Result<String, String> {
self.get("/api/capabilities")
}
pub fn get_system_status(&self) -> Result<String, String> {
self.get("/api/system/status")
}
pub fn get_system_metrics(&self) -> Result<String, String> {
self.get("/api/system/metrics")
}
pub fn get_machines(&self) -> Result<String, String> {
self.get("/api/machines")
}
fn get(&self, endpoint: &str) -> Result<String, String> {
let path = self.server.path(endpoint);
let mut request = format!(
"GET {path} HTTP/1.1\r\nHost: {}\r\nUser-Agent: system-update-gnome/0.1\r\nAccept: application/json\r\nConnection: close\r\n",
self.server.host
);
if let Some(token) = &self.token {
request.push_str(&format!("Authorization: Bearer {token}\r\n"));
}
request.push_str("\r\n");
let mut stream = TcpStream::connect((&*self.server.host, self.server.port))
.map_err(|err| format!("connexion serveur échouée: {err}"))?;
stream
.write_all(request.as_bytes())
.map_err(|err| format!("envoi requête échoué: {err}"))?;
let mut response = String::new();
stream
.read_to_string(&mut response)
.map_err(|err| format!("lecture réponse échouée: {err}"))?;
split_http_response(&response)
}
}
fn split_http_response(response: &str) -> Result<String, String> {
let (headers, body) = response
.split_once("\r\n\r\n")
.ok_or_else(|| "réponse HTTP invalide".to_string())?;
let status_line = headers.lines().next().unwrap_or_default();
if !status_line.contains(" 200 ") {
return Err(format!("réponse serveur inattendue: {status_line}"));
}
Ok(body.to_string())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn parses_default_http_port() {
let url = HttpUrl::parse("http://localhost").expect("url");
assert_eq!(url.host, "localhost");
assert_eq!(url.port, 80);
assert_eq!(url.path("/api/capabilities"), "/api/capabilities");
}
#[test]
fn parses_explicit_http_port_and_base_path() {
let url = HttpUrl::parse("http://10.0.0.80:8787/system-update").expect("url");
assert_eq!(url.host, "10.0.0.80");
assert_eq!(url.port, 8787);
assert_eq!(
url.path("/api/capabilities"),
"/system-update/api/capabilities"
);
}
#[test]
fn rejects_https_until_tls_client_is_added() {
assert!(HttpUrl::parse("https://10.0.0.80:8787").is_err());
}
#[test]
fn extracts_success_body() {
let body = split_http_response(
"HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n\r\n{\"ok\":true}",
)
.expect("body");
assert_eq!(body, "{\"ok\":true}");
}
}
+122
View File
@@ -0,0 +1,122 @@
use crate::token_store::TokenSource;
use std::env;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct AppConfig {
pub server_url: String,
pub token: Option<String>,
}
impl AppConfig {
pub fn from_args(args: &[String]) -> Result<(Self, Command), String> {
let mut server_url = env::var("SYSTEM_UPDATE_SERVER")
.unwrap_or_else(|_| "http://127.0.0.1:8787".to_string());
let mut token = TokenSource::from_env().load();
let mut command = None;
let mut i = 1;
while i < args.len() {
match args[i].as_str() {
"--server" => {
i += 1;
server_url = args
.get(i)
.ok_or_else(|| "--server attend une URL".to_string())?
.clone();
}
"--token" => {
i += 1;
let raw_token = args
.get(i)
.ok_or_else(|| "--token attend une valeur".to_string())?
.clone();
token = TokenSource::CliArgument(raw_token).load();
}
"capabilities" => command = Some(Command::Capabilities),
"status" => command = Some(Command::Status),
"metrics" => command = Some(Command::Metrics),
"machines" => command = Some(Command::Machines),
"gui" => command = Some(Command::Gui),
"help" | "--help" | "-h" => command = Some(Command::Help),
other => return Err(format!("argument inconnu: {other}")),
}
i += 1;
}
validate_server_url(&server_url)?;
Ok((Self { server_url, token }, command.unwrap_or(Command::Help)))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Command {
Capabilities,
Status,
Metrics,
Machines,
Gui,
Help,
}
pub fn validate_server_url(url: &str) -> Result<(), String> {
if url.starts_with("http://") || url.starts_with("https://") {
Ok(())
} else {
Err("l'URL serveur doit commencer par http:// ou https://".to_string())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn accepts_http_server_url() {
assert!(validate_server_url("http://127.0.0.1:8787").is_ok());
}
#[test]
fn rejects_missing_scheme() {
assert!(validate_server_url("127.0.0.1:8787").is_err());
}
#[test]
fn parses_capabilities_command() {
let args = vec![
"system-update-gnome".to_string(),
"--server".to_string(),
"http://10.0.0.80:8787".to_string(),
"capabilities".to_string(),
];
let (config, command) = AppConfig::from_args(&args).expect("config");
assert_eq!(config.server_url, "http://10.0.0.80:8787");
assert_eq!(command, Command::Capabilities);
}
#[test]
fn parses_status_command() {
let args = vec!["system-update-gnome".to_string(), "status".to_string()];
let (_, command) = AppConfig::from_args(&args).expect("config");
assert_eq!(command, Command::Status);
}
#[test]
fn parses_gui_command() {
let args = vec!["system-update-gnome".to_string(), "gui".to_string()];
let (_, command) = AppConfig::from_args(&args).expect("config");
assert_eq!(command, Command::Gui);
}
#[test]
fn parses_machines_command() {
let args = vec!["system-update-gnome".to_string(), "machines".to_string()];
let (_, command) = AppConfig::from_args(&args).expect("config");
assert_eq!(command, Command::Machines);
}
}
+517
View File
@@ -0,0 +1,517 @@
use crate::api::ApiClient;
use crate::config::AppConfig;
use adw::prelude::*;
use serde::Deserialize;
const APP_CSS: &str = r#"
window { background: #28201b; color: #ead9b8; }
.su-header { background: #241b17; border-bottom: 1px solid #4a3a2f; }
.su-root { background: #28201b; }
.su-sidebar {
background: #30261f;
border-right: 1px solid #5a4738;
padding: 12px;
}
.su-terminal-pane {
background: #181b1d;
border-left: 1px solid #4a3a2f;
}
.su-terminal-head {
background: #241b17;
color: #bdae93;
border-bottom: 1px solid #3a2c24;
padding: 8px 10px;
font-family: monospace;
font-size: 11px;
}
.su-terminal-output {
background: #181b1d;
color: #f6e3b4;
padding: 8px;
font-family: monospace;
font-size: 11px;
}
.su-center { padding: 16px; }
.su-title { font-size: 20px; font-weight: 700; color: #f1d8aa; }
.su-muted { color: #bdae93; font-size: 12px; }
.su-label {
color: #bdae93;
font-family: monospace;
font-size: 10px;
letter-spacing: 2px;
text-transform: uppercase;
}
.su-card {
background: #30261f;
border: 1px solid #6a5544;
border-radius: 8px;
padding: 14px;
box-shadow: 0 6px 14px rgba(0,0,0,.22);
}
.su-dot-ok { background: #6ad13f; border-radius: 999px; min-width: 10px; min-height: 10px; }
.su-dot-unknown { background: #928374; border-radius: 999px; min-width: 10px; min-height: 10px; }
.su-machine-name { font-weight: 700; font-size: 15px; color: #f1e0bc; }
.su-mono { font-family: monospace; color: #bdae93; font-size: 12px; }
.su-taskbar {
background: #241b17;
border-top: 1px solid #5a4738;
min-height: 28px;
}
.su-task-cell {
padding: 5px 12px;
border-right: 1px solid #4a3a2f;
color: #bdae93;
font-family: monospace;
font-size: 11px;
}
.su-task-mode { background: #d79921; color: #28201b; font-weight: 700; }
"#;
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct Machine {
name: String,
hostname: String,
port: u16,
os_family: String,
status: String,
}
pub fn run(config: AppConfig) {
let app = adw::Application::builder()
.application_id("local.system-update.gnome")
.build();
app.connect_activate(move |app| {
build_window(app, config.clone());
});
app.run_with_args::<&str>(&[]);
}
fn build_window(app: &adw::Application, config: AppConfig) {
install_css();
let server_entry = gtk::Entry::builder()
.text(&config.server_url)
.hexpand(true)
.placeholder_text("http://10.0.1.137:8787")
.build();
let terminal = gtk::TextView::builder()
.editable(false)
.monospace(true)
.vexpand(true)
.hexpand(true)
.css_classes(["su-terminal-output"])
.build();
terminal.buffer().set_text(
"Terminal API prêt.\nLes retours capabilities/status/metrics/machines apparaissent ici.",
);
let machines_flow = gtk::FlowBox::builder()
.selection_mode(gtk::SelectionMode::None)
.column_spacing(12)
.row_spacing(12)
.max_children_per_line(3)
.min_children_per_line(1)
.homogeneous(false)
.build();
let task_status = gtk::Label::new(Some("server 10.0.1.137:8787"));
task_status.add_css_class("su-task-cell");
let task_metrics = gtk::Label::new(Some("metrics --"));
task_metrics.add_css_class("su-task-cell");
let add_button = gtk::Button::with_label("+ Ajouter");
let refresh_button = gtk::Button::with_label("Refresh");
let capabilities = gtk::Button::with_label("Capabilities");
let status = gtk::Button::with_label("Status");
let metrics = gtk::Button::with_label("Metrics");
let header = adw::HeaderBar::builder()
.title_widget(&gtk::Label::new(Some("System Update")))
.css_classes(["su-header"])
.build();
let left = build_hermes_panel();
let center = build_center_panel(&machines_flow, &refresh_button, &add_button);
let right = build_terminal_panel(&terminal, &server_entry, &capabilities, &status, &metrics);
let taskbar = build_taskbar(&task_status, &task_metrics);
let body = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.hexpand(true)
.vexpand(true)
.build();
body.append(&left);
body.append(&center);
body.append(&right);
let root = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.css_classes(["su-root"])
.build();
root.append(&header);
root.append(&body);
root.append(&taskbar);
let window = adw::ApplicationWindow::builder()
.application(app)
.title("System Update")
.default_width(1480)
.default_height(760)
.content(&root)
.build();
wire_machine_refresh(
&refresh_button,
&server_entry,
config.token.clone(),
&terminal,
&machines_flow,
&task_status,
);
connect_action(
&capabilities,
&server_entry,
config.token.clone(),
&terminal,
&task_status,
Action::Capabilities,
);
connect_action(
&status,
&server_entry,
config.token.clone(),
&terminal,
&task_status,
Action::Status,
);
connect_action(
&metrics,
&server_entry,
config.token,
&terminal,
&task_status,
Action::Metrics,
);
load_machines(&server_entry, None, &terminal, &machines_flow, &task_status);
window.present();
}
fn install_css() {
let provider = gtk::CssProvider::new();
provider.load_from_data(APP_CSS);
if let Some(display) = gtk::gdk::Display::default() {
gtk::style_context_add_provider_for_display(
&display,
&provider,
gtk::STYLE_PROVIDER_PRIORITY_APPLICATION,
);
}
}
fn build_hermes_panel() -> gtk::Box {
let panel = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.width_request(225)
.css_classes(["su-sidebar"])
.spacing(12)
.build();
let title = gtk::Label::new(Some("HERMES"));
title.set_xalign(0.0);
title.add_css_class("su-label");
let text = gtk::Label::new(Some(
"Copilote d'exploitation -- à venir.\nAnalyse des mises à jour, plans et rapports seront disponibles ici.",
));
text.set_wrap(true);
text.set_xalign(0.0);
text.add_css_class("su-muted");
panel.append(&title);
panel.append(&text);
panel
}
fn build_center_panel(
machines_flow: &gtk::FlowBox,
refresh_button: &gtk::Button,
add_button: &gtk::Button,
) -> gtk::Box {
let center = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.hexpand(true)
.vexpand(true)
.spacing(14)
.css_classes(["su-center"])
.build();
let title = gtk::Label::new(Some("Machines"));
title.set_xalign(0.0);
title.add_css_class("su-title");
let tools = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.spacing(8)
.build();
tools.append(&title);
tools.append(&gtk::Box::builder().hexpand(true).build());
tools.append(refresh_button);
tools.append(add_button);
let scroll = gtk::ScrolledWindow::builder()
.hexpand(true)
.vexpand(true)
.child(machines_flow)
.build();
center.append(&tools);
center.append(&scroll);
center
}
fn build_terminal_panel(
terminal: &gtk::TextView,
server_entry: &gtk::Entry,
capabilities: &gtk::Button,
status: &gtk::Button,
metrics: &gtk::Button,
) -> gtk::Box {
let right = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.width_request(365)
.css_classes(["su-terminal-pane"])
.build();
let head = gtk::Label::new(Some("TERMINAL API"));
head.set_xalign(0.0);
head.add_css_class("su-terminal-head");
let controls = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.spacing(8)
.margin_top(8)
.margin_bottom(8)
.margin_start(8)
.margin_end(8)
.build();
controls.append(server_entry);
let buttons = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.spacing(6)
.build();
buttons.append(capabilities);
buttons.append(status);
buttons.append(metrics);
controls.append(&buttons);
let scroll = gtk::ScrolledWindow::builder()
.hexpand(true)
.vexpand(true)
.child(terminal)
.build();
right.append(&head);
right.append(&controls);
right.append(&scroll);
right
}
fn build_taskbar(task_status: &gtk::Label, task_metrics: &gtk::Label) -> gtk::Box {
let taskbar = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.css_classes(["su-taskbar"])
.build();
let mode = gtk::Label::new(Some(" SYSTEM UPDATE "));
mode.add_css_class("su-task-cell");
mode.add_css_class("su-task-mode");
let scope = gtk::Label::new(Some("machines"));
scope.add_css_class("su-task-cell");
let spacer = gtk::Box::builder().hexpand(true).build();
taskbar.append(&mode);
taskbar.append(&scope);
taskbar.append(task_status);
taskbar.append(&spacer);
taskbar.append(task_metrics);
taskbar
}
fn wire_machine_refresh(
button: &gtk::Button,
server_entry: &gtk::Entry,
token: Option<String>,
terminal: &gtk::TextView,
machines_flow: &gtk::FlowBox,
task_status: &gtk::Label,
) {
let server_entry = server_entry.clone();
let terminal = terminal.clone();
let machines_flow = machines_flow.clone();
let task_status = task_status.clone();
button.connect_clicked(move |_| {
load_machines(
&server_entry,
token.clone(),
&terminal,
&machines_flow,
&task_status,
);
});
}
fn load_machines(
server_entry: &gtk::Entry,
token: Option<String>,
terminal: &gtk::TextView,
machines_flow: &gtk::FlowBox,
task_status: &gtk::Label,
) {
let server_url = server_entry.text().to_string();
let raw = ApiClient::new(&server_url, token).and_then(|client| client.get_machines());
while let Some(child) = machines_flow.first_child() {
machines_flow.remove(&child);
}
match raw {
Ok(json) => {
terminal.buffer().set_text(&json);
match serde_json::from_str::<Vec<Machine>>(&json) {
Ok(machines) => {
for machine in &machines {
machines_flow.insert(&machine_card(machine), -1);
}
task_status.set_text(&format!("{server_url} · {} machines", machines.len()));
}
Err(err) => {
task_status.set_text("machines: JSON invalide");
machines_flow
.insert(&empty_card(&format!("JSON machines invalide: {err}")), -1);
}
}
}
Err(err) => {
terminal.buffer().set_text(&format!("Erreur: {err}"));
task_status.set_text("server erreur");
machines_flow.insert(&empty_card(&err), -1);
}
}
}
fn machine_card(machine: &Machine) -> gtk::Box {
let card = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.spacing(8)
.width_request(230)
.css_classes(["su-card"])
.build();
let row = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.spacing(8)
.build();
let dot = gtk::Box::builder()
.width_request(10)
.height_request(10)
.css_classes([if machine.status == "unknown" {
"su-dot-unknown"
} else {
"su-dot-ok"
}])
.build();
let name = gtk::Label::new(Some(&machine.name));
name.set_xalign(0.0);
name.add_css_class("su-machine-name");
row.append(&dot);
row.append(&name);
let host = gtk::Label::new(Some(&format!(
"{}:{} · {}",
machine.hostname, machine.port, machine.os_family
)));
host.set_xalign(0.0);
host.add_css_class("su-mono");
let updates = gtk::Label::new(Some("UPDATES 0"));
updates.set_xalign(0.0);
updates.add_css_class("su-label");
let buttons = gtk::Box::builder()
.orientation(gtk::Orientation::Horizontal)
.spacing(6)
.build();
buttons.append(&gtk::Button::with_label("Refresh"));
buttons.append(&gtk::Button::with_label("Upgrade"));
buttons.append(&gtk::Button::with_label("Reboot"));
card.append(&row);
card.append(&host);
card.append(&updates);
card.append(&buttons);
card
}
fn empty_card(message: &str) -> gtk::Box {
let card = gtk::Box::builder()
.orientation(gtk::Orientation::Vertical)
.spacing(8)
.width_request(300)
.css_classes(["su-card"])
.build();
let title = gtk::Label::new(Some("Aucune machine"));
title.set_xalign(0.0);
title.add_css_class("su-machine-name");
let text = gtk::Label::new(Some(message));
text.set_wrap(true);
text.set_xalign(0.0);
text.add_css_class("su-muted");
card.append(&title);
card.append(&text);
card
}
#[derive(Clone, Copy)]
enum Action {
Capabilities,
Status,
Metrics,
}
fn connect_action(
button: &gtk::Button,
server_entry: &gtk::Entry,
token: Option<String>,
terminal: &gtk::TextView,
task_status: &gtk::Label,
action: Action,
) {
let server_entry = server_entry.clone();
let terminal = terminal.clone();
let task_status = task_status.clone();
button.connect_clicked(move |_| {
let server_url = server_entry.text().to_string();
let body = ApiClient::new(&server_url, token.clone()).and_then(|client| match action {
Action::Capabilities => client.get_capabilities(),
Action::Status => client.get_system_status(),
Action::Metrics => client.get_system_metrics(),
});
match body {
Ok(json) => {
terminal.buffer().set_text(&json);
task_status.set_text(&format!("{server_url} · ok"));
}
Err(err) => {
terminal.buffer().set_text(&format!("Erreur: {err}"));
task_status.set_text("server erreur");
}
}
});
}
+89
View File
@@ -0,0 +1,89 @@
mod api;
mod config;
#[cfg(feature = "gui")]
mod gui;
mod token_store;
use api::ApiClient;
use config::{AppConfig, Command};
use std::env;
use std::process::ExitCode;
use token_store::keyring_identity;
fn main() -> ExitCode {
let args: Vec<String> = env::args().collect();
match run(&args) {
Ok(()) => ExitCode::SUCCESS,
Err(err) => {
eprintln!("erreur: {err}");
ExitCode::FAILURE
}
}
}
fn run(args: &[String]) -> Result<(), String> {
let (config, command) = AppConfig::from_args(args)?;
match command {
Command::Capabilities => {
let client = ApiClient::new(&config.server_url, config.token)?;
let body = client.get_capabilities()?;
println!("{body}");
}
Command::Status => {
let client = ApiClient::new(&config.server_url, config.token)?;
let body = client.get_system_status()?;
println!("{body}");
}
Command::Metrics => {
let client = ApiClient::new(&config.server_url, config.token)?;
let body = client.get_system_metrics()?;
println!("{body}");
}
Command::Machines => {
let client = ApiClient::new(&config.server_url, config.token)?;
let body = client.get_machines()?;
println!("{body}");
}
Command::Gui => run_gui(config)?,
Command::Help => print_help(),
}
Ok(())
}
#[cfg(feature = "gui")]
fn run_gui(config: AppConfig) -> Result<(), String> {
gui::run(config);
Ok(())
}
#[cfg(not(feature = "gui"))]
fn run_gui(_config: AppConfig) -> Result<(), String> {
Err(
"l'interface graphique n'est pas compilée. Lance: cargo run --features gui -- gui"
.to_string(),
)
}
fn print_help() {
let (keyring_service, keyring_account) = keyring_identity();
println!(
"system-update-gnome\n\
\n\
Usage:\n\
system-update-gnome --server http://127.0.0.1:8787 capabilities\n\
system-update-gnome --server http://127.0.0.1:8787 status\n\
system-update-gnome --server http://127.0.0.1:8787 metrics\n\
system-update-gnome --server http://127.0.0.1:8787 machines\n\
system-update-gnome --server http://127.0.0.1:8787 gui\n\
\n\
Variables:\n\
SYSTEM_UPDATE_SERVER URL du backend, défaut http://127.0.0.1:8787\n\
SYSTEM_UPDATE_TOKEN Token API optionnel\n\
\n\
Futur trousseau:\n\
service: {keyring_service}\n\
compte: {keyring_account}\n"
);
}
@@ -0,0 +1,59 @@
use std::env;
pub const KEYRING_SERVICE: &str = "system-update";
pub const KEYRING_ACCOUNT: &str = "api-token";
pub fn keyring_identity() -> (&'static str, &'static str) {
(KEYRING_SERVICE, KEYRING_ACCOUNT)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum TokenSource {
CliArgument(String),
Environment(Option<String>),
}
impl TokenSource {
pub fn from_env() -> Self {
Self::Environment(env::var("SYSTEM_UPDATE_TOKEN").ok())
}
pub fn load(self) -> Option<String> {
match self {
Self::CliArgument(token) => clean_token(token),
Self::Environment(token) => token.and_then(clean_token),
}
}
}
fn clean_token(token: String) -> Option<String> {
let trimmed = token.trim().to_string();
if trimmed.is_empty() {
None
} else {
Some(trimmed)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn trims_cli_token() {
assert_eq!(
TokenSource::CliArgument(" su_token ".to_string()).load(),
Some("su_token".to_string())
);
}
#[test]
fn ignores_empty_token() {
assert_eq!(TokenSource::CliArgument(" ".to_string()).load(), None);
}
#[test]
fn documents_future_keyring_identity() {
assert_eq!(keyring_identity(), ("system-update", "api-token"));
}
}