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

View File

@ -3,7 +3,8 @@ CREATE DATABASE repository;
USE repository;
-- 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,
email VARCHAR(255) UNIQUE 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
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,
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.
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,
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
CREATE TABLE PackageBaseUserRoles (
base_id INT UNSIGNED,
user_id INT UNSIGNED,
role_id TINYINT UNSIGNED,
base INT UNSIGNED,
user INT UNSIGNED,
role TINYINT UNSIGNED,
comment VARCHAR(255) NULL,
PRIMARY KEY (base_id, user_id, role_id), -- composite key
FOREIGN KEY (base_id) REFERENCES PackageBases(id) ON DELETE CASCADE,
FOREIGN KEY (user_id) REFERENCES Users(id) ON DELETE CASCADE,
FOREIGN KEY (role_id) REFERENCES PackageBaseRoles(id) ON DELETE CASCADE
PRIMARY KEY (base, user, role), -- composite key
FOREIGN KEY (base) REFERENCES PackageBases(id) ON DELETE CASCADE,
FOREIGN KEY (user) REFERENCES Users(id) ON DELETE CASCADE,
FOREIGN KEY (role) REFERENCES PackageBaseRoles(id) ON DELETE CASCADE
);
-- Information about the actual packages
CREATE TABLE Packages ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
package_base INT UNSIGNED NOT NULL,
CREATE TABLE Packages (
id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
base INT UNSIGNED NOT NULL,
name VARCHAR(127) UNIQUE NOT NULL,
version VARCHAR(127) NOT 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,
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.
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
);
INSERT INTO DependencyTypes (id, name) VALUES
@ -68,7 +79,8 @@ INSERT INTO DependencyTypes (id, name) VALUES
(4, 'optdepends');
-- 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,
requirement VARCHAR(255) NULL,
description VARCHAR(127) NULL,
@ -82,7 +94,8 @@ CREATE TABLE PackageDependencies ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
);
-- 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
);
INSERT INTO RelationTypes (id, name) VALUES
@ -91,9 +104,10 @@ INSERT INTO RelationTypes (id, name) VALUES
(3, 'replaces');
-- 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,
requiremen VARCHAR(255) NULL,
requirement VARCHAR(255) NULL,
package INT 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,
"name": "package_base",
"name": "base",
"type_info": {
"type": "Long",
"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,
"name": "package_base",
"name": "base",
"type_info": {
"type": "Long",
"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"
[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 = [
"std",
"now",
] }
sqlx = { version = "0.8.3", default-features = false, features = [
"mysql",
"macros",
"chrono",
"runtime-tokio",
] }
sqlx = { version = "0.8.3", default-features = false, features = ["mysql", "macros", "chrono", "runtime-tokio"] }
# thiserror = "2.0.11"
# garde = { version = "0.22.0", features = ["email", "url", "derive"] }

View File

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

View File

@ -12,7 +12,7 @@ where
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
E: Send,
for<'a> &'a E: Executor<'a, Database = MySql>,

View File

@ -12,7 +12,7 @@ where
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
E: Send,
for<'a> &'a E: Executor<'a, Database = MySql>,
@ -26,7 +26,7 @@ where
let created_at = Utc::now();
let id = sqlx::query!(
"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 (?, ?, ?, ?, ?, ?, ?, ?)",
data.package_base.id,
data.name.as_str(),
@ -43,7 +43,7 @@ where
Ok(Self::Existing {
id,
package_base: data.package_base.id,
base: data.package_base.id,
name: data.name.into(),
version: data.version.into(),
description: data.description.into(),
@ -88,7 +88,7 @@ where
}
Field::PackageBase(package_base) => {
sqlx::query!(
"UPDATE Packages SET package_base = ? WHERE id = ?",
"UPDATE Packages SET base = ? WHERE id = ?",
package_base.id,
existing.id
)
@ -107,7 +107,7 @@ where
existing.id
)
}
Field::URL(url) => {
Field::Url(url) => {
sqlx::query!(
"UPDATE Packages SET url = ? WHERE id = ?",
url.as_ref(),
@ -135,10 +135,10 @@ where
match data {
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::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::CreatedAt(date_time) => existing.created_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>,
{
}
impl<E> crate::port::CRUD<E> for UserAdapter
impl<E> crate::port::Crud<E> for UserAdapter
where
E: Send,
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 atomic::Atomic;
pub use connect::*;
pub use adapter::mysql::base::BaseAdapter as MySqlBaseAdapter;
pub use adapter::mysql::package::PackageAdapter as MySqlPackageAdapter;
pub use adapter::mysql::user::UserAdapter as MySqlUserAdapter;
pub use port::base::{self, Base, BaseRepository};
pub use port::package::{self, Package, PackageRepository};
pub use port::user::{self, User, UserRepository};
pub use atomic::Atomic;
pub use connect::*;
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.
//!
//! No data validation besides very basic one like length violation.
use crate::Result;
//! Very mild argument validation.
use crate::{BoxDynError, Result};
pub mod base;
pub mod package;
pub mod search;
pub mod user;
pub trait CRUD<C> {
pub trait Crud<C> {
type New;
type Unique;
type Update;
@ -16,23 +17,27 @@ pub trait CRUD<C> {
fn create(
connection: &mut C,
data: Self::New,
) -> impl Future<Output = crate::Result<Self::Existing>> + Send;
) -> impl Future<Output = Result<Self::Existing>> + Send;
fn read(
connection: &C,
data: Self::Unique,
) -> impl Future<Output = crate::Result<Option<Self::Existing>>> + Send;
) -> impl Future<Output = Result<Option<Self::Existing>>> + Send;
fn update(
connection: &mut C,
existing: &mut Self::Existing,
data: Self::Update,
) -> impl Future<Output = crate::Result> + Send;
fn delete(connection: &mut C, data: Self::Unique)
-> impl Future<Output = crate::Result> + Send;
) -> impl Future<Output = Result> + Send;
fn delete(connection: &mut C, data: Self::Unique) -> impl Future<Output = Result> + Send;
}
trait CharLength {
pub trait CharLength {
fn length(&self) -> usize;
}
impl CharLength for &str {
fn length(&self) -> usize {
self.chars().count()
}
}
impl CharLength for String {
fn length(&self) -> usize {
self.chars().count()
@ -44,15 +49,43 @@ impl CharLength for Option<String> {
}
}
trait MaxLength {
trait Validatable {
type Inner: CharLength;
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 {
Err("too long")
Err(format!(
"too long (length: {}, max length: {})",
value.length(),
Self::MAX_LENGTH
))
} else {
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 derive_more::{Deref, Into};
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)]
pub struct Name(String);
impl MaxLength for Name {
impl Validatable for Name {
type Inner = String;
const MAX_LENGTH: usize = 127;
}
impl TryFrom<String> for Name {
type Error = &'static str;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::validate(&value)?;
Ok(Self(value))
fn encapsulate(value: Self::Inner) -> Self {
Self(value)
}
}
#[derive(Clone, Deref, Into)]
pub struct Description(Option<String>);
impl MaxLength for Description {
impl Validatable for Description {
type Inner = Option<String>;
const MAX_LENGTH: usize = 510;
}
impl TryFrom<Option<String>> for Description {
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)),
}
fn encapsulate(value: Self::Inner) -> Self {
Self(value)
}
}

View File

@ -1,79 +1,51 @@
use super::MaxLength;
use super::Validatable;
use crate::Base;
use chrono::{DateTime, Utc};
use derive_more::{Deref, Into};
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)]
pub struct Name(String);
impl MaxLength for Name {
impl Validatable for Name {
type Inner = String;
const MAX_LENGTH: usize = 127;
}
impl TryFrom<String> for Name {
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)),
}
fn encapsulate(value: Self::Inner) -> Self {
Self(value)
}
}
#[derive(Clone, Deref, Into)]
pub struct Version(String);
impl MaxLength for Version {
impl Validatable for Version {
type Inner = String;
const MAX_LENGTH: usize = 127;
}
impl TryFrom<String> for Version {
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)),
}
fn encapsulate(value: Self::Inner) -> Self {
Self(value)
}
}
#[derive(Clone, Deref, Into)]
pub struct Description(Option<String>);
impl MaxLength for Description {
impl Validatable for Description {
type Inner = Option<String>;
const MAX_LENGTH: usize = 255;
}
impl TryFrom<Option<String>> for Description {
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)),
}
fn encapsulate(value: Self::Inner) -> Self {
Self(value)
}
}
#[derive(Clone, Deref, Into)]
pub struct URL(Option<String>);
impl MaxLength for URL {
pub struct Url(Option<String>);
impl Validatable for Url {
type Inner = Option<String>;
const MAX_LENGTH: usize = 510;
}
impl TryFrom<Option<String>> for URL {
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)),
}
fn encapsulate(value: Self::Inner) -> Self {
Self(value)
}
}
@ -87,7 +59,7 @@ pub enum Field {
Name(Name),
Version(Version),
Description(Description),
URL(URL),
Url(Url),
FlaggedAt(Option<DateTime<Utc>>),
CreatedAt(DateTime<Utc>),
UpdatedAt(DateTime<Utc>),
@ -98,13 +70,13 @@ pub struct New {
pub name: Name,
pub version: Version,
pub description: Description,
pub url: URL,
pub url: Url,
pub flagged_at: Option<DateTime<Utc>>,
}
pub struct Package {
pub(crate) id: u64,
pub(crate) package_base: u64,
pub(crate) base: u64,
pub(crate) name: String,
pub(crate) version: String,
pub(crate) description: Option<String>,
@ -119,7 +91,7 @@ impl Package {
self.id
}
pub const fn package_base(&self) -> u64 {
self.package_base
self.base
}
pub const fn name(&self) -> &String {
&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 derive_more::{Deref, Into};
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)]
pub struct Name(String);
impl MaxLength for Name {
impl Validatable for Name {
type Inner = String;
const MAX_LENGTH: usize = 31;
}
impl TryFrom<String> for Name {
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)),
}
fn encapsulate(value: Self::Inner) -> Self {
Self(value)
}
}
#[derive(Clone, Deref, Into)]
pub struct Email(String);
impl MaxLength for Email {
impl Validatable for Email {
type Inner = String;
const MAX_LENGTH: usize = 255;
}
impl TryFrom<String> for Email {
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)),
}
fn encapsulate(value: Self::Inner) -> Self {
Self(value)
}
}
#[derive(Clone, Deref, Into)]
pub struct Password(String);
impl MaxLength for Password {
impl Validatable for Password {
type Inner = String;
const MAX_LENGTH: usize = 255;
}
impl TryFrom<String> for Password {
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)),
}
fn encapsulate(value: Self::Inner) -> Self {
Self(value)
}
}
@ -81,7 +60,7 @@ pub struct New {
pub last_used: Option<DateTime<Utc>>,
}
#[derive(Debug)]
#[derive(Debug, Clone)]
pub struct User {
pub(crate) id: u64,
pub(crate) name: String,

View File

@ -7,7 +7,7 @@ edition = "2024"
thiserror = "2.0.11"
argon2 = { version = "0.5.3", features = ["std"] }
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]
path = "../data"

View File

@ -11,6 +11,7 @@ where
UR: UserRepository<C> + Sync,
{
driver: D,
// connection: Option<C>,
_user_repository: PhantomData<UR>,
}
@ -23,14 +24,24 @@ where
pub const fn new(driver: D) -> Self {
Self {
driver,
// connection: None,
_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>
where
C: Send,
C: Send, //+ Sync,
D: Connect<Connection = C> + Sync,
UR: UserRepository<C> + Sync,
{

View File

@ -1,12 +1,13 @@
use super::Authenticated;
use data::user;
pub use data::Validation;
use data::{BoxDynError, user};
use derive_more::{Deref, Into};
use garde::Validate;
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 email_available(&self, email: Email) -> impl Future<Output = Result> + Send;
@ -45,78 +46,110 @@ pub enum Error {
InvalidPassword(data::BoxDynError),
#[error("data source error: {0}")]
Repository(data::BoxDynError),
#[error(transparent)]
Other(data::BoxDynError),
}
pub type ReturnError<T = String> = (T, BoxDynError);
#[derive(Clone)]
pub enum Login {
Name(Name),
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 {
type Error = (String, &'static str);
type Error = ReturnError;
fn try_from(value: String) -> Result<Self, Self::Error> {
let value = match Email::try_from(value) {
Ok(x) => return Ok(Self::Email(x)),
Err((s, _)) => s,
Err((v, _)) => v,
};
match Name::try_from(value) {
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)]
pub struct Name(user::Name);
impl AsRef<str> for Name {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
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> {
#[derive(Validate)]
#[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() {
return Err((value, e.into()));
}
match user::Name::try_from(value) {
Ok(x) => Ok(Self(x)),
Err((s, e)) => Err((s, e.into())),
match Username(value.as_str()).validate() {
Ok(()) => (),
Err(e) => return Err((value, e.into())),
}
Ok(Self(user::Name::new(value)?))
}
}
#[derive(Clone, Deref, Into)]
pub struct Email(user::Email);
impl AsRef<str> for Email {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
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> {
#[derive(Validate)]
#[garde(transparent)]
pub struct Email<'a>(#[garde(email, length(chars, max = 255))] &'a str);
if let Err(e) = Email(&value).validate() {
return Err((value, e.into()));
}
match user::Email::try_from(value) {
Ok(x) => Ok(Self(x)),
Err((s, e)) => Err((s, e.into())),
match Email(value.as_str()).validate() {
Ok(()) => (),
Err(e) => return Err((value, e.into())),
}
Ok(Self(user::Email::new(value)?))
}
}
#[derive(Clone, Deref, Into)]
pub struct Password(String);
impl AsRef<str> for Password {
fn as_ref(&self) -> &str {
self.0.as_ref()
}
}
impl TryFrom<String> for Password {
type Error = (String, &'static str);
type Error = ReturnError;
fn try_from(value: String) -> Result<Self, Self::Error> {
if value.chars().count() >= 8 {
if value.chars().count() > 7 {
Ok(Self(value))
} 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};
#[derive(Deref, DerefMut, Debug)]
#[derive(Debug, Clone, Deref, DerefMut)]
pub struct Authenticated(pub(super) User);
pub trait AuthenticationRepository {

View File

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

View File

@ -4,3 +4,5 @@ pub use authentication::{
Authenticated, AuthenticationAdapter, AuthenticationContract, AuthenticationRepository,
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 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::{Length, Task, padding};
use std::sync::Arc;
pub struct Register {
state: State,
name: Input,
email: Input,
password: Input,
repeat: Input,
pub struct Register<S> {
name: Input<Name>,
email: Input<Email>,
password: Input<Password>,
repeat: Input<Password>,
show_password: bool,
state: State,
service: Arc<Mutex<S>>,
}
enum State {
None,
@ -18,25 +27,15 @@ enum State {
Error(String),
}
pub enum Request {
pub enum Event {
SwitchToLogin,
SimpleValidation(Field),
Task(Task<Message>),
Register {
name: String,
email: String,
password: String,
},
Authenticated(Authenticated),
}
pub enum Field {
Name(String),
Email(String),
Password(String),
}
pub enum RequestResult {
Error(String),
Validation(Field, Validation),
impl From<Task<Message>> for Event {
fn from(value: Task<Message>) -> Self {
Self::Task(value)
}
}
#[derive(Debug, Clone)]
@ -54,98 +53,110 @@ pub enum Message {
RegisterPressed,
LoginPressed,
RequestResult(Arc<Result<Authenticated>>),
}
impl Default for Register {
fn default() -> Self {
Self::new()
}
}
impl Register {
pub const fn new() -> Self {
impl<S: AuthenticationContract + 'static> Register<S> {
pub fn new(service: Arc<Mutex<S>>) -> Self {
Self {
state: State::None,
name: Input::new("register_name"),
email: Input::new("register_email"),
password: Input::new("register_password"),
repeat: Input::new("register_repeat"),
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) {
if self.password.value() != self.repeat.value() {
self.repeat.set_error("passwords are different".into());
if self.password.as_ref() != self.repeat.as_ref() {
self.repeat.set_error(&"passwords are different");
}
}
pub fn update(&mut self, message: Message) -> Option<Request> {
Some(match message {
Message::NameChanged(s) => {
self.name.update(s.clone());
Request::SimpleValidation(Field::Name(s))
}
Message::EmailChanged(s) => {
self.email.update(s.clone());
Request::SimpleValidation(Field::Email(s))
}
pub fn update(&mut self, message: Message) -> Option<Event> {
match message {
Message::NameChanged(s) => self.name.update(s),
Message::EmailChanged(s) => self.email.update(s),
Message::PasswordChanged(s) => {
self.password.update(s.clone());
self.password.update(s);
self.check_passwords();
Request::SimpleValidation(Field::Password(s))
}
Message::RepeatChanged(s) => {
self.repeat.update(s);
self.check_passwords();
return None;
}
Message::ShowPasswordToggled(b) => self.show_password = b,
Message::ShowPasswordToggled(b) => {
self.show_password = b;
return None;
}
Message::NameSubmitted if !self.name.submittable() => return None,
Message::NameSubmitted => Request::Task(self.email.focus()),
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::NameSubmitted if self.name.critical() => (),
Message::NameSubmitted => return Some(self.email.focus().into()),
Message::EmailSubmitted if self.email.critical() => (),
Message::EmailSubmitted => return Some(self.password.focus().into()),
Message::PasswordSubmitted if self.password.critical() => (),
Message::PasswordSubmitted => return Some(self.repeat.focus().into()),
Message::RepeatSubmitted if self.repeat.critical() => (),
Message::RegisterPressed | Message::RepeatSubmitted => {
if !self.name.submittable() {
Request::Task(self.name.focus())
} 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 {
if self.repeat.critical() {
return Some(self.repeat.focus().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();
Request::Register {
name: self.name.value().into(),
email: self.email.value().into(),
password: self.password.value().into(),
}
}
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> {
@ -194,7 +205,7 @@ impl Register {
)
}
pub fn title(&self) -> std::borrow::Cow<str> {
pub fn title(&self) -> String {
let errors = [
self.name.error(),
self.email.error(),

View File

@ -14,10 +14,13 @@ pub fn centerbox<'a, Message: 'a>(
/// Scrollable but in both vertical and horizontal directions
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(
content,
scrollable::Direction::Both {
vertical: scrollable::Scrollbar::default(),
horizontal: scrollable::Scrollbar::default(),
})
},
)
.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};