From a7a474743c0e8338e4c70e239ff571d485e73952 Mon Sep 17 00:00:00 2001 From: Anton Bilous Date: Sat, 8 Feb 2025 16:35:07 +0200 Subject: [PATCH] Working Login & Register functionality --- src/Cargo.lock | 217 ++++++++---------- src/Cargo.toml | 13 +- src/assets/init/init.sql | 50 ++-- ...655c21f03d04973e4291e6d4b6c7847364659.json | 12 + ...4a9cd92aaea3ea58ef8702903983cfc32ab47.json | 2 +- ...ad10878bb9aaa612f62080d40ba24682b0c3b.json | 12 + ...e6efb16919aff7172189c81240cb12462ae58.json | 2 +- ...8f9b092d74c2298ac126f7edbb7d59c536910.json | 12 - ...8bf8a5ce85e7f84fa0dadb53c88cce63e5634.json | 12 - src/data/Cargo.toml | 10 +- src/data/src/adapter/mysql.rs | 1 + src/data/src/adapter/mysql/base.rs | 2 +- src/data/src/adapter/mysql/package.rs | 14 +- src/data/src/adapter/mysql/search.rs | 152 ++++++++++++ src/data/src/adapter/mysql/user.rs | 2 +- src/data/src/lib.rs | 13 +- src/data/src/port.rs | 57 ++++- src/data/src/port/base.rs | 28 +-- src/data/src/port/package.rs | 66 ++---- src/data/src/port/search.rs | 69 ++++++ src/data/src/port/user.rs | 45 +--- src/service/Cargo.toml | 2 +- src/service/src/authentication/adapter.rs | 13 +- src/service/src/authentication/contract.rs | 79 +++++-- src/service/src/authentication/repository.rs | 2 +- src/service/src/authentication/service.rs | 14 +- src/service/src/lib.rs | 2 + src/src/input.rs | 171 ++++++++++++++ src/src/login.rs | 177 ++++++++++++++ src/src/main.rs | 184 +++++++++++++++ .../src/authentication => src}/register.rs | 189 ++++++++------- src/{view => }/src/widget.rs | 11 +- src/view/Cargo.toml | 7 - src/view/src/authentication.rs | 104 --------- src/view/src/authentication/login.rs | 164 ------------- src/view/src/input.rs | 93 -------- src/view/src/lib.rs | 7 - 37 files changed, 1208 insertions(+), 802 deletions(-) create mode 100644 src/data/.sqlx/query-3ffe9168c1eb30bb54c65055314655c21f03d04973e4291e6d4b6c7847364659.json create mode 100644 src/data/.sqlx/query-6b6f842d169e2a9628474edcd6dad10878bb9aaa612f62080d40ba24682b0c3b.json delete mode 100644 src/data/.sqlx/query-c8de918b432ce82bc7bf1d09a378f9b092d74c2298ac126f7edbb7d59c536910.json delete mode 100644 src/data/.sqlx/query-dfc2574c39f3f5a9afc1bdb642d8bf8a5ce85e7f84fa0dadb53c88cce63e5634.json create mode 100644 src/data/src/adapter/mysql/search.rs create mode 100644 src/data/src/port/search.rs create mode 100644 src/src/input.rs create mode 100644 src/src/login.rs create mode 100644 src/src/main.rs rename src/{view/src/authentication => src}/register.rs (50%) rename src/{view => }/src/widget.rs (89%) delete mode 100644 src/view/Cargo.toml delete mode 100644 src/view/src/authentication.rs delete mode 100644 src/view/src/authentication/login.rs delete mode 100644 src/view/src/input.rs delete mode 100644 src/view/src/lib.rs diff --git a/src/Cargo.lock b/src/Cargo.lock index 8431ca4..696bd6a 100644 --- a/src/Cargo.lock +++ b/src/Cargo.lock @@ -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", ] diff --git a/src/Cargo.toml b/src/Cargo.toml index 99ee3bb..3a63616 100644 --- a/src/Cargo.toml +++ b/src/Cargo.toml @@ -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"] diff --git a/src/assets/init/init.sql b/src/assets/init/init.sql index 66dfbb0..6d1981a 100644 --- a/src/assets/init/init.sql +++ b/src/assets/init/init.sql @@ -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, diff --git a/src/data/.sqlx/query-3ffe9168c1eb30bb54c65055314655c21f03d04973e4291e6d4b6c7847364659.json b/src/data/.sqlx/query-3ffe9168c1eb30bb54c65055314655c21f03d04973e4291e6d4b6c7847364659.json new file mode 100644 index 0000000..9d27eba --- /dev/null +++ b/src/data/.sqlx/query-3ffe9168c1eb30bb54c65055314655c21f03d04973e4291e6d4b6c7847364659.json @@ -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" +} diff --git a/src/data/.sqlx/query-695f4b0a4286cf625dc60dc3dfc4a9cd92aaea3ea58ef8702903983cfc32ab47.json b/src/data/.sqlx/query-695f4b0a4286cf625dc60dc3dfc4a9cd92aaea3ea58ef8702903983cfc32ab47.json index 64fcfb8..07c6a04 100644 --- a/src/data/.sqlx/query-695f4b0a4286cf625dc60dc3dfc4a9cd92aaea3ea58ef8702903983cfc32ab47.json +++ b/src/data/.sqlx/query-695f4b0a4286cf625dc60dc3dfc4a9cd92aaea3ea58ef8702903983cfc32ab47.json @@ -14,7 +14,7 @@ }, { "ordinal": 1, - "name": "package_base", + "name": "base", "type_info": { "type": "Long", "flags": "NOT_NULL | MULTIPLE_KEY | UNSIGNED | NO_DEFAULT_VALUE", diff --git a/src/data/.sqlx/query-6b6f842d169e2a9628474edcd6dad10878bb9aaa612f62080d40ba24682b0c3b.json b/src/data/.sqlx/query-6b6f842d169e2a9628474edcd6dad10878bb9aaa612f62080d40ba24682b0c3b.json new file mode 100644 index 0000000..320b56f --- /dev/null +++ b/src/data/.sqlx/query-6b6f842d169e2a9628474edcd6dad10878bb9aaa612f62080d40ba24682b0c3b.json @@ -0,0 +1,12 @@ +{ + "db_name": "MySQL", + "query": "UPDATE Packages SET base = ? WHERE id = ?", + "describe": { + "columns": [], + "parameters": { + "Right": 2 + }, + "nullable": [] + }, + "hash": "6b6f842d169e2a9628474edcd6dad10878bb9aaa612f62080d40ba24682b0c3b" +} diff --git a/src/data/.sqlx/query-944eb40633e943a75244dee639fe6efb16919aff7172189c81240cb12462ae58.json b/src/data/.sqlx/query-944eb40633e943a75244dee639fe6efb16919aff7172189c81240cb12462ae58.json index 023fc05..336a2f1 100644 --- a/src/data/.sqlx/query-944eb40633e943a75244dee639fe6efb16919aff7172189c81240cb12462ae58.json +++ b/src/data/.sqlx/query-944eb40633e943a75244dee639fe6efb16919aff7172189c81240cb12462ae58.json @@ -14,7 +14,7 @@ }, { "ordinal": 1, - "name": "package_base", + "name": "base", "type_info": { "type": "Long", "flags": "NOT_NULL | MULTIPLE_KEY | UNSIGNED | NO_DEFAULT_VALUE", diff --git a/src/data/.sqlx/query-c8de918b432ce82bc7bf1d09a378f9b092d74c2298ac126f7edbb7d59c536910.json b/src/data/.sqlx/query-c8de918b432ce82bc7bf1d09a378f9b092d74c2298ac126f7edbb7d59c536910.json deleted file mode 100644 index a432718..0000000 --- a/src/data/.sqlx/query-c8de918b432ce82bc7bf1d09a378f9b092d74c2298ac126f7edbb7d59c536910.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "db_name": "MySQL", - "query": "UPDATE Packages SET package_base = ? WHERE id = ?", - "describe": { - "columns": [], - "parameters": { - "Right": 2 - }, - "nullable": [] - }, - "hash": "c8de918b432ce82bc7bf1d09a378f9b092d74c2298ac126f7edbb7d59c536910" -} diff --git a/src/data/.sqlx/query-dfc2574c39f3f5a9afc1bdb642d8bf8a5ce85e7f84fa0dadb53c88cce63e5634.json b/src/data/.sqlx/query-dfc2574c39f3f5a9afc1bdb642d8bf8a5ce85e7f84fa0dadb53c88cce63e5634.json deleted file mode 100644 index 5c3f63a..0000000 --- a/src/data/.sqlx/query-dfc2574c39f3f5a9afc1bdb642d8bf8a5ce85e7f84fa0dadb53c88cce63e5634.json +++ /dev/null @@ -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" -} diff --git a/src/data/Cargo.toml b/src/data/Cargo.toml index 432c9e2..83a4d2c 100644 --- a/src/data/Cargo.toml +++ b/src/data/Cargo.toml @@ -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"] } diff --git a/src/data/src/adapter/mysql.rs b/src/data/src/adapter/mysql.rs index 9d6db43..67d439e 100644 --- a/src/data/src/adapter/mysql.rs +++ b/src/data/src/adapter/mysql.rs @@ -1,4 +1,5 @@ //! `MySQL` adapters. pub mod base; pub mod package; +pub mod search; pub mod user; diff --git a/src/data/src/adapter/mysql/base.rs b/src/data/src/adapter/mysql/base.rs index 6d9fa8d..51c0517 100644 --- a/src/data/src/adapter/mysql/base.rs +++ b/src/data/src/adapter/mysql/base.rs @@ -12,7 +12,7 @@ where for<'a> &'a E: Executor<'a, Database = MySql>, { } -impl crate::port::CRUD for BaseAdapter +impl crate::port::Crud for BaseAdapter where E: Send, for<'a> &'a E: Executor<'a, Database = MySql>, diff --git a/src/data/src/adapter/mysql/package.rs b/src/data/src/adapter/mysql/package.rs index b1286d1..96dbf5e 100644 --- a/src/data/src/adapter/mysql/package.rs +++ b/src/data/src/adapter/mysql/package.rs @@ -12,7 +12,7 @@ where for<'a> &'a E: Executor<'a, Database = MySql>, { } -impl crate::port::CRUD for PackageAdapter +impl crate::port::Crud 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, diff --git a/src/data/src/adapter/mysql/search.rs b/src/data/src/adapter/mysql/search.rs new file mode 100644 index 0000000..684cbf0 --- /dev/null +++ b/src/data/src/adapter/mysql/search.rs @@ -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 SearchRepository for UserAdapter +where + E: Send, + for<'a> &'a E: Executor<'a, Database = MySql>, +{ + async fn search(connection: &E, data: Data) -> Result> { + 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(()) + } +} diff --git a/src/data/src/adapter/mysql/user.rs b/src/data/src/adapter/mysql/user.rs index ca66177..c7490ab 100644 --- a/src/data/src/adapter/mysql/user.rs +++ b/src/data/src/adapter/mysql/user.rs @@ -12,7 +12,7 @@ where for<'a> &'a E: Executor<'a, Database = MySql>, { } -impl crate::port::CRUD for UserAdapter +impl crate::port::Crud for UserAdapter where E: Send, for<'a> &'a E: Executor<'a, Database = MySql>, diff --git a/src/data/src/lib.rs b/src/data/src/lib.rs index 8633dbc..ac3eebb 100644 --- a/src/data/src/lib.rs +++ b/src/data/src/lib.rs @@ -10,12 +10,13 @@ pub type Result = std::result::Result; 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::*; + diff --git a/src/data/src/port.rs b/src/data/src/port.rs index a9b70f4..e1d3dcb 100644 --- a/src/data/src/port.rs +++ b/src/data/src/port.rs @@ -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 { +pub trait Crud { type New; type Unique; type Update; @@ -16,23 +17,27 @@ pub trait CRUD { fn create( connection: &mut C, data: Self::New, - ) -> impl Future> + Send; + ) -> impl Future> + Send; fn read( connection: &C, data: Self::Unique, - ) -> impl Future>> + Send; + ) -> impl Future>> + Send; fn update( connection: &mut C, existing: &mut Self::Existing, data: Self::Update, - ) -> impl Future + Send; - fn delete(connection: &mut C, data: Self::Unique) - -> impl Future + Send; + ) -> impl Future + Send; + fn delete(connection: &mut C, data: Self::Unique) -> impl Future + 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 { } } -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: Validatable +where + T: CharLength + Into, +{ + 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 + where + Self: Sized, + { + match Self::valid(&value) { + Ok(()) => Ok(Self::encapsulate(value.into())), + Err(e) => Err((value, e.into())), + } + } +} + +impl Validation for T +where + T: Validatable, + U: CharLength + Into, +{ } diff --git a/src/data/src/port/base.rs b/src/data/src/port/base.rs index d7c72fe..a6e3a96 100644 --- a/src/data/src/port/base.rs +++ b/src/data/src/port/base.rs @@ -1,10 +1,10 @@ -use super::MaxLength; +use super::Validatable; use chrono::{DateTime, Utc}; use derive_more::{Deref, Into}; pub trait BaseRepository: - super::CRUD + super::Crud { } @@ -13,33 +13,21 @@ pub trait BaseRepository: #[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 for Name { - type Error = &'static str; - - fn try_from(value: String) -> Result { - Self::validate(&value)?; - Ok(Self(value)) + fn encapsulate(value: Self::Inner) -> Self { + Self(value) } } #[derive(Clone, Deref, Into)] pub struct Description(Option); -impl MaxLength for Description { +impl Validatable for Description { type Inner = Option; const MAX_LENGTH: usize = 510; -} -impl TryFrom> for Description { - type Error = (Option, &'static str); - - fn try_from(value: Option) -> Result { - match Self::validate(&value) { - Ok(()) => Ok(Self(value)), - Err(e) => Err((value, e)), - } + fn encapsulate(value: Self::Inner) -> Self { + Self(value) } } diff --git a/src/data/src/port/package.rs b/src/data/src/port/package.rs index 6d7862e..2f35392 100644 --- a/src/data/src/port/package.rs +++ b/src/data/src/port/package.rs @@ -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: - super::CRUD + super::Crud { } #[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 for Name { - type Error = (String, &'static str); - - fn try_from(value: String) -> Result { - 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 for Version { - type Error = (String, &'static str); - - fn try_from(value: String) -> Result { - 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); -impl MaxLength for Description { +impl Validatable for Description { type Inner = Option; const MAX_LENGTH: usize = 255; -} -impl TryFrom> for Description { - type Error = (Option, &'static str); - - fn try_from(value: Option) -> Result { - 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); -impl MaxLength for URL { +pub struct Url(Option); +impl Validatable for Url { type Inner = Option; const MAX_LENGTH: usize = 510; -} -impl TryFrom> for URL { - type Error = (Option, &'static str); - - fn try_from(value: Option) -> Result { - 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>), CreatedAt(DateTime), UpdatedAt(DateTime), @@ -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>, } 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, @@ -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 diff --git a/src/data/src/port/search.rs b/src/data/src/port/search.rs new file mode 100644 index 0000000..0475992 --- /dev/null +++ b/src/data/src/port/search.rs @@ -0,0 +1,69 @@ +use super::Validatable; +use crate::Result; + +use chrono::{DateTime, Utc}; +use derive_more::{Deref, Into}; + +pub trait SearchRepository { + fn search(connection: &C, data: Data) -> impl Future>> + 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, + pub version: Box, + pub base_id: u64, + pub base_name: Box, + pub url: Option>, + pub description: Box, + pub submitter_id: u64, + pub submitter_name: Box, + pub updated_at: DateTime, + pub created_at: DateTime, +} + +#[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, +} diff --git a/src/data/src/port/user.rs b/src/data/src/port/user.rs index 359ce02..f1085a0 100644 --- a/src/data/src/port/user.rs +++ b/src/data/src/port/user.rs @@ -1,61 +1,40 @@ -use super::MaxLength; +use super::Validatable; use chrono::{DateTime, Utc}; use derive_more::{Deref, Into}; pub trait UserRepository: - super::CRUD + super::Crud { } #[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 for Name { - type Error = (String, &'static str); - - fn try_from(value: String) -> Result { - 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 for Email { - type Error = (String, &'static str); - - fn try_from(value: String) -> Result { - 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 for Password { - type Error = (String, &'static str); - - fn try_from(value: String) -> Result { - 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>, } -#[derive(Debug)] +#[derive(Debug, Clone)] pub struct User { pub(crate) id: u64, pub(crate) name: String, diff --git a/src/service/Cargo.toml b/src/service/Cargo.toml index 372db4c..c7270b6 100644 --- a/src/service/Cargo.toml +++ b/src/service/Cargo.toml @@ -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" diff --git a/src/service/src/authentication/adapter.rs b/src/service/src/authentication/adapter.rs index 76173de..98ac5d0 100644 --- a/src/service/src/authentication/adapter.rs +++ b/src/service/src/authentication/adapter.rs @@ -11,6 +11,7 @@ where UR: UserRepository + Sync, { driver: D, + // connection: Option, _user_repository: PhantomData, } @@ -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 AuthenticationRepository for AuthenticationAdapter where - C: Send, + C: Send, //+ Sync, D: Connect + Sync, UR: UserRepository + Sync, { diff --git a/src/service/src/authentication/contract.rs b/src/service/src/authentication/contract.rs index b226a68..0d80243 100644 --- a/src/service/src/authentication/contract.rs +++ b/src/service/src/authentication/contract.rs @@ -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 = std::result::Result; -pub trait AuthenticationContract { +pub trait AuthenticationContract: Send { fn name_available(&self, name: Name) -> impl Future + Send; fn email_available(&self, email: Email) -> impl Future + 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, BoxDynError); + #[derive(Clone)] pub enum Login { Name(Name), Email(Email), } +impl AsRef for Login { + fn as_ref(&self) -> &str { + match self { + Self::Name(name) => name.as_ref(), + Self::Email(email) => email.as_ref(), + } + } +} impl TryFrom for Login { - type Error = (String, &'static str); + type Error = ReturnError; fn try_from(value: String) -> Result { 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 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 for Name { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} impl TryFrom for Name { - type Error = (String, Box); + type Error = ReturnError; fn try_from(value: String) -> Result { #[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 for Email { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} impl TryFrom for Email { - type Error = (String, Box); + type Error = ReturnError; fn try_from(value: String) -> Result { #[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 for Password { + fn as_ref(&self) -> &str { + self.0.as_ref() + } +} impl TryFrom for Password { - type Error = (String, &'static str); + type Error = ReturnError; fn try_from(value: String) -> Result { - 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())) } } } diff --git a/src/service/src/authentication/repository.rs b/src/service/src/authentication/repository.rs index aa7bca6..39eaefa 100644 --- a/src/service/src/authentication/repository.rs +++ b/src/service/src/authentication/repository.rs @@ -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 { diff --git a/src/service/src/authentication/service.rs b/src/service/src/authentication/service.rs index 7df788d..e044d3a 100644 --- a/src/service/src/authentication/service.rs +++ b/src/service/src/authentication/service.rs @@ -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 diff --git a/src/service/src/lib.rs b/src/service/src/lib.rs index 997a1fd..9111453 100644 --- a/src/service/src/lib.rs +++ b/src/service/src/lib.rs @@ -4,3 +4,5 @@ pub use authentication::{ Authenticated, AuthenticationAdapter, AuthenticationContract, AuthenticationRepository, AuthenticationService, }; + +// pub diff --git a/src/src/input.rs b/src/src/input.rs new file mode 100644 index 0000000..d50ebfd --- /dev/null +++ b/src/src/input.rs @@ -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 { + id: &'static str, + value: Value, + warning: Option, +} + +// use std::ops::Deref; +// impl Deref for Input { +// type Target = Value; +// fn deref(&self) -> &Self::Target { +// &self.value +// } +// } + +impl> AsRef for Input { + fn as_ref(&self) -> &str { + self.value.as_ref() + } +} + +impl Input { + pub const fn new(id: &'static str) -> Self { + Self { + id, + value: Value::None, + warning: None, + } + } + + pub fn focus(&self) -> iced::Task { + 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) { + self.value = value; + } + pub fn set_warning(&mut self, value: &impl ToString) { + self.warning = Some(value.to_string()); + } +} + +impl> Input { + pub fn set_error(&mut self, value: &impl ToString) { + self.value.set_error(value.to_string()); + } + + pub fn view(&self, placeholder: &str) -> TextInput { + 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 Input +where + E: ToString, + T: TryFrom, +{ + 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 Input +where + E: ToString, + T: TryFrom + Clone, +{ + pub fn submit(&mut self) -> Result> { + match self.value() { + Ok(x) => Ok(x.clone()), + Err(_) => Err(self.focus()), + } + } +} + +#[derive(Default)] +pub enum Value { + #[default] + None, + Valid(T), + Invalid { + value: String, + error: String, + }, +} + +impl> AsRef for Value { + fn as_ref(&self) -> &str { + match self { + Self::None => "", + Self::Valid(x) => x.as_ref(), + Self::Invalid { value, .. } => value.as_ref(), + } + } +} + +impl> Value { + 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 Value +where + E: ToString, + T: TryFrom, +{ + 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(), + }, + }; + } +} diff --git a/src/src/login.rs b/src/src/login.rs new file mode 100644 index 0000000..880db49 --- /dev/null +++ b/src/src/login.rs @@ -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 { + login: Input, + password: Input, + show_password: bool, + + state: State, + service: Arc>, +} +enum State { + None, + Requesting, + Error(String), +} + +pub enum Event { + SwitchToRegister, + Task(Task), + Authenticated(Authenticated), +} +impl From> for Event { + fn from(value: Task) -> Self { + Self::Task(value) + } +} + +#[derive(Debug, Clone)] +pub enum Message { + LoginChanged(String), + PasswordChanged(String), + ShowPasswordToggled(bool), + + LoginSubmitted, + PasswordSubmitted, + + LoginPressed, + RegisterPressed, + + RequestResult(Arc>), +} + +impl Login { + pub fn new(service: Arc>) -> 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 { + 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 { + 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(), + } + } +} diff --git a/src/src/main.rs b/src/src/main.rs new file mode 100644 index 0000000..7355706 --- /dev/null +++ b/src/src/main.rs @@ -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>>, + register: Register< + AuthenticationService>, + >, + 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) { + 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 { + 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 { + 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 { + 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 + } +} diff --git a/src/view/src/authentication/register.rs b/src/src/register.rs similarity index 50% rename from src/view/src/authentication/register.rs rename to src/src/register.rs index a3db851..e62876c 100644 --- a/src/view/src/authentication/register.rs +++ b/src/src/register.rs @@ -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 { + name: Input, + email: Input, + password: Input, + repeat: Input, show_password: bool, + + state: State, + service: Arc>, } enum State { None, @@ -18,25 +27,15 @@ enum State { Error(String), } -pub enum Request { +pub enum Event { SwitchToLogin, - SimpleValidation(Field), Task(Task), - 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> for Event { + fn from(value: Task) -> Self { + Self::Task(value) + } } #[derive(Debug, Clone)] @@ -54,98 +53,110 @@ pub enum Message { RegisterPressed, LoginPressed, + + RequestResult(Arc>), } -impl Default for Register { - fn default() -> Self { - Self::new() - } -} - -impl Register { - pub const fn new() -> Self { +impl Register { + pub fn new(service: Arc>) -> 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 { - 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 { + 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 { - self.state = State::Requesting; - - Request::Register { - name: self.name.value().into(), - email: self.email.value().into(), - password: self.password.value().into(), - } + 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(); + + 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 { @@ -194,7 +205,7 @@ impl Register { ) } - pub fn title(&self) -> std::borrow::Cow { + pub fn title(&self) -> String { let errors = [ self.name.error(), self.email.error(), diff --git a/src/view/src/widget.rs b/src/src/widget.rs similarity index 89% rename from src/view/src/widget.rs rename to src/src/widget.rs index e9e3f7c..d30c20f 100644 --- a/src/view/src/widget.rs +++ b/src/src/widget.rs @@ -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> { - Scrollable::with_direction(content, scrollable::Direction::Both { - vertical: scrollable::Scrollbar::default(), - horizontal: scrollable::Scrollbar::default(), - }) + Scrollable::with_direction( + content, + scrollable::Direction::Both { + vertical: scrollable::Scrollbar::default(), + horizontal: scrollable::Scrollbar::default(), + }, + ) .into() } diff --git a/src/view/Cargo.toml b/src/view/Cargo.toml deleted file mode 100644 index b558b15..0000000 --- a/src/view/Cargo.toml +++ /dev/null @@ -1,7 +0,0 @@ -[package] -name = "view" -version = "0.1.0" -edition = "2024" - -[dependencies] -iced = { version = "0.13.1", features = ["lazy", "tokio"] } diff --git a/src/view/src/authentication.rs b/src/view/src/authentication.rs deleted file mode 100644 index f5685a3..0000000 --- a/src/view/src/authentication.rs +++ /dev/null @@ -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), - 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 { - 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 { - 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 { - match self.screen { - Screen::Login => self.login.title(), - Screen::Register => self.register.title(), - } - } -} diff --git a/src/view/src/authentication/login.rs b/src/view/src/authentication/login.rs deleted file mode 100644 index d0f2e3b..0000000 --- a/src/view/src/authentication/login.rs +++ /dev/null @@ -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), - 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 { - 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 { - 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 { - 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(), - } - } -} diff --git a/src/view/src/input.rs b/src/view/src/input.rs deleted file mode 100644 index 5e8a70a..0000000 --- a/src/view/src/input.rs +++ /dev/null @@ -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, - 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 { - // 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(&self) -> iced::Task { - iced::widget::text_input::focus(self.id) - } - pub fn view(&self, placeholder: &str) -> TextInput { - 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, - }) - } -} diff --git a/src/view/src/lib.rs b/src/view/src/lib.rs deleted file mode 100644 index df79b91..0000000 --- a/src/view/src/lib.rs +++ /dev/null @@ -1,7 +0,0 @@ -mod widget; - -mod input; -pub use input::Validation; - -pub mod authentication; -pub use authentication::{Authentication, login, register};