1
0

Working Login & Register functionality

This commit is contained in:
2025-02-08 16:35:07 +02:00
parent f65312209c
commit a7a474743c
37 changed files with 1208 additions and 802 deletions

217
src/Cargo.lock generated
View File

@ -273,7 +273,7 @@ checksum = "3b43422f69d8ff38f95f1b2bb76517c91589a924d1559a0e935d7c8ce0274c11"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@ -302,13 +302,13 @@ checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de"
[[package]] [[package]]
name = "async-trait" name = "async-trait"
version = "0.1.85" version = "0.1.86"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3f934833b4b7233644e5848f235df3f57ed8c80f1528a26c3dfa13d2147fa056" checksum = "644dd749086bf3771a2fbc5f256fdb982d53f011c7d5d560304eafeecebce79d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@ -464,7 +464,7 @@ checksum = "3fa76293b4f7bb636ab88fd78228235b5248b4d05cc589aed610f954af5d7c7a"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@ -475,9 +475,9 @@ checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b"
[[package]] [[package]]
name = "bytes" name = "bytes"
version = "1.9.0" version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "325918d6fe32f23b19878fe4b34794ae41fc19ddbe53b10571a4874d44ffd39b" checksum = "f61dac84819c6588b558454b194026eb1f09c293b9036ae9b159e74e73ab6cf9"
[[package]] [[package]]
name = "calloop" name = "calloop"
@ -516,9 +516,9 @@ dependencies = [
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.2.10" version = "1.2.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "13208fcbb66eaeffe09b99fffbe1af420f00a7b35aa99ad683dfc1aa76145229" checksum = "755717a7de9ec452bf7f3f1a3099085deabd7f2962b861dae91ecd7a365903d2"
dependencies = [ dependencies = [
"jobserver", "jobserver",
"libc", "libc",
@ -905,6 +905,7 @@ version = "0.1.0"
dependencies = [ dependencies = [
"chrono", "chrono",
"derive_more", "derive_more",
"futures",
"sqlx", "sqlx",
] ]
@ -927,22 +928,22 @@ dependencies = [
[[package]] [[package]]
name = "derive_more" name = "derive_more"
version = "1.0.0" version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a9b99b9cbbe49445b21764dc0625032a89b145a2642e67603e1c936f5458d05" checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
dependencies = [ dependencies = [
"derive_more-impl", "derive_more-impl",
] ]
[[package]] [[package]]
name = "derive_more-impl" name = "derive_more-impl"
version = "1.0.0" version = "2.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cb7330aeadfbe296029522e6c40f315320aba36fc43a5b3632f3795348f3bd22" checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@ -997,7 +998,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@ -1105,7 +1106,7 @@ checksum = "fc4caf64a58d7a6d65ab00639b046ff54399a39f5f2554728895ace4b297cd79"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@ -1279,7 +1280,7 @@ checksum = "1a5c6c585bc94aaf2c7b51dd4c2ba22680844aba4c687be581871a6f518c5742"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@ -1378,7 +1379,7 @@ checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@ -1434,7 +1435,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"regex", "regex",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@ -1764,7 +1765,7 @@ dependencies = [
"num-traits", "num-traits",
"once_cell", "once_cell",
"palette", "palette",
"rustc-hash 2.1.0", "rustc-hash 2.1.1",
"smol_str", "smol_str",
"thiserror 1.0.69", "thiserror 1.0.69",
"web-time", "web-time",
@ -1779,7 +1780,7 @@ dependencies = [
"futures", "futures",
"iced_core", "iced_core",
"log", "log",
"rustc-hash 2.1.0", "rustc-hash 2.1.1",
"tokio", "tokio",
"wasm-bindgen-futures", "wasm-bindgen-futures",
"wasm-timer", "wasm-timer",
@ -1794,7 +1795,7 @@ dependencies = [
"cosmic-text", "cosmic-text",
"etagere", "etagere",
"lru", "lru",
"rustc-hash 2.1.0", "rustc-hash 2.1.1",
"wgpu", "wgpu",
] ]
@ -1813,7 +1814,7 @@ dependencies = [
"log", "log",
"once_cell", "once_cell",
"raw-window-handle", "raw-window-handle",
"rustc-hash 2.1.0", "rustc-hash 2.1.1",
"thiserror 1.0.69", "thiserror 1.0.69",
"unicode-segmentation", "unicode-segmentation",
] ]
@ -1855,7 +1856,7 @@ dependencies = [
"iced_graphics", "iced_graphics",
"kurbo", "kurbo",
"log", "log",
"rustc-hash 2.1.0", "rustc-hash 2.1.1",
"softbuffer", "softbuffer",
"tiny-skia", "tiny-skia",
] ]
@ -1875,7 +1876,7 @@ dependencies = [
"iced_graphics", "iced_graphics",
"log", "log",
"once_cell", "once_cell",
"rustc-hash 2.1.0", "rustc-hash 2.1.1",
"thiserror 1.0.69", "thiserror 1.0.69",
"wgpu", "wgpu",
] ]
@ -1891,7 +1892,7 @@ dependencies = [
"num-traits", "num-traits",
"once_cell", "once_cell",
"ouroboros", "ouroboros",
"rustc-hash 2.1.0", "rustc-hash 2.1.1",
"thiserror 1.0.69", "thiserror 1.0.69",
"unicode-segmentation", "unicode-segmentation",
] ]
@ -1906,7 +1907,7 @@ dependencies = [
"iced_graphics", "iced_graphics",
"iced_runtime", "iced_runtime",
"log", "log",
"rustc-hash 2.1.0", "rustc-hash 2.1.1",
"thiserror 1.0.69", "thiserror 1.0.69",
"tracing", "tracing",
"wasm-bindgen-futures", "wasm-bindgen-futures",
@ -2031,7 +2032,7 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@ -2250,14 +2251,6 @@ version = "0.12.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38" checksum = "234cf4f4a04dc1f57e24b96cc0cd600cf2af460d4161ac5ecdd0af8e1f3b2a38"
[[package]]
name = "main"
version = "0.1.0"
dependencies = [
"iced",
"strum",
]
[[package]] [[package]]
name = "malloc_buf" name = "malloc_buf"
version = "0.0.6" version = "0.0.6"
@ -2484,7 +2477,7 @@ dependencies = [
"proc-macro-crate", "proc-macro-crate",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@ -2720,9 +2713,9 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.20.2" version = "1.20.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" checksum = "945462a4b81e43c4e3ba96bd7b49d834c6f61198356aa858733bc4acf3cbe62e"
[[package]] [[package]]
name = "orbclient" name = "orbclient"
@ -2774,7 +2767,7 @@ dependencies = [
"proc-macro2", "proc-macro2",
"proc-macro2-diagnostics", "proc-macro2-diagnostics",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@ -2807,7 +2800,7 @@ dependencies = [
"by_address", "by_address",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@ -2926,7 +2919,7 @@ dependencies = [
"phf_shared", "phf_shared",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@ -2940,22 +2933,22 @@ dependencies = [
[[package]] [[package]]
name = "pin-project" name = "pin-project"
version = "1.1.8" version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e2ec53ad785f4d35dac0adea7f7dc6f1bb277ad84a680c7afefeae05d1f5916" checksum = "dfe2e71e1471fe07709406bf725f710b02927c9c54b2b5b2ec0e8087d97c327d"
dependencies = [ dependencies = [
"pin-project-internal", "pin-project-internal",
] ]
[[package]] [[package]]
name = "pin-project-internal" name = "pin-project-internal"
version = "1.1.8" version = "1.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d56a66c0c55993aa927429d0f8a0abfd74f084e4d9c192cffed01e418d83eefb" checksum = "f6e859e6e5bd50440ab63c47e3ebabc90f26251f7c73c3d3e837b74a1cc3fa67"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@ -3077,7 +3070,7 @@ checksum = "af066a9c399a26e020ada66a034357a868728e72cd426f3adcd35f80d88d88c8"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
"version_check", "version_check",
"yansi", "yansi",
] ]
@ -3090,9 +3083,9 @@ checksum = "afbdc74edc00b6f6a218ca6a5364d6226a259d4b8ea1af4a0ea063f27e179f4d"
[[package]] [[package]]
name = "quick-xml" name = "quick-xml"
version = "0.36.2" version = "0.37.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7649a7b4df05aed9ea7ec6f628c67c9953a43869b8bc50929569b2999d443fe" checksum = "165859e9e55f79d67b96c5d96f4e88b6f2695a1972849c15a6a3f5c59fc2c003"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@ -3257,6 +3250,15 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
[[package]]
name = "repo"
version = "0.1.0"
dependencies = [
"data",
"iced",
"service",
]
[[package]] [[package]]
name = "roxmltree" name = "roxmltree"
version = "0.20.0" version = "0.20.0"
@ -3307,9 +3309,9 @@ checksum = "08d43f7aa6b08d49f382cde6a7982047c3426db949b1424bc4b7ec9ae12c6ce2"
[[package]] [[package]]
name = "rustc-hash" name = "rustc-hash"
version = "2.1.0" version = "2.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c7fb8039b3032c191086b10f11f319a6e99e1e82889c5cc6046f515c9db1d497" checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d"
[[package]] [[package]]
name = "rustix" name = "rustix"
@ -3410,7 +3412,7 @@ checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@ -3433,7 +3435,7 @@ checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@ -3727,7 +3729,7 @@ dependencies = [
"quote", "quote",
"sqlx-core", "sqlx-core",
"sqlx-macros-core", "sqlx-macros-core",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@ -3750,7 +3752,7 @@ dependencies = [
"sqlx-mysql", "sqlx-mysql",
"sqlx-postgres", "sqlx-postgres",
"sqlx-sqlite", "sqlx-sqlite",
"syn 2.0.96", "syn 2.0.98",
"tempfile", "tempfile",
"tokio", "tokio",
"url", "url",
@ -3890,28 +3892,6 @@ dependencies = [
"unicode-properties", "unicode-properties",
] ]
[[package]]
name = "strum"
version = "0.26.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fec0f0aef304996cf250b31b5a10dee7980c85da9d759361292b8bca5a18f06"
dependencies = [
"strum_macros",
]
[[package]]
name = "strum_macros"
version = "0.26.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be"
dependencies = [
"heck 0.5.0",
"proc-macro2",
"quote",
"rustversion",
"syn 2.0.96",
]
[[package]] [[package]]
name = "subtle" name = "subtle"
version = "2.6.1" version = "2.6.1"
@ -3948,9 +3928,9 @@ dependencies = [
[[package]] [[package]]
name = "syn" name = "syn"
version = "2.0.96" version = "2.0.98"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" checksum = "36147f1a48ae0ec2b5b3bc5b537d267457555a10dc06f3dbc8cb11ba3006d3b1"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -3965,7 +3945,7 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@ -4026,7 +4006,7 @@ checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@ -4037,7 +4017,7 @@ checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@ -4138,9 +4118,9 @@ checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41"
[[package]] [[package]]
name = "toml_edit" name = "toml_edit"
version = "0.22.22" version = "0.22.23"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" checksum = "02a8b472d1a3d7c18e2d61a489aee3453fd9031c33e4f55bd533f4a7adca1bee"
dependencies = [ dependencies = [
"indexmap", "indexmap",
"toml_datetime", "toml_datetime",
@ -4167,7 +4147,7 @@ checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@ -4318,13 +4298,6 @@ version = "0.9.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a"
[[package]]
name = "view"
version = "0.1.0"
dependencies = [
"iced",
]
[[package]] [[package]]
name = "walkdir" name = "walkdir"
version = "2.5.0" version = "2.5.0"
@ -4378,7 +4351,7 @@ dependencies = [
"log", "log",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -4413,7 +4386,7 @@ checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
"wasm-bindgen-backend", "wasm-bindgen-backend",
"wasm-bindgen-shared", "wasm-bindgen-shared",
] ]
@ -4444,9 +4417,9 @@ dependencies = [
[[package]] [[package]]
name = "wayland-backend" name = "wayland-backend"
version = "0.3.7" version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "056535ced7a150d45159d3a8dc30f91a2e2d588ca0b23f70e56033622b8016f6" checksum = "b7208998eaa3870dad37ec8836979581506e0c5c64c20c9e79e9d2a10d6f47bf"
dependencies = [ dependencies = [
"cc", "cc",
"downcast-rs", "downcast-rs",
@ -4458,9 +4431,9 @@ dependencies = [
[[package]] [[package]]
name = "wayland-client" name = "wayland-client"
version = "0.31.7" version = "0.31.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b66249d3fc69f76fd74c82cc319300faa554e9d865dab1f7cd66cc20db10b280" checksum = "c2120de3d33638aaef5b9f4472bff75f07c56379cf76ea320bd3a3d65ecaf73f"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.8.0",
"rustix", "rustix",
@ -4481,9 +4454,9 @@ dependencies = [
[[package]] [[package]]
name = "wayland-cursor" name = "wayland-cursor"
version = "0.31.7" version = "0.31.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "32b08bc3aafdb0035e7fe0fdf17ba0c09c268732707dca4ae098f60cb28c9e4c" checksum = "a93029cbb6650748881a00e4922b076092a6a08c11e7fbdb923f064b23968c5d"
dependencies = [ dependencies = [
"rustix", "rustix",
"wayland-client", "wayland-client",
@ -4492,9 +4465,9 @@ dependencies = [
[[package]] [[package]]
name = "wayland-protocols" name = "wayland-protocols"
version = "0.32.5" version = "0.32.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd0ade57c4e6e9a8952741325c30bf82f4246885dca8bf561898b86d0c1f58e" checksum = "0781cf46869b37e36928f7b432273c0995aa8aed9552c556fb18754420541efc"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.8.0",
"wayland-backend", "wayland-backend",
@ -4504,9 +4477,9 @@ dependencies = [
[[package]] [[package]]
name = "wayland-protocols-plasma" name = "wayland-protocols-plasma"
version = "0.3.5" version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b31cab548ee68c7eb155517f2212049dc151f7cd7910c2b66abfd31c3ee12bd" checksum = "7ccaacc76703fefd6763022ac565b590fcade92202492381c95b2edfdf7d46b3"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.8.0",
"wayland-backend", "wayland-backend",
@ -4517,9 +4490,9 @@ dependencies = [
[[package]] [[package]]
name = "wayland-protocols-wlr" name = "wayland-protocols-wlr"
version = "0.3.5" version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "782e12f6cd923c3c316130d56205ebab53f55d6666b7faddfad36cecaeeb4022" checksum = "248a02e6f595aad796561fa82d25601bd2c8c3b145b1c7453fc8f94c1a58f8b2"
dependencies = [ dependencies = [
"bitflags 2.8.0", "bitflags 2.8.0",
"wayland-backend", "wayland-backend",
@ -4530,9 +4503,9 @@ dependencies = [
[[package]] [[package]]
name = "wayland-scanner" name = "wayland-scanner"
version = "0.31.5" version = "0.31.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "597f2001b2e5fc1121e3d5b9791d3e78f05ba6bfa4641053846248e3a13661c3" checksum = "896fdafd5d28145fce7958917d69f2fd44469b1d4e861cb5961bcbeebc6d1484"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quick-xml", "quick-xml",
@ -4541,9 +4514,9 @@ dependencies = [
[[package]] [[package]]
name = "wayland-sys" name = "wayland-sys"
version = "0.31.5" version = "0.31.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "efa8ac0d8e8ed3e3b5c9fc92c7881406a268e11555abe36493efabe649a29e09" checksum = "dbcebb399c77d5aa9fa5db874806ee7b4eba4e73650948e8f93963f128896615"
dependencies = [ dependencies = [
"dlib", "dlib",
"log", "log",
@ -4974,9 +4947,9 @@ checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec"
[[package]] [[package]]
name = "winit" name = "winit"
version = "0.30.8" version = "0.30.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f5d74280aabb958072864bff6cfbcf9025cf8bfacdde5e32b5e12920ef703b0f" checksum = "a809eacf18c8eca8b6635091543f02a5a06ddf3dad846398795460e6e0ae3cc0"
dependencies = [ dependencies = [
"ahash 0.8.11", "ahash 0.8.11",
"android-activity", "android-activity",
@ -5026,9 +4999,9 @@ dependencies = [
[[package]] [[package]]
name = "winnow" name = "winnow"
version = "0.6.25" version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ad699df48212c6cc6eb4435f35500ac6fd3b9913324f938aea302022ce19d310" checksum = "86e376c75f4f43f44db463cf729e0d3acbf954d13e22c51e26e4c264b4ab545f"
dependencies = [ dependencies = [
"memchr", "memchr",
] ]
@ -5168,7 +5141,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
"synstructure", "synstructure",
] ]
@ -5219,7 +5192,7 @@ dependencies = [
"proc-macro-crate", "proc-macro-crate",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
"zvariant_utils", "zvariant_utils",
] ]
@ -5258,7 +5231,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@ -5278,7 +5251,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
"synstructure", "synstructure",
] ]
@ -5307,7 +5280,7 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]
[[package]] [[package]]
@ -5332,7 +5305,7 @@ dependencies = [
"proc-macro-crate", "proc-macro-crate",
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
"zvariant_utils", "zvariant_utils",
] ]
@ -5344,5 +5317,5 @@ checksum = "c51bcff7cc3dbb5055396bcf774748c3dab426b4b8659046963523cee4808340"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
"syn 2.0.96", "syn 2.0.98",
] ]

View File

@ -1,3 +1,14 @@
[package]
name = "repo"
version = "0.1.0"
edition = "2024"
[dependencies]
data = { path = "data" }
service = { path = "service" }
iced = { version = "0.13.1", features = ["tokio", "lazy"] }
[workspace] [workspace]
resolver = "2" resolver = "2"
members = ["data", "main", "service", "view"] members = ["data", "service"]

View File

@ -3,7 +3,8 @@ CREATE DATABASE repository;
USE repository; USE repository;
-- Required info for an account -- Required info for an account
CREATE TABLE Users ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, CREATE TABLE Users (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(31) UNIQUE NOT NULL, name VARCHAR(31) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL, email VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL, password VARCHAR(255) NOT NULL,
@ -15,7 +16,8 @@ CREATE TABLE Users ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
); );
-- Enables multiple packages to have the same base yet different components -- Enables multiple packages to have the same base yet different components
CREATE TABLE PackageBases ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, CREATE TABLE PackageBases (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(127) UNIQUE NOT NULL, name VARCHAR(127) UNIQUE NOT NULL,
description VARCHAR(510) NULL, description VARCHAR(510) NULL,
@ -24,27 +26,35 @@ CREATE TABLE PackageBases ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
); );
-- User roles for working on packages: flagger, packager, submitter, maintainer, etc. -- User roles for working on packages: flagger, packager, submitter, maintainer, etc.
CREATE TABLE PackageBaseRoles ( id TINYINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, CREATE TABLE PackageBaseRoles (
id TINYINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(31) UNIQUE NOT NULL, name VARCHAR(31) UNIQUE NOT NULL,
description VARCHAR(255) NULL description VARCHAR(255) NULL
); );
INSERT INTO PackageBaseRoles (id, name) VALUES
(1, 'submitter'),
(2, 'packager'),
(3, 'maintainer'),
(4, 'flagger');
-- Roles that a user has for a package -- Roles that a user has for a package
CREATE TABLE PackageBaseUserRoles ( CREATE TABLE PackageBaseUserRoles (
base_id INT UNSIGNED, base INT UNSIGNED,
user_id INT UNSIGNED, user INT UNSIGNED,
role_id TINYINT UNSIGNED, role TINYINT UNSIGNED,
comment VARCHAR(255) NULL, comment VARCHAR(255) NULL,
PRIMARY KEY (base_id, user_id, role_id), -- composite key PRIMARY KEY (base, user, role), -- composite key
FOREIGN KEY (base_id) REFERENCES PackageBases(id) ON DELETE CASCADE, FOREIGN KEY (base) REFERENCES PackageBases(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES Users(id) ON DELETE CASCADE, FOREIGN KEY (user) REFERENCES Users(id) ON DELETE CASCADE,
FOREIGN KEY (role_id) REFERENCES PackageBaseRoles(id) ON DELETE CASCADE FOREIGN KEY (role) REFERENCES PackageBaseRoles(id) ON DELETE CASCADE
); );
-- Information about the actual packages -- Information about the actual packages
CREATE TABLE Packages ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, CREATE TABLE Packages (
package_base INT UNSIGNED NOT NULL, id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
base INT UNSIGNED NOT NULL,
name VARCHAR(127) UNIQUE NOT NULL, name VARCHAR(127) UNIQUE NOT NULL,
version VARCHAR(127) NOT NULL, version VARCHAR(127) NOT NULL,
description VARCHAR(255) NULL, description VARCHAR(255) NULL,
@ -54,11 +64,12 @@ CREATE TABLE Packages ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL, updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP NOT NULL,
FOREIGN KEY (package_base) REFERENCES PackageBases (id) ON DELETE CASCADE FOREIGN KEY (base) REFERENCES PackageBases (id) ON DELETE CASCADE
); );
-- depends, makedepends, optdepends, etc. -- depends, makedepends, optdepends, etc.
CREATE TABLE DependencyTypes ( id TINYINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, CREATE TABLE DependencyTypes (
id TINYINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(31) UNIQUE NOT NULL name VARCHAR(31) UNIQUE NOT NULL
); );
INSERT INTO DependencyTypes (id, name) VALUES INSERT INTO DependencyTypes (id, name) VALUES
@ -68,7 +79,8 @@ INSERT INTO DependencyTypes (id, name) VALUES
(4, 'optdepends'); (4, 'optdepends');
-- Track which dependencies a package has -- Track which dependencies a package has
CREATE TABLE PackageDependencies ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, CREATE TABLE PackageDependencies (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
arch VARCHAR(63) NULL, arch VARCHAR(63) NULL,
requirement VARCHAR(255) NULL, requirement VARCHAR(255) NULL,
description VARCHAR(127) NULL, description VARCHAR(127) NULL,
@ -82,7 +94,8 @@ CREATE TABLE PackageDependencies ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
); );
-- conflicts, provides, replaces, etc. -- conflicts, provides, replaces, etc.
CREATE TABLE RelationTypes ( id TINYINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, CREATE TABLE RelationTypes (
id TINYINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
name VARCHAR(31) UNIQUE NOT NULL name VARCHAR(31) UNIQUE NOT NULL
); );
INSERT INTO RelationTypes (id, name) VALUES INSERT INTO RelationTypes (id, name) VALUES
@ -91,9 +104,10 @@ INSERT INTO RelationTypes (id, name) VALUES
(3, 'replaces'); (3, 'replaces');
-- Track which conflicts, provides and replaces a package has -- Track which conflicts, provides and replaces a package has
CREATE TABLE PackageRelations ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, CREATE TABLE PackageRelations (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
arch VARCHAR(63) NULL, arch VARCHAR(63) NULL,
requiremen VARCHAR(255) NULL, requirement VARCHAR(255) NULL,
package INT UNSIGNED NOT NULL, package INT UNSIGNED NOT NULL,
relation_type TINYINT UNSIGNED NOT NULL, relation_type TINYINT UNSIGNED NOT NULL,

View File

@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "INSERT INTO Packages (base, name, version, description, url, flagged_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 8
},
"nullable": []
},
"hash": "3ffe9168c1eb30bb54c65055314655c21f03d04973e4291e6d4b6c7847364659"
}

View File

@ -14,7 +14,7 @@
}, },
{ {
"ordinal": 1, "ordinal": 1,
"name": "package_base", "name": "base",
"type_info": { "type_info": {
"type": "Long", "type": "Long",
"flags": "NOT_NULL | MULTIPLE_KEY | UNSIGNED | NO_DEFAULT_VALUE", "flags": "NOT_NULL | MULTIPLE_KEY | UNSIGNED | NO_DEFAULT_VALUE",

View File

@ -0,0 +1,12 @@
{
"db_name": "MySQL",
"query": "UPDATE Packages SET base = ? WHERE id = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "6b6f842d169e2a9628474edcd6dad10878bb9aaa612f62080d40ba24682b0c3b"
}

View File

@ -14,7 +14,7 @@
}, },
{ {
"ordinal": 1, "ordinal": 1,
"name": "package_base", "name": "base",
"type_info": { "type_info": {
"type": "Long", "type": "Long",
"flags": "NOT_NULL | MULTIPLE_KEY | UNSIGNED | NO_DEFAULT_VALUE", "flags": "NOT_NULL | MULTIPLE_KEY | UNSIGNED | NO_DEFAULT_VALUE",

View File

@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "UPDATE Packages SET package_base = ? WHERE id = ?",
"describe": {
"columns": [],
"parameters": {
"Right": 2
},
"nullable": []
},
"hash": "c8de918b432ce82bc7bf1d09a378f9b092d74c2298ac126f7edbb7d59c536910"
}

View File

@ -1,12 +0,0 @@
{
"db_name": "MySQL",
"query": "INSERT INTO Packages (package_base, name, version, description, url, flagged_at, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
"describe": {
"columns": [],
"parameters": {
"Right": 8
},
"nullable": []
},
"hash": "dfc2574c39f3f5a9afc1bdb642d8bf8a5ce85e7f84fa0dadb53c88cce63e5634"
}

View File

@ -4,18 +4,14 @@ version = "0.1.0"
edition = "2024" edition = "2024"
[dependencies] [dependencies]
derive_more = { version = "1.0.0", features = ["deref", "into"] } derive_more = { version = "2.0.1", features = ["deref", "into"] }
futures = "0.3.31"
chrono = { version = "0.4.39", default-features = false, features = [ chrono = { version = "0.4.39", default-features = false, features = [
"std", "std",
"now", "now",
] } ] }
sqlx = { version = "0.8.3", default-features = false, features = [ sqlx = { version = "0.8.3", default-features = false, features = ["mysql", "macros", "chrono", "runtime-tokio"] }
"mysql",
"macros",
"chrono",
"runtime-tokio",
] }
# thiserror = "2.0.11" # thiserror = "2.0.11"
# garde = { version = "0.22.0", features = ["email", "url", "derive"] } # garde = { version = "0.22.0", features = ["email", "url", "derive"] }

View File

@ -1,4 +1,5 @@
//! `MySQL` adapters. //! `MySQL` adapters.
pub mod base; pub mod base;
pub mod package; pub mod package;
pub mod search;
pub mod user; pub mod user;

View File

@ -12,7 +12,7 @@ where
for<'a> &'a E: Executor<'a, Database = MySql>, for<'a> &'a E: Executor<'a, Database = MySql>,
{ {
} }
impl<E> crate::port::CRUD<E> for BaseAdapter impl<E> crate::port::Crud<E> for BaseAdapter
where where
E: Send, E: Send,
for<'a> &'a E: Executor<'a, Database = MySql>, for<'a> &'a E: Executor<'a, Database = MySql>,

View File

@ -12,7 +12,7 @@ where
for<'a> &'a E: Executor<'a, Database = MySql>, for<'a> &'a E: Executor<'a, Database = MySql>,
{ {
} }
impl<E> crate::port::CRUD<E> for PackageAdapter impl<E> crate::port::Crud<E> for PackageAdapter
where where
E: Send, E: Send,
for<'a> &'a E: Executor<'a, Database = MySql>, for<'a> &'a E: Executor<'a, Database = MySql>,
@ -26,7 +26,7 @@ where
let created_at = Utc::now(); let created_at = Utc::now();
let id = sqlx::query!( let id = sqlx::query!(
"INSERT INTO Packages \ "INSERT INTO Packages \
(package_base, name, version, description, url, flagged_at, created_at, updated_at) \ (base, name, version, description, url, flagged_at, created_at, updated_at) \
VALUES (?, ?, ?, ?, ?, ?, ?, ?)", VALUES (?, ?, ?, ?, ?, ?, ?, ?)",
data.package_base.id, data.package_base.id,
data.name.as_str(), data.name.as_str(),
@ -43,7 +43,7 @@ where
Ok(Self::Existing { Ok(Self::Existing {
id, id,
package_base: data.package_base.id, base: data.package_base.id,
name: data.name.into(), name: data.name.into(),
version: data.version.into(), version: data.version.into(),
description: data.description.into(), description: data.description.into(),
@ -88,7 +88,7 @@ where
} }
Field::PackageBase(package_base) => { Field::PackageBase(package_base) => {
sqlx::query!( sqlx::query!(
"UPDATE Packages SET package_base = ? WHERE id = ?", "UPDATE Packages SET base = ? WHERE id = ?",
package_base.id, package_base.id,
existing.id existing.id
) )
@ -107,7 +107,7 @@ where
existing.id existing.id
) )
} }
Field::URL(url) => { Field::Url(url) => {
sqlx::query!( sqlx::query!(
"UPDATE Packages SET url = ? WHERE id = ?", "UPDATE Packages SET url = ? WHERE id = ?",
url.as_ref(), url.as_ref(),
@ -135,10 +135,10 @@ where
match data { match data {
Field::Name(s) => existing.name = s.into(), Field::Name(s) => existing.name = s.into(),
Field::PackageBase(s) => existing.package_base = s.id, Field::PackageBase(s) => existing.base = s.id,
Field::Version(s) => existing.version = s.into(), Field::Version(s) => existing.version = s.into(),
Field::Description(o) => existing.description = o.into(), Field::Description(o) => existing.description = o.into(),
Field::URL(o) => existing.url = o.into(), Field::Url(o) => existing.url = o.into(),
Field::FlaggedAt(date_time) => existing.flagged_at = date_time, Field::FlaggedAt(date_time) => existing.flagged_at = date_time,
Field::CreatedAt(date_time) => existing.created_at = date_time, Field::CreatedAt(date_time) => existing.created_at = date_time,
Field::UpdatedAt(date_time) => existing.updated_at = date_time, Field::UpdatedAt(date_time) => existing.updated_at = date_time,

View File

@ -0,0 +1,152 @@
use crate::port::search::{Data, Entry, Mode, Order, SearchRepository};
use crate::{Result, adapter::mysql::search};
// use chrono::Utc;
use futures::TryStreamExt;
use sqlx::{Executor, MySql, QueryBuilder, Row};
pub struct UserAdapter;
impl<E> SearchRepository<E> for UserAdapter
where
E: Send,
for<'a> &'a E: Executor<'a, Database = MySql>,
{
async fn search(connection: &E, data: Data) -> Result<Vec<Entry>> {
let mut builder = QueryBuilder::new(
"SELECT \
p.id, p.name, p.version, p.url, p.description, \
p.updated_at, p.created_at, \
pb.id AS base_id, pb.name AS base_name, \
( \
SELECT COUNT(DISTINCT pbur.user) \
FROM PackageBaseUserRoles pbur \
WHERE pbur.base = pb.id AND pbur.role = 3 \
) AS maintainers_num \
FROM \
Packages p \
JOIN \
PackageBases pb ON p.base = pb.id ",
);
let mut push_search = |cond, param| {
builder.push(format_args!(
" {cond} {param} {} ",
if data.exact { "=" } else { "LIKE" }
));
builder.push_bind(if data.exact {
data.search.to_string()
} else {
format!("%{}%", data.search.as_str())
});
};
let join_user = " JOIN PackageBaseUserRoles pbur ON pb.id = pbur.base \
JOIN Users u ON pbur.user = u.id WHERE ";
match data.mode {
Mode::Url => push_search("WHERE", "p.url"),
Mode::Name => push_search("WHERE", "p.name"),
Mode::PackageBase => push_search("WHERE", "pb.name"),
Mode::Description => push_search("WHERE", "p.description"),
Mode::BaseDescription => push_search("WHERE", "pb.description"),
Mode::NameAndDescription => {
// WHERE (p.name LIKE '%search_term%' OR p.description LIKE '%search_term%')
builder.push(" WHERE p.name LIKE ");
builder.push_bind(format!("%{}%", data.search.as_str()));
builder.push(" OR p.description LIKE ");
builder.push_bind(format!("%{}%", data.search.as_str()));
}
Mode::User => {
push_search(
"WHERE EXISTS ( \
SELECT 1 \
FROM PackageBaseUserRoles pbur \
JOIN Users u ON pbur.user = u.id \
WHERE pbur.base = pb.id AND",
"u.name",
);
builder.push(" ) ");
}
Mode::Flagger => {
push_search(join_user, "u.name");
builder.push(" AND pbur.role = 4 ");
} // 4
Mode::Packager => {
push_search(join_user, "u.name");
builder.push(" AND pbur.role = 2 ");
} // 2
Mode::Submitter => {
push_search(join_user, "u.name");
builder.push(" AND pbur.role = 1 ");
} // 1
Mode::Maintainer => {
push_search(join_user, "u.name");
builder.push(" AND pbur.role = 3 ");
} // 3
}
builder.push(format_args!(
" ORDER BY {} {} LIMIT {};",
match data.order {
Order::Name => "p.name",
Order::Version => "p.version",
Order::BaseName => "pb.name",
Order::UpdatedAt => "p.updated_at",
Order::CreatedAt => "p.created_at",
},
if data.ascending { "ASC" } else { "DESC" },
data.limit
));
let mut entries = Vec::new();
let mut rows = builder.build().fetch(connection);
while let Some(row) = rows.try_next().await? {
entries.push(Entry {
id: row.try_get("id")?,
name: row.try_get("name")?,
version: row.try_get("version")?,
base_id: row.try_get("base_id")?,
base_name: row.try_get("base_name")?,
url: row.try_get("url")?,
description: row.try_get("description")?,
submitter_id: row.try_get("submitter_id")?,
submitter_name: row.try_get("submitter_name")?,
updated_at: row.try_get("updated_at")?,
created_at: row.try_get("created_at")?,
});
}
Ok(entries)
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Validation;
use crate::port::search::Search;
use sqlx::MySqlPool;
#[sqlx::test]
async fn search() -> crate::Result {
let pool = MySqlPool::connect_lazy(
&std::env::var("DATABASE_URL")
.expect("environment variable `DATABASE_URL` should be set"),
)?;
let data = Data {
mode: Mode::NameAndDescription,
order: Order::UpdatedAt,
search: Search::new("f")?,
limit: 50,
exact: true,
ascending: false,
};
UserAdapter::search(&pool, data).await?;
Ok(())
}
}

View File

@ -12,7 +12,7 @@ where
for<'a> &'a E: Executor<'a, Database = MySql>, for<'a> &'a E: Executor<'a, Database = MySql>,
{ {
} }
impl<E> crate::port::CRUD<E> for UserAdapter impl<E> crate::port::Crud<E> for UserAdapter
where where
E: Send, E: Send,
for<'a> &'a E: Executor<'a, Database = MySql>, for<'a> &'a E: Executor<'a, Database = MySql>,

View File

@ -10,12 +10,13 @@ pub type Result<T = (), E = BoxDynError> = std::result::Result<T, E>;
pub use chrono::Utc; pub use chrono::Utc;
pub use atomic::Atomic;
pub use connect::*;
pub use adapter::mysql::base::BaseAdapter as MySqlBaseAdapter; pub use adapter::mysql::base::BaseAdapter as MySqlBaseAdapter;
pub use adapter::mysql::package::PackageAdapter as MySqlPackageAdapter; pub use adapter::mysql::package::PackageAdapter as MySqlPackageAdapter;
pub use adapter::mysql::user::UserAdapter as MySqlUserAdapter; pub use adapter::mysql::user::UserAdapter as MySqlUserAdapter;
pub use port::base::{self, Base, BaseRepository}; pub use atomic::Atomic;
pub use port::package::{self, Package, PackageRepository}; pub use connect::*;
pub use port::user::{self, User, UserRepository}; pub use port::base::{Base, BaseRepository};
pub use port::package::{Package, PackageRepository};
pub use port::user::{User, UserRepository};
pub use port::*;

View File

@ -1,13 +1,14 @@
//! Low-level repository traits for unified data access. //! Low-level repository traits for unified data access.
//! //!
//! No data validation besides very basic one like length violation. //! Very mild argument validation.
use crate::Result; use crate::{BoxDynError, Result};
pub mod base; pub mod base;
pub mod package; pub mod package;
pub mod search;
pub mod user; pub mod user;
pub trait CRUD<C> { pub trait Crud<C> {
type New; type New;
type Unique; type Unique;
type Update; type Update;
@ -16,23 +17,27 @@ pub trait CRUD<C> {
fn create( fn create(
connection: &mut C, connection: &mut C,
data: Self::New, data: Self::New,
) -> impl Future<Output = crate::Result<Self::Existing>> + Send; ) -> impl Future<Output = Result<Self::Existing>> + Send;
fn read( fn read(
connection: &C, connection: &C,
data: Self::Unique, data: Self::Unique,
) -> impl Future<Output = crate::Result<Option<Self::Existing>>> + Send; ) -> impl Future<Output = Result<Option<Self::Existing>>> + Send;
fn update( fn update(
connection: &mut C, connection: &mut C,
existing: &mut Self::Existing, existing: &mut Self::Existing,
data: Self::Update, data: Self::Update,
) -> impl Future<Output = crate::Result> + Send; ) -> impl Future<Output = Result> + Send;
fn delete(connection: &mut C, data: Self::Unique) fn delete(connection: &mut C, data: Self::Unique) -> impl Future<Output = Result> + Send;
-> impl Future<Output = crate::Result> + Send;
} }
trait CharLength { pub trait CharLength {
fn length(&self) -> usize; fn length(&self) -> usize;
} }
impl CharLength for &str {
fn length(&self) -> usize {
self.chars().count()
}
}
impl CharLength for String { impl CharLength for String {
fn length(&self) -> usize { fn length(&self) -> usize {
self.chars().count() self.chars().count()
@ -44,15 +49,43 @@ impl CharLength for Option<String> {
} }
} }
trait MaxLength { trait Validatable {
type Inner: CharLength; type Inner: CharLength;
const MAX_LENGTH: usize; const MAX_LENGTH: usize;
fn encapsulate(value: Self::Inner) -> Self;
}
fn validate(value: &Self::Inner) -> Result<(), &'static str> { #[allow(private_bounds)] // don't expose the impl details
pub trait Validation<T>: Validatable
where
T: CharLength + Into<Self::Inner>,
{
fn valid(value: &T) -> Result<(), String> {
if value.length() > Self::MAX_LENGTH { if value.length() > Self::MAX_LENGTH {
Err("too long") Err(format!(
"too long (length: {}, max length: {})",
value.length(),
Self::MAX_LENGTH
))
} else { } else {
Ok(()) Ok(())
} }
} }
fn new(value: T) -> Result<Self, (T, BoxDynError)>
where
Self: Sized,
{
match Self::valid(&value) {
Ok(()) => Ok(Self::encapsulate(value.into())),
Err(e) => Err((value, e.into())),
}
}
}
impl<T, U> Validation<U> for T
where
T: Validatable,
U: CharLength + Into<T::Inner>,
{
} }

View File

@ -1,10 +1,10 @@
use super::MaxLength; use super::Validatable;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use derive_more::{Deref, Into}; use derive_more::{Deref, Into};
pub trait BaseRepository<C>: pub trait BaseRepository<C>:
super::CRUD<C, New = New, Unique = u64, Update = Field, Existing = Base> super::Crud<C, New = New, Unique = u64, Update = Field, Existing = Base>
{ {
} }
@ -13,33 +13,21 @@ pub trait BaseRepository<C>:
#[derive(Clone, Deref, Into)] #[derive(Clone, Deref, Into)]
pub struct Name(String); pub struct Name(String);
impl MaxLength for Name { impl Validatable for Name {
type Inner = String; type Inner = String;
const MAX_LENGTH: usize = 127; const MAX_LENGTH: usize = 127;
} fn encapsulate(value: Self::Inner) -> Self {
impl TryFrom<String> for Name { Self(value)
type Error = &'static str;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::validate(&value)?;
Ok(Self(value))
} }
} }
#[derive(Clone, Deref, Into)] #[derive(Clone, Deref, Into)]
pub struct Description(Option<String>); pub struct Description(Option<String>);
impl MaxLength for Description { impl Validatable for Description {
type Inner = Option<String>; type Inner = Option<String>;
const MAX_LENGTH: usize = 510; const MAX_LENGTH: usize = 510;
} fn encapsulate(value: Self::Inner) -> Self {
impl TryFrom<Option<String>> for Description { Self(value)
type Error = (Option<String>, &'static str);
fn try_from(value: Option<String>) -> Result<Self, Self::Error> {
match Self::validate(&value) {
Ok(()) => Ok(Self(value)),
Err(e) => Err((value, e)),
}
} }
} }

View File

@ -1,79 +1,51 @@
use super::MaxLength; use super::Validatable;
use crate::Base; use crate::Base;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use derive_more::{Deref, Into}; use derive_more::{Deref, Into};
pub trait PackageRepository<C>: pub trait PackageRepository<C>:
super::CRUD<C, New = New, Update = Field, Unique = Unique, Existing = Package> super::Crud<C, New = New, Update = Field, Unique = Unique, Existing = Package>
{ {
} }
#[derive(Clone, Deref, Into)] #[derive(Clone, Deref, Into)]
pub struct Name(String); pub struct Name(String);
impl MaxLength for Name { impl Validatable for Name {
type Inner = String; type Inner = String;
const MAX_LENGTH: usize = 127; const MAX_LENGTH: usize = 127;
} fn encapsulate(value: Self::Inner) -> Self {
impl TryFrom<String> for Name { Self(value)
type Error = (String, &'static str);
fn try_from(value: String) -> Result<Self, Self::Error> {
match Self::validate(&value) {
Ok(()) => Ok(Self(value)),
Err(e) => Err((value, e)),
}
} }
} }
#[derive(Clone, Deref, Into)] #[derive(Clone, Deref, Into)]
pub struct Version(String); pub struct Version(String);
impl MaxLength for Version { impl Validatable for Version {
type Inner = String; type Inner = String;
const MAX_LENGTH: usize = 127; const MAX_LENGTH: usize = 127;
} fn encapsulate(value: Self::Inner) -> Self {
impl TryFrom<String> for Version { Self(value)
type Error = (String, &'static str);
fn try_from(value: String) -> Result<Self, Self::Error> {
match Self::validate(&value) {
Ok(()) => Ok(Self(value)),
Err(e) => Err((value, e)),
}
} }
} }
#[derive(Clone, Deref, Into)] #[derive(Clone, Deref, Into)]
pub struct Description(Option<String>); pub struct Description(Option<String>);
impl MaxLength for Description { impl Validatable for Description {
type Inner = Option<String>; type Inner = Option<String>;
const MAX_LENGTH: usize = 255; const MAX_LENGTH: usize = 255;
} fn encapsulate(value: Self::Inner) -> Self {
impl TryFrom<Option<String>> for Description { Self(value)
type Error = (Option<String>, &'static str);
fn try_from(value: Option<String>) -> Result<Self, Self::Error> {
match Self::validate(&value) {
Ok(()) => Ok(Self(value)),
Err(e) => Err((value, e)),
}
} }
} }
#[derive(Clone, Deref, Into)] #[derive(Clone, Deref, Into)]
pub struct URL(Option<String>); pub struct Url(Option<String>);
impl MaxLength for URL { impl Validatable for Url {
type Inner = Option<String>; type Inner = Option<String>;
const MAX_LENGTH: usize = 510; const MAX_LENGTH: usize = 510;
} fn encapsulate(value: Self::Inner) -> Self {
impl TryFrom<Option<String>> for URL { Self(value)
type Error = (Option<String>, &'static str);
fn try_from(value: Option<String>) -> Result<Self, Self::Error> {
match Self::validate(&value) {
Ok(()) => Ok(Self(value)),
Err(e) => Err((value, e)),
}
} }
} }
@ -87,7 +59,7 @@ pub enum Field {
Name(Name), Name(Name),
Version(Version), Version(Version),
Description(Description), Description(Description),
URL(URL), Url(Url),
FlaggedAt(Option<DateTime<Utc>>), FlaggedAt(Option<DateTime<Utc>>),
CreatedAt(DateTime<Utc>), CreatedAt(DateTime<Utc>),
UpdatedAt(DateTime<Utc>), UpdatedAt(DateTime<Utc>),
@ -98,13 +70,13 @@ pub struct New {
pub name: Name, pub name: Name,
pub version: Version, pub version: Version,
pub description: Description, pub description: Description,
pub url: URL, pub url: Url,
pub flagged_at: Option<DateTime<Utc>>, pub flagged_at: Option<DateTime<Utc>>,
} }
pub struct Package { pub struct Package {
pub(crate) id: u64, pub(crate) id: u64,
pub(crate) package_base: u64, pub(crate) base: u64,
pub(crate) name: String, pub(crate) name: String,
pub(crate) version: String, pub(crate) version: String,
pub(crate) description: Option<String>, pub(crate) description: Option<String>,
@ -119,7 +91,7 @@ impl Package {
self.id self.id
} }
pub const fn package_base(&self) -> u64 { pub const fn package_base(&self) -> u64 {
self.package_base self.base
} }
pub const fn name(&self) -> &String { pub const fn name(&self) -> &String {
&self.name &self.name

View File

@ -0,0 +1,69 @@
use super::Validatable;
use crate::Result;
use chrono::{DateTime, Utc};
use derive_more::{Deref, Into};
pub trait SearchRepository<C> {
fn search(connection: &C, data: Data) -> impl Future<Output = Result<Vec<Entry>>> + Send;
}
#[derive(Clone, Deref, Into)]
pub struct Search(String);
impl Validatable for Search {
type Inner = String;
const MAX_LENGTH: usize = 255;
fn encapsulate(value: Self::Inner) -> Self {
Self(value)
}
}
pub struct Data {
pub mode: Mode,
pub order: Order,
pub search: Search,
pub limit: u8,
pub exact: bool,
pub ascending: bool,
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub struct Entry {
pub id: u64,
pub name: Box<str>,
pub version: Box<str>,
pub base_id: u64,
pub base_name: Box<str>,
pub url: Option<Box<str>>,
pub description: Box<str>,
pub submitter_id: u64,
pub submitter_name: Box<str>,
pub updated_at: DateTime<Utc>,
pub created_at: DateTime<Utc>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Mode {
Url,
Name,
PackageBase,
Description,
BaseDescription,
NameAndDescription,
User,
Flagger,
Packager,
Submitter,
Maintainer,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Order {
Name,
Version,
BaseName,
// Submitter,
UpdatedAt,
CreatedAt,
}

View File

@ -1,61 +1,40 @@
use super::MaxLength; use super::Validatable;
use chrono::{DateTime, Utc}; use chrono::{DateTime, Utc};
use derive_more::{Deref, Into}; use derive_more::{Deref, Into};
pub trait UserRepository<C>: pub trait UserRepository<C>:
super::CRUD<C, New = New, Update = Field, Unique = Unique, Existing = User> super::Crud<C, New = New, Update = Field, Unique = Unique, Existing = User>
{ {
} }
#[derive(Clone, Deref, Into)] #[derive(Clone, Deref, Into)]
pub struct Name(String); pub struct Name(String);
impl MaxLength for Name { impl Validatable for Name {
type Inner = String; type Inner = String;
const MAX_LENGTH: usize = 31; const MAX_LENGTH: usize = 31;
} fn encapsulate(value: Self::Inner) -> Self {
impl TryFrom<String> for Name { Self(value)
type Error = (String, &'static str);
fn try_from(value: String) -> Result<Self, Self::Error> {
match Self::validate(&value) {
Ok(()) => Ok(Self(value)),
Err(e) => Err((value, e)),
}
} }
} }
#[derive(Clone, Deref, Into)] #[derive(Clone, Deref, Into)]
pub struct Email(String); pub struct Email(String);
impl MaxLength for Email { impl Validatable for Email {
type Inner = String; type Inner = String;
const MAX_LENGTH: usize = 255; const MAX_LENGTH: usize = 255;
} fn encapsulate(value: Self::Inner) -> Self {
impl TryFrom<String> for Email { Self(value)
type Error = (String, &'static str);
fn try_from(value: String) -> Result<Self, Self::Error> {
match Self::validate(&value) {
Ok(()) => Ok(Self(value)),
Err(e) => Err((value, e)),
}
} }
} }
#[derive(Clone, Deref, Into)] #[derive(Clone, Deref, Into)]
pub struct Password(String); pub struct Password(String);
impl MaxLength for Password { impl Validatable for Password {
type Inner = String; type Inner = String;
const MAX_LENGTH: usize = 255; const MAX_LENGTH: usize = 255;
} fn encapsulate(value: Self::Inner) -> Self {
impl TryFrom<String> for Password { Self(value)
type Error = (String, &'static str);
fn try_from(value: String) -> Result<Self, Self::Error> {
match Self::validate(&value) {
Ok(()) => Ok(Self(value)),
Err(e) => Err((value, e)),
}
} }
} }
@ -81,7 +60,7 @@ pub struct New {
pub last_used: Option<DateTime<Utc>>, pub last_used: Option<DateTime<Utc>>,
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct User { pub struct User {
pub(crate) id: u64, pub(crate) id: u64,
pub(crate) name: String, pub(crate) name: String,

View File

@ -7,7 +7,7 @@ edition = "2024"
thiserror = "2.0.11" thiserror = "2.0.11"
argon2 = { version = "0.5.3", features = ["std"] } argon2 = { version = "0.5.3", features = ["std"] }
garde = { version = "0.22.0", features = ["email", "url", "derive"] } garde = { version = "0.22.0", features = ["email", "url", "derive"] }
derive_more = { version = "1.0.0", features = ["deref", "deref_mut", "into"] } derive_more = { version = "2.0.1", features = ["deref", "deref_mut", "into"] }
[dependencies.data] [dependencies.data]
path = "../data" path = "../data"

View File

@ -11,6 +11,7 @@ where
UR: UserRepository<C> + Sync, UR: UserRepository<C> + Sync,
{ {
driver: D, driver: D,
// connection: Option<C>,
_user_repository: PhantomData<UR>, _user_repository: PhantomData<UR>,
} }
@ -23,14 +24,24 @@ where
pub const fn new(driver: D) -> Self { pub const fn new(driver: D) -> Self {
Self { Self {
driver, driver,
// connection: None,
_user_repository: PhantomData, _user_repository: PhantomData,
} }
} }
// async fn connection(&mut self) -> Result<&C> {
// if let Some(ref c) = self.connection {
// Ok(c)
// } else {
// self.connection = Some(self.driver.open_connection().await?);
// self.connection().await
// }
// }
} }
impl<D, C, UR> AuthenticationRepository for AuthenticationAdapter<D, C, UR> impl<D, C, UR> AuthenticationRepository for AuthenticationAdapter<D, C, UR>
where where
C: Send, C: Send, //+ Sync,
D: Connect<Connection = C> + Sync, D: Connect<Connection = C> + Sync,
UR: UserRepository<C> + Sync, UR: UserRepository<C> + Sync,
{ {

View File

@ -1,12 +1,13 @@
use super::Authenticated; use super::Authenticated;
use data::user; pub use data::Validation;
use data::{BoxDynError, user};
use derive_more::{Deref, Into}; use derive_more::{Deref, Into};
use garde::Validate; use garde::Validate;
pub type Result<T = (), E = Error> = std::result::Result<T, E>; pub type Result<T = (), E = Error> = std::result::Result<T, E>;
pub trait AuthenticationContract { pub trait AuthenticationContract: Send {
fn name_available(&self, name: Name) -> impl Future<Output = Result> + Send; fn name_available(&self, name: Name) -> impl Future<Output = Result> + Send;
fn email_available(&self, email: Email) -> impl Future<Output = Result> + Send; fn email_available(&self, email: Email) -> impl Future<Output = Result> + Send;
@ -45,78 +46,110 @@ pub enum Error {
InvalidPassword(data::BoxDynError), InvalidPassword(data::BoxDynError),
#[error("data source error: {0}")] #[error("data source error: {0}")]
Repository(data::BoxDynError), Repository(data::BoxDynError),
#[error(transparent)]
Other(data::BoxDynError),
} }
pub type ReturnError<T = String> = (T, BoxDynError);
#[derive(Clone)] #[derive(Clone)]
pub enum Login { pub enum Login {
Name(Name), Name(Name),
Email(Email), Email(Email),
} }
impl AsRef<str> for Login {
fn as_ref(&self) -> &str {
match self {
Self::Name(name) => name.as_ref(),
Self::Email(email) => email.as_ref(),
}
}
}
impl TryFrom<String> for Login { impl TryFrom<String> for Login {
type Error = (String, &'static str); type Error = ReturnError;
fn try_from(value: String) -> Result<Self, Self::Error> { fn try_from(value: String) -> Result<Self, Self::Error> {
let value = match Email::try_from(value) { let value = match Email::try_from(value) {
Ok(x) => return Ok(Self::Email(x)), Ok(x) => return Ok(Self::Email(x)),
Err((s, _)) => s, Err((v, _)) => v,
}; };
match Name::try_from(value) { match Name::try_from(value) {
Ok(x) => Ok(Self::Name(x)), Ok(x) => Ok(Self::Name(x)),
Err((s, _)) => Err((s, "login is invalid")), Err((v, _)) => Err((v, "login is invalid".into())),
}
}
}
impl From<Login> for String {
fn from(val: Login) -> Self {
match val {
Login::Name(name) => name.0.into(),
Login::Email(email) => email.0.into(),
} }
} }
} }
#[derive(Clone, Deref, Into)] #[derive(Clone, Deref, Into)]
pub struct Name(user::Name); pub struct Name(user::Name);
impl AsRef<str> for Name {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl TryFrom<String> for Name { impl TryFrom<String> for Name {
type Error = (String, Box<dyn std::error::Error>); type Error = ReturnError;
fn try_from(value: String) -> Result<Self, Self::Error> { fn try_from(value: String) -> Result<Self, Self::Error> {
#[derive(Validate)] #[derive(Validate)]
#[garde(transparent)] #[garde(transparent)]
struct Username<'a>(#[garde(alphanumeric, length(chars, min = 2, max = 31))] &'a str); struct Username<'a>(#[garde(ascii, length(chars, min = 2, max = 31))] &'a str);
if let Err(e) = Username(&value).validate() { match Username(value.as_str()).validate() {
return Err((value, e.into())); Ok(()) => (),
} Err(e) => return Err((value, e.into())),
match user::Name::try_from(value) {
Ok(x) => Ok(Self(x)),
Err((s, e)) => Err((s, e.into())),
} }
Ok(Self(user::Name::new(value)?))
} }
} }
#[derive(Clone, Deref, Into)] #[derive(Clone, Deref, Into)]
pub struct Email(user::Email); pub struct Email(user::Email);
impl AsRef<str> for Email {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl TryFrom<String> for Email { impl TryFrom<String> for Email {
type Error = (String, Box<dyn std::error::Error>); type Error = ReturnError;
fn try_from(value: String) -> Result<Self, Self::Error> { fn try_from(value: String) -> Result<Self, Self::Error> {
#[derive(Validate)] #[derive(Validate)]
#[garde(transparent)] #[garde(transparent)]
pub struct Email<'a>(#[garde(email, length(chars, max = 255))] &'a str); pub struct Email<'a>(#[garde(email, length(chars, max = 255))] &'a str);
if let Err(e) = Email(&value).validate() { match Email(value.as_str()).validate() {
return Err((value, e.into())); Ok(()) => (),
} Err(e) => return Err((value, e.into())),
match user::Email::try_from(value) {
Ok(x) => Ok(Self(x)),
Err((s, e)) => Err((s, e.into())),
} }
Ok(Self(user::Email::new(value)?))
} }
} }
#[derive(Clone, Deref, Into)] #[derive(Clone, Deref, Into)]
pub struct Password(String); pub struct Password(String);
impl AsRef<str> for Password {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl TryFrom<String> for Password { impl TryFrom<String> for Password {
type Error = (String, &'static str); type Error = ReturnError;
fn try_from(value: String) -> Result<Self, Self::Error> { fn try_from(value: String) -> Result<Self, Self::Error> {
if value.chars().count() >= 8 { if value.chars().count() > 7 {
Ok(Self(value)) Ok(Self(value))
} else { } else {
Err((value, "password must be 8 characters or more")) Err((value, "password must be longer than 7 characters".into()))
} }
} }
} }

View File

@ -3,7 +3,7 @@ use data::user::{Email, Name, New, Unique, User};
use derive_more::{Deref, DerefMut}; use derive_more::{Deref, DerefMut};
#[derive(Deref, DerefMut, Debug)] #[derive(Debug, Clone, Deref, DerefMut)]
pub struct Authenticated(pub(super) User); pub struct Authenticated(pub(super) User);
pub trait AuthenticationRepository { pub trait AuthenticationRepository {

View File

@ -1,6 +1,6 @@
use super::{ use super::{
Authenticated, AuthenticationContract, AuthenticationRepository, Email, Error, Get, Login, Authenticated, AuthenticationContract, AuthenticationRepository, Email, Error, Get, Login,
LoginData, Name, RegisterData, Result, LoginData, Name, RegisterData, Result, Validation,
}; };
use argon2::{ use argon2::{
@ -48,7 +48,7 @@ where
.is_some() .is_some()
{ {
return Err(Error::NameExists); return Err(Error::NameExists);
}; }
Ok(()) Ok(())
} }
async fn email_available(&self, email: Email) -> Result { async fn email_available(&self, email: Email) -> Result {
@ -60,7 +60,7 @@ where
.is_some() .is_some()
{ {
return Err(Error::EmailExists); return Err(Error::EmailExists);
}; }
Ok(()) Ok(())
} }
@ -89,11 +89,11 @@ where
self.email_available(data.email.clone()).await?; self.email_available(data.email.clone()).await?;
// Get PHC string ($argon2id$v=19$...) // Get PHC string ($argon2id$v=19$...)
let password = Argon2::default() let phc = Argon2::default()
.hash_password(data.password.as_bytes(), &SaltString::generate(&mut OsRng))? .hash_password(data.password.as_bytes(), &SaltString::generate(&mut OsRng))?
.to_string() .to_string();
.try_into() let password = data::user::Password::new(phc)
.map_err(|(_, e)| Error::InvalidPassword(Box::from(e)))?; .map_err(|(_, e)| Error::InvalidPassword(e))?;
let user = self let user = self
.repository .repository

View File

@ -4,3 +4,5 @@ pub use authentication::{
Authenticated, AuthenticationAdapter, AuthenticationContract, AuthenticationRepository, Authenticated, AuthenticationAdapter, AuthenticationContract, AuthenticationRepository,
AuthenticationService, AuthenticationService,
}; };
// pub

171
src/src/input.rs Normal file
View File

@ -0,0 +1,171 @@
use crate::widget::text_input::{error, success, warning};
use iced::widget::{TextInput, text_input, text_input::default};
/// A smarter [`text_input`].to avoid boilerplate.
pub struct Input<T> {
id: &'static str,
value: Value<T>,
warning: Option<String>,
}
// use std::ops::Deref;
// impl<T> Deref for Input<T> {
// type Target = Value<T>;
// fn deref(&self) -> &Self::Target {
// &self.value
// }
// }
impl<T: AsRef<str>> AsRef<str> for Input<T> {
fn as_ref(&self) -> &str {
self.value.as_ref()
}
}
impl<T> Input<T> {
pub const fn new(id: &'static str) -> Self {
Self {
id,
value: Value::None,
warning: None,
}
}
pub fn focus<Message>(&self) -> iced::Task<Message> {
iced::widget::text_input::focus(self.id)
}
pub fn warning(&self) -> Option<&str> {
self.warning.as_ref().map(AsRef::as_ref)
}
pub fn error(&self) -> Option<&str> {
match &self.value {
Value::Invalid { error, .. } => Some(error.as_ref()),
_ => None,
}
}
pub fn set_value(&mut self, value: Value<T>) {
self.value = value;
}
pub fn set_warning(&mut self, value: &impl ToString) {
self.warning = Some(value.to_string());
}
}
impl<T: AsRef<str>> Input<T> {
pub fn set_error(&mut self, value: &impl ToString) {
self.value.set_error(value.to_string());
}
pub fn view<Message: Clone>(&self, placeholder: &str) -> TextInput<Message> {
text_input(placeholder, self.value.as_ref())
.id(self.id)
.padding(12)
.style(match self.value {
Value::Invalid { .. } => error,
Value::None | Value::Valid(_) if self.warning.is_some() => warning,
Value::Valid(_) => success,
Value::None => default,
})
}
}
impl<T, E> Input<T>
where
E: ToString,
T: TryFrom<String, Error = (String, E)>,
{
pub fn update(&mut self, value: String) {
self.value.update(value);
self.warning = None;
}
pub fn value(&mut self) -> Result<&T, &str> {
match self.value {
Value::None => {
self.value.update(String::new());
self.value()
}
Value::Valid(ref x) => Ok(x),
Value::Invalid { ref error, .. } => Err(error),
}
}
pub fn submittable(&mut self) -> bool {
self.value().is_ok()
}
pub fn critical(&mut self) -> bool {
self.value().is_err()
}
}
impl<T, E> Input<T>
where
E: ToString,
T: TryFrom<String, Error = (String, E)> + Clone,
{
pub fn submit<Message>(&mut self) -> Result<T, iced::Task<Message>> {
match self.value() {
Ok(x) => Ok(x.clone()),
Err(_) => Err(self.focus()),
}
}
}
#[derive(Default)]
pub enum Value<T> {
#[default]
None,
Valid(T),
Invalid {
value: String,
error: String,
},
}
impl<T: AsRef<str>> AsRef<str> for Value<T> {
fn as_ref(&self) -> &str {
match self {
Self::None => "",
Self::Valid(x) => x.as_ref(),
Self::Invalid { value, .. } => value.as_ref(),
}
}
}
impl<T: AsRef<str>> Value<T> {
fn set_error(&mut self, error: String) {
match self {
Self::None => {
*self = Self::Invalid {
value: String::new(),
error,
}
}
Self::Valid(x) => {
*self = Self::Invalid {
value: x.as_ref().to_string(),
error,
}
}
Self::Invalid { error: e, .. } => *e = error,
}
}
}
impl<T, E> Value<T>
where
E: ToString,
T: TryFrom<String, Error = (String, E)>,
{
fn update(&mut self, value: String) {
*self = match T::try_from(value) {
Ok(x) => Self::Valid(x),
Err((s, e)) => Self::Invalid {
value: s,
error: e.to_string(),
},
};
}
}

177
src/src/login.rs Normal file
View File

@ -0,0 +1,177 @@
use crate::input::Input;
use crate::widget::centerbox;
use service::authentication;
use service::{
Authenticated, AuthenticationContract,
authentication::{Error, LoginData, Result},
};
use iced::futures::lock::Mutex;
use iced::widget::{Space, button, checkbox, column, container, row, text};
use iced::{Length, Task, padding};
use std::sync::Arc;
pub struct Login<S> {
login: Input<authentication::Login>,
password: Input<authentication::Password>,
show_password: bool,
state: State,
service: Arc<Mutex<S>>,
}
enum State {
None,
Requesting,
Error(String),
}
pub enum Event {
SwitchToRegister,
Task(Task<Message>),
Authenticated(Authenticated),
}
impl From<Task<Message>> for Event {
fn from(value: Task<Message>) -> Self {
Self::Task(value)
}
}
#[derive(Debug, Clone)]
pub enum Message {
LoginChanged(String),
PasswordChanged(String),
ShowPasswordToggled(bool),
LoginSubmitted,
PasswordSubmitted,
LoginPressed,
RegisterPressed,
RequestResult(Arc<Result<Authenticated>>),
}
impl<S: AuthenticationContract + 'static> Login<S> {
pub fn new(service: Arc<Mutex<S>>) -> Self {
Self {
login: Input::new("login_name"),
password: Input::new("login_password"),
show_password: false,
state: State::None,
service,
}
}
pub fn update(&mut self, message: Message) -> Option<Event> {
match message {
Message::LoginChanged(s) => self.login.update(s),
Message::PasswordChanged(s) => self.password.update(s),
Message::ShowPasswordToggled(b) => self.show_password = b,
Message::LoginSubmitted if self.login.critical() => (),
Message::LoginSubmitted => return Some(self.password.focus().into()),
Message::RegisterPressed => return Some(Event::SwitchToRegister),
Message::LoginPressed | Message::PasswordSubmitted => {
let login_data = LoginData {
login: match self.login.submit() {
Ok(x) => x,
Err(t) => return Some(t.into()),
},
password: match self.password.submit() {
Ok(x) => x,
Err(t) => return Some(t.into()),
},
};
self.state = State::Requesting;
let arc = self.service.clone();
return Some(
Task::perform(
async move {
let Some(service) = arc.try_lock() else {
return Err(Error::Other(
"other authentication request is being performed".into(),
));
};
service.login(login_data).await
},
|r| Message::RequestResult(Arc::new(r)),
)
.into(),
);
}
Message::RequestResult(r) => match &*r {
Ok(a) => return Some(Event::Authenticated(a.clone())),
Err(e) => {
self.state = State::None;
match e {
Error::LoginNotFound => self.login.set_warning(e),
Error::IncorrectPassword => self.password.set_warning(e),
Error::InvalidPassword(_) => self.password.set_error(e),
_ => self.state = State::Error(e.to_string()),
}
}
},
}
None
}
pub fn view(&self) -> iced::Element<Message> {
centerbox(
column![
container(text(self.title()).size(20))
.center_x(Length::Fill)
.padding(padding::bottom(10)),
self.login
.view("Email or Username")
.on_input(Message::LoginChanged)
.on_submit(Message::LoginSubmitted),
self.password
.view("Password")
.on_input(Message::PasswordChanged)
.on_submit(Message::PasswordSubmitted)
.secure(!self.show_password),
checkbox("Show password", self.show_password)
.on_toggle(Message::ShowPasswordToggled),
row![
button(text("Register").center().size(18))
.on_press(Message::RegisterPressed)
.style(button::secondary)
.width(Length::FillPortion(3))
.padding(10),
Space::with_width(Length::FillPortion(2)),
button(text("Login").center().size(18))
.on_press(Message::LoginPressed)
.style(button::primary)
.width(Length::FillPortion(3))
.padding(10)
]
.padding(padding::top(15)),
]
.width(Length::Fixed(250.))
.spacing(20),
)
}
pub fn title(&self) -> String {
let errors = [
self.login.error(),
self.password.error(),
self.login.warning(),
self.password.warning(),
];
let error = errors.into_iter().flatten().next();
match &self.state {
State::None => error.map_or_else(|| "Login".into(), Into::into),
State::Requesting => "Requesting...".into(),
State::Error(e) => e.into(),
}
}
}

184
src/src/main.rs Normal file
View File

@ -0,0 +1,184 @@
// mod main_window;
// mod authentication;
mod input;
mod login;
mod register;
mod widget;
use std::sync::Arc;
use crate::login::Login;
use crate::register::Register;
// use crate::authentication::Authentication;
// use crate::main_window::MainWindow;
use data::{MySqlPool, MySqlUserAdapter, SqlxPool};
use iced::{
Element, Subscription, Task, Theme,
futures::lock::Mutex,
widget::{center, row},
window,
};
use service::{AuthenticationAdapter, AuthenticationService};
// #[macro_export]
macro_rules! log {
($($arg:tt)*) => {
#[cfg(debug_assertions)]
println!($($arg)*)
};
}
fn main() -> iced::Result {
iced::daemon(Repository::title, Repository::update, Repository::view)
.subscription(Repository::subscription)
.scale_factor(Repository::scale_factor)
.theme(Repository::theme)
.run_with(Repository::new)
}
struct Repository {
scale_factor: f64,
main_id: window::Id,
login:
Login<AuthenticationService<AuthenticationAdapter<MySqlPool, SqlxPool, MySqlUserAdapter>>>,
register: Register<
AuthenticationService<AuthenticationAdapter<MySqlPool, SqlxPool, MySqlUserAdapter>>,
>,
screen: Screen,
// authentication: Authentication,
}
enum Screen {
Login,
Register,
}
#[derive(Debug)]
enum Message {
ScaleUp,
ScaleDown,
WindowOpened(window::Id),
WindowClosed(window::Id),
Login(login::Message),
Register(register::Message),
// MainWindow(main_window::Message),
}
impl Repository {
fn new() -> (Self, Task<Message>) {
let (main_id, open_task) = window::open(window::Settings::default());
// let (main_window, main_window_task) = MainWindow::new();
let pool = MySqlPool::new(
SqlxPool::connect_lazy(
&std::env::var("DATABASE_URL")
.expect("environment variable `DATABASE_URL` should be set"),
)
.unwrap(),
);
let auth_service = Arc::new(Mutex::new(AuthenticationService::new(
AuthenticationAdapter::new(pool.clone()),
)));
(
Self {
scale_factor: 1.4,
main_id,
login: Login::new(auth_service.clone()),
register: Register::new(auth_service),
screen: Screen::Login,
},
Task::batch([
open_task.map(Message::WindowOpened),
// main_window_task.map(Message::MainWindow),
]),
)
}
fn update(&mut self, message: Message) -> Task<Message> {
match message {
Message::ScaleUp => self.scale_factor = (self.scale_factor + 0.2).min(5.0),
Message::ScaleDown => self.scale_factor = (self.scale_factor - 0.2).max(0.2),
Message::WindowOpened(id) => {
log!("Window opened: {id}");
return iced::widget::focus_next();
}
Message::WindowClosed(id) => {
log!("Window closed: {id}");
if id == self.main_id {
return iced::exit();
}
}
Message::Login(message) => {
if let Some(action) = self.login.update(message) {
match action {
login::Event::SwitchToRegister => self.screen = Screen::Register,
login::Event::Task(task) => return task.map(Message::Login),
login::Event::Authenticated(authenticated) => {
log!("authenticated via login {:#?}", authenticated);
}
}
}
}
Message::Register(message) => {
if let Some(action) = self.register.update(message) {
match action {
register::Event::SwitchToLogin => self.screen = Screen::Login,
register::Event::Task(task) => return task.map(Message::Register),
register::Event::Authenticated(authenticated) => {
log!("authenticated via register: {:#?}", authenticated);
}
}
}
} //
// Message::MainWindow(message) => match self.main_window.update(message) {
// main_window::Action::None => (),
// main_window::Action::Task(task) => return task.map(Message::MainWindow),
// },
}
Task::none()
}
fn view(&self, id: window::Id) -> Element<Message> {
if self.main_id == id {
// self.main_window.view().map(Message::MainWindow)
match self.screen {
Screen::Login => self.login.view().map(Message::Login),
Screen::Register => self.register.view().map(Message::Register),
}
} else {
center(row!["This window is unknown.", "It may be closed."]).into()
}
}
fn title(&self, _: window::Id) -> String {
// "Repository".into()
match self.screen {
Screen::Login => self.login.title(),
Screen::Register => self.register.title(),
}
}
fn subscription(&self) -> Subscription<Message> {
use iced::keyboard::{self, Key, Modifiers};
let hotkeys = keyboard::on_key_press(|key, modifiers| match (modifiers, key) {
(Modifiers::CTRL, Key::Character(c)) if c == "-" => Some(Message::ScaleDown),
(Modifiers::CTRL, Key::Character(c)) if c == "=" || c == "+" => Some(Message::ScaleUp),
_ => None,
});
Subscription::batch([hotkeys, window::close_events().map(Message::WindowClosed)])
}
const fn scale_factor(&self, _: window::Id) -> f64 {
self.scale_factor
}
const fn theme(_: &Self, _: window::Id) -> Theme {
Theme::TokyoNight
}
}

View File

@ -1,16 +1,25 @@
use crate::input::{Input, Validation}; use crate::input::Input;
use crate::widget::centerbox; use crate::widget::centerbox;
use service::authentication::{self, Email, Name, Password, RegisterData};
use service::{
Authenticated, AuthenticationContract,
authentication::{Error, LoginData, Result},
};
use iced::futures::lock::Mutex;
use iced::widget::{Space, button, checkbox, column, container, row, text}; use iced::widget::{Space, button, checkbox, column, container, row, text};
use iced::{Length, Task, padding}; use iced::{Length, Task, padding};
use std::sync::Arc;
pub struct Register { pub struct Register<S> {
state: State, name: Input<Name>,
name: Input, email: Input<Email>,
email: Input, password: Input<Password>,
password: Input, repeat: Input<Password>,
repeat: Input,
show_password: bool, show_password: bool,
state: State,
service: Arc<Mutex<S>>,
} }
enum State { enum State {
None, None,
@ -18,25 +27,15 @@ enum State {
Error(String), Error(String),
} }
pub enum Request { pub enum Event {
SwitchToLogin, SwitchToLogin,
SimpleValidation(Field),
Task(Task<Message>), Task(Task<Message>),
Register { Authenticated(Authenticated),
name: String,
email: String,
password: String,
},
} }
pub enum Field { impl From<Task<Message>> for Event {
Name(String), fn from(value: Task<Message>) -> Self {
Email(String), Self::Task(value)
Password(String), }
}
pub enum RequestResult {
Error(String),
Validation(Field, Validation),
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
@ -54,98 +53,110 @@ pub enum Message {
RegisterPressed, RegisterPressed,
LoginPressed, LoginPressed,
RequestResult(Arc<Result<Authenticated>>),
} }
impl Default for Register { impl<S: AuthenticationContract + 'static> Register<S> {
fn default() -> Self { pub fn new(service: Arc<Mutex<S>>) -> Self {
Self::new()
}
}
impl Register {
pub const fn new() -> Self {
Self { Self {
state: State::None,
name: Input::new("register_name"), name: Input::new("register_name"),
email: Input::new("register_email"), email: Input::new("register_email"),
password: Input::new("register_password"), password: Input::new("register_password"),
repeat: Input::new("register_repeat"), repeat: Input::new("register_repeat"),
show_password: false, show_password: false,
state: State::None,
service,
} }
} }
pub fn handle_result(&mut self, result: RequestResult) {
match result {
RequestResult::Error(e) => self.state = State::Error(e),
RequestResult::Validation(field, validation) => match &field {
Field::Name(s) => self.name.apply_if_eq(s, validation),
Field::Email(s) => self.email.apply_if_eq(s, validation),
Field::Password(s) => self.password.apply_if_eq(s, validation),
},
}
}
#[inline]
fn check_passwords(&mut self) { fn check_passwords(&mut self) {
if self.password.value() != self.repeat.value() { if self.password.as_ref() != self.repeat.as_ref() {
self.repeat.set_error("passwords are different".into()); self.repeat.set_error(&"passwords are different");
} }
} }
pub fn update(&mut self, message: Message) -> Option<Request> {
Some(match message { pub fn update(&mut self, message: Message) -> Option<Event> {
Message::NameChanged(s) => { match message {
self.name.update(s.clone()); Message::NameChanged(s) => self.name.update(s),
Request::SimpleValidation(Field::Name(s)) Message::EmailChanged(s) => self.email.update(s),
}
Message::EmailChanged(s) => {
self.email.update(s.clone());
Request::SimpleValidation(Field::Email(s))
}
Message::PasswordChanged(s) => { Message::PasswordChanged(s) => {
self.password.update(s.clone()); self.password.update(s);
self.check_passwords(); self.check_passwords();
Request::SimpleValidation(Field::Password(s))
} }
Message::RepeatChanged(s) => { Message::RepeatChanged(s) => {
self.repeat.update(s); self.repeat.update(s);
self.check_passwords(); self.check_passwords();
return None;
} }
Message::ShowPasswordToggled(b) => self.show_password = b,
Message::ShowPasswordToggled(b) => { Message::NameSubmitted if self.name.critical() => (),
self.show_password = b; Message::NameSubmitted => return Some(self.email.focus().into()),
return None; Message::EmailSubmitted if self.email.critical() => (),
} Message::EmailSubmitted => return Some(self.password.focus().into()),
Message::PasswordSubmitted if self.password.critical() => (),
Message::NameSubmitted if !self.name.submittable() => return None, Message::PasswordSubmitted => return Some(self.repeat.focus().into()),
Message::NameSubmitted => Request::Task(self.email.focus()), Message::RepeatSubmitted if self.repeat.critical() => (),
Message::EmailSubmitted if !self.email.submittable() => return None,
Message::EmailSubmitted => Request::Task(self.password.focus()),
Message::PasswordSubmitted if !self.password.submittable() => return None,
Message::PasswordSubmitted => Request::Task(self.repeat.focus()),
Message::RegisterPressed | Message::RepeatSubmitted => { Message::RegisterPressed | Message::RepeatSubmitted => {
if !self.name.submittable() { if self.repeat.critical() {
Request::Task(self.name.focus()) return Some(self.repeat.focus().into());
} else if !self.email.submittable() {
Request::Task(self.email.focus())
} else if !self.password.submittable() {
Request::Task(self.password.focus())
} else if !self.repeat.submittable() {
Request::Task(self.repeat.focus())
} else {
self.state = State::Requesting;
Request::Register {
name: self.name.value().into(),
email: self.email.value().into(),
password: self.password.value().into(),
}
} }
let register_data = RegisterData {
name: match self.name.submit() {
Ok(x) => x,
Err(t) => return Some(t.into()),
},
email: match self.email.submit() {
Ok(x) => x,
Err(t) => return Some(t.into()),
},
password: match self.password.submit() {
Ok(x) => x,
Err(t) => return Some(t.into()),
},
};
self.state = State::Requesting;
let arc = self.service.clone();
return Some(
Task::perform(
async move {
let Some(mut service) = arc.try_lock() else {
return Err(Error::Other(
"other authentication request is being performed".into(),
));
};
service.register(register_data).await
},
|r| Message::RequestResult(Arc::new(r)),
)
.into(),
);
} }
Message::LoginPressed => Request::SwitchToLogin, Message::LoginPressed => return Some(Event::SwitchToLogin),
}) Message::RequestResult(r) => match &*r {
Ok(a) => return Some(Event::Authenticated(a.clone())),
Err(e) => {
self.state = State::None;
match e {
Error::NameExists => self.name.set_warning(e),
Error::EmailExists => self.email.set_warning(e),
Error::IncorrectPassword => self.password.set_warning(e),
Error::InvalidPassword(_) => self.password.set_error(e),
_ => self.state = State::Error(e.to_string()),
}
}
},
}
None
} }
pub fn view(&self) -> iced::Element<Message> { pub fn view(&self) -> iced::Element<Message> {
@ -194,7 +205,7 @@ impl Register {
) )
} }
pub fn title(&self) -> std::borrow::Cow<str> { pub fn title(&self) -> String {
let errors = [ let errors = [
self.name.error(), self.name.error(),
self.email.error(), self.email.error(),

View File

@ -14,10 +14,13 @@ pub fn centerbox<'a, Message: 'a>(
/// Scrollable but in both vertical and horizontal directions /// Scrollable but in both vertical and horizontal directions
pub fn scroll<'a, Message: 'a>(content: impl Into<Element<'a, Message>>) -> Element<'a, Message> { pub fn scroll<'a, Message: 'a>(content: impl Into<Element<'a, Message>>) -> Element<'a, Message> {
Scrollable::with_direction(content, scrollable::Direction::Both { Scrollable::with_direction(
vertical: scrollable::Scrollbar::default(), content,
horizontal: scrollable::Scrollbar::default(), scrollable::Direction::Both {
}) vertical: scrollable::Scrollbar::default(),
horizontal: scrollable::Scrollbar::default(),
},
)
.into() .into()
} }

View File

@ -1,7 +0,0 @@
[package]
name = "view"
version = "0.1.0"
edition = "2024"
[dependencies]
iced = { version = "0.13.1", features = ["lazy", "tokio"] }

View File

@ -1,104 +0,0 @@
pub mod login;
pub mod register;
use crate::input::Validation;
use iced::Task;
pub struct Authentication {
screen: Screen,
login: login::Login,
register: register::Register,
}
pub enum Screen {
Login,
Register,
}
#[derive(Debug)]
pub enum Message {
Login(login::Message),
Register(register::Message),
}
pub enum Request {
Task(Task<Message>),
SimpleLoginValidation(login::Field),
SimpleRegisterValidation(register::Field),
Login {
login: String,
password: String,
},
Register {
name: String,
email: String,
password: String,
},
}
pub enum RequestResult {
Error(String),
LoginValidation(login::Field, Validation),
RegisterValidation(register::Field, Validation),
}
impl Default for Authentication {
fn default() -> Self {
Self::new()
}
}
impl Authentication {
pub const fn new() -> Self {
Self {
screen: Screen::Login,
login: Login::new(),
register: Register::new(),
}
}
pub fn update(&mut self, message: Message) -> Option<Request> {
Some(match message {
Message::Login(message) => match self.login.update(message)? {
login::Request::SwitchToRegister => {
self.screen = Screen::Register;
return None;
}
login::Request::SimpleValidation(x) => Request::SimpleLoginValidation(x),
login::Request::Task(task) => Request::Task(task.map(Message::Login)),
login::Request::Login { login, password } => Request::Login { login, password },
},
Message::Register(message) => match self.register.update(message)? {
register::Request::SwitchToLogin => {
self.screen = Screen::Login;
return None;
}
register::Request::SimpleValidation(x) => Request::SimpleRegisterValidation(x),
register::Request::Task(task) => Request::Task(task.map(Message::Register)),
register::Request::Register {
name,
email,
password,
} => Request::Register {
name,
email,
password,
},
},
})
}
pub fn view(&self) -> iced::Element<Message> {
match self.screen {
Screen::Login => self.login.view().map(Message::Login),
Screen::Register => self.register.view().map(Message::Register),
}
}
pub fn title(&self) -> std::borrow::Cow<str> {
match self.screen {
Screen::Login => self.login.title(),
Screen::Register => self.register.title(),
}
}
}

View File

@ -1,164 +0,0 @@
use crate::input::{Input, Validation};
use crate::widget::centerbox;
use iced::widget::{Space, button, checkbox, column, container, row, text};
use iced::{Length, Task, padding};
pub struct Login {
state: State,
login: Input,
password: Input,
show_password: bool,
}
enum State {
None,
Requesting,
Error(String),
}
pub enum Request {
SwitchToRegister,
SimpleValidation(Field),
Task(Task<Message>),
Login { login: String, password: String },
}
pub enum Field {
Login(String),
Password(String),
}
pub enum RequestResult {
Error(String),
Validation(Field, Validation),
}
#[derive(Debug, Clone)]
pub enum Message {
LoginChanged(String),
PasswordChanged(String),
ShowPasswordToggled(bool),
LoginSubmitted,
PasswordSubmitted,
LoginPressed,
RegisterPressed,
}
impl Default for Login {
fn default() -> Self {
Self::new()
}
}
impl Login {
pub const fn new() -> Self {
Self {
state: State::None,
login: Input::new("login_name"),
password: Input::new("login_password"),
show_password: false,
}
}
pub fn handle_result(&mut self, result: RequestResult) {
match result {
RequestResult::Error(e) => self.state = State::Error(e),
RequestResult::Validation(field, validation) => match &field {
Field::Login(s) => self.login.apply_if_eq(s, validation),
Field::Password(s) => self.password.apply_if_eq(s, validation),
},
}
}
pub fn update(&mut self, message: Message) -> Option<Request> {
Some(match message {
Message::LoginChanged(s) => {
self.login.update(s.clone());
Request::SimpleValidation(Field::Login(s))
}
Message::PasswordChanged(s) => {
self.password.update(s.clone());
Request::SimpleValidation(Field::Password(s))
}
Message::ShowPasswordToggled(b) => {
self.show_password = b;
return None;
}
Message::LoginSubmitted if !self.login.submittable() => return None,
Message::LoginSubmitted => Request::Task(self.login.focus()),
Message::LoginPressed | Message::PasswordSubmitted => {
if !self.login.submittable() {
Request::Task(self.login.focus())
} else if !self.password.submittable() {
Request::Task(self.password.focus())
} else {
self.state = State::Requesting;
Request::Login {
login: self.login.value().into(),
password: self.password.value().into(),
}
}
}
Message::RegisterPressed => Request::SwitchToRegister,
})
}
pub fn view(&self) -> iced::Element<Message> {
centerbox(
column![
container(text(self.title()).size(20))
.center_x(Length::Fill)
.padding(padding::bottom(10)),
self.login
.view("Email or Username")
.on_input(Message::LoginChanged)
.on_submit(Message::LoginSubmitted),
self.password
.view("Password")
.on_input(Message::PasswordChanged)
.on_submit(Message::PasswordSubmitted)
.secure(!self.show_password),
checkbox("Show password", self.show_password)
.on_toggle(Message::ShowPasswordToggled),
row![
button(text("Register").center().size(18))
.on_press(Message::RegisterPressed)
.style(button::secondary)
.width(Length::FillPortion(3))
.padding(10),
Space::with_width(Length::FillPortion(2)),
button(text("Login").center().size(18))
.on_press(Message::LoginPressed)
.style(button::primary)
.width(Length::FillPortion(3))
.padding(10)
]
.padding(padding::top(15)),
]
.width(Length::Fixed(250.))
.spacing(20),
)
}
pub fn title(&self) -> std::borrow::Cow<str> {
let errors = [
self.login.error(),
self.password.error(),
self.login.warning(),
self.password.warning(),
];
let error = errors.into_iter().flatten().next();
match &self.state {
State::None => error.map_or_else(|| "Login".into(), Into::into),
State::Requesting => "Requesting...".into(),
State::Error(e) => e.into(),
}
}
}

View File

@ -1,93 +0,0 @@
use crate::widget::text_input::{error, success, warning};
use iced::widget::{TextInput, text_input, text_input::default};
pub struct Input {
id: &'static str,
value: String,
warning: Option<String>,
state: State,
}
enum State {
None,
Valid,
Invalid(String),
}
pub enum Validation {
Valid,
Warning(String),
Invalid(String),
}
impl Input {
pub const fn new(id: &'static str) -> Self {
Self {
id,
value: String::new(),
warning: None,
state: State::None,
}
}
pub fn value(&self) -> &str {
self.value.as_ref()
}
pub fn error(&self) -> Option<&str> {
match &self.state {
State::Invalid(e) => Some(e.as_ref()),
_ => None,
}
}
pub fn warning(&self) -> Option<&str> {
self.warning.as_ref().map(AsRef::as_ref)
}
// pub fn submit(&self) -> Result<String, &str> {
// match &self.state {
// State::Invalid(e) => Err(e.as_ref()),
// State::None | State::Valid => Ok(self.value.clone()),
// }
// }
pub const fn submittable(&self) -> bool {
!matches!(self.state, State::Invalid(_))
}
pub fn update(&mut self, value: String) {
self.value = value;
self.warning = None;
self.state = State::None;
}
pub fn set_error(&mut self, value: String) {
self.state = State::Invalid(value);
}
pub fn set_warning(&mut self, value: String) {
self.warning = Some(value);
}
pub fn apply(&mut self, validation: Validation) {
match validation {
Validation::Valid => self.state = State::Valid,
Validation::Warning(w) => self.warning = Some(w),
Validation::Invalid(e) => self.state = State::Invalid(e),
}
}
pub fn apply_if_eq(&mut self, value: &str, validation: Validation) {
if self.value == value {
self.apply(validation);
}
}
pub fn focus<Message>(&self) -> iced::Task<Message> {
iced::widget::text_input::focus(self.id)
}
pub fn view<Message: Clone>(&self, placeholder: &str) -> TextInput<Message> {
text_input(placeholder, &self.value)
.id(self.id)
.padding(12)
.style(match self.state {
State::None if self.warning.is_none() => default,
State::Valid if self.warning.is_none() => success,
State::Invalid(_) => error,
_ => warning,
})
}
}

View File

@ -1,7 +0,0 @@
mod widget;
mod input;
pub use input::Validation;
pub mod authentication;
pub use authentication::{Authentication, login, register};