From 0d2e406b73d515cb3f56c69a57f4b258992398b8 Mon Sep 17 00:00:00 2001 From: turtton Date: Fri, 11 Oct 2024 00:14:52 +0900 Subject: [PATCH 01/59] :hammer: Publish database module --- driver/src/lib.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/driver/src/lib.rs b/driver/src/lib.rs index 4008cf3..da08c67 100644 --- a/driver/src/lib.rs +++ b/driver/src/lib.rs @@ -1,4 +1,4 @@ -mod database; +pub mod database; mod error; pub use self::error::*; From faa5a58f6c0c29bd20171e70a61c0085f215ac8b Mon Sep 17 00:00:00 2001 From: turtton Date: Fri, 11 Oct 2024 00:15:22 +0900 Subject: [PATCH 02/59] :hammer: Prepare axum server --- Cargo.lock | 577 +++++++++++++++++++++++++++++++++++++++++- Cargo.toml | 4 + kernel/Cargo.toml | 2 +- server/Cargo.toml | 16 ++ server/src/error.rs | 40 +++ server/src/handler.rs | 45 ++++ server/src/main.rs | 57 ++++- 7 files changed, 735 insertions(+), 6 deletions(-) create mode 100644 server/src/error.rs create mode 100644 server/src/handler.rs diff --git a/Cargo.lock b/Cargo.lock index 0c17b00..109ada6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -30,6 +30,15 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + [[package]] name = "allocator-api2" version = "0.2.18" @@ -49,8 +58,16 @@ dependencies = [ "async-trait", "error-stack", "kernel", + "time", + "uuid", ] +[[package]] +name = "arc-swap" +version = "1.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" + [[package]] name = "async-trait" version = "0.1.81" @@ -77,6 +94,85 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0c4b4d0bd25bd0b74681c0ad21497610ce1b7c91b1022cd21c80c6fbdd9476b0" +[[package]] +name = "axum" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "504e3947307ac8326a5437504c517c4b56716c9d98fac0028c2acc7ca47d70ae" +dependencies = [ + "async-trait", + "axum-core", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "itoa", + "matchit", + "memchr", + "mime", + "percent-encoding", + "pin-project-lite", + "rustversion", + "serde", + "serde_json", + "serde_path_to_error", + "serde_urlencoded", + "sync_wrapper 1.0.1", + "tokio", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-core" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09f2bd6146b97ae3359fa0cc6d6b376d9539582c7b4220f041a33ec24c226199" +dependencies = [ + "async-trait", + "bytes", + "futures-util", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "rustversion", + "sync_wrapper 1.0.1", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "axum-extra" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73c3220b188aea709cf1b6c5f9b01c3bd936bb08bd2b5184a12b35ac8131b1f9" +dependencies = [ + "axum", + "axum-core", + "bytes", + "futures-util", + "headers", + "http", + "http-body", + "http-body-util", + "mime", + "pin-project-lite", + "serde", + "serde_html_form", + "tower", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "backtrace" version = "0.3.73" @@ -212,6 +308,15 @@ version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +[[package]] +name = "crossbeam-channel" +version = "0.5.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33480d6946193aa8033910124896ca395333cae7e2d1113d1fef6c3272217df2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-queue" version = "0.3.11" @@ -250,14 +355,35 @@ dependencies = [ "tokio", ] +[[package]] +name = "deadpool" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6541a3916932fe57768d4be0b1ffb5ec7cbf74ca8c903fdfd5c0fe8aa958f0ed" +dependencies = [ + "deadpool-runtime", + "num_cpus", + "tokio", +] + [[package]] name = "deadpool-redis" version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f1760f60ffc6653b4afd924c5792098d8c00d9a3deb6b3d989eac17949dc422" dependencies = [ - "deadpool", - "redis", + "deadpool 0.9.5", + "redis 0.23.3", +] + +[[package]] +name = "deadpool-redis" +version = "0.18.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfae6799b68a735270e4344ee3e834365f707c72da09c9a8bb89b45cc3351395" +dependencies = [ + "deadpool 0.12.1", + "redis 0.27.4", ] [[package]] @@ -323,7 +449,7 @@ name = "driver" version = "0.1.0" dependencies = [ "async-trait", - "deadpool-redis", + "deadpool-redis 0.12.0", "dotenvy", "error-stack", "kernel", @@ -404,6 +530,12 @@ dependencies = [ "spin", ] +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + [[package]] name = "foreign-types" version = "0.3.2" @@ -546,6 +678,30 @@ dependencies = [ "hashbrown", ] +[[package]] +name = "headers" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" +dependencies = [ + "base64", + "bytes", + "headers-core", + "http", + "httpdate", + "mime", + "sha1", +] + +[[package]] +name = "headers-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" +dependencies = [ + "http", +] + [[package]] name = "heck" version = "0.4.1" @@ -594,6 +750,87 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "http" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +dependencies = [ + "bytes", + "fnv", + "itoa", +] + +[[package]] +name = "http-body" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" +dependencies = [ + "bytes", + "http", +] + +[[package]] +name = "http-body-util" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "793429d76616a256bcb62c2a2ec2bed781c8307e797e2598c50010f2bee2544f" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "pin-project-lite", +] + +[[package]] +name = "httparse" +version = "1.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d71d3574edd2771538b901e6549113b4006ece66150fb69c0fb6d9a2adae946" + +[[package]] +name = "httpdate" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" + +[[package]] +name = "hyper" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50dfd22e0e76d0f662d429a5f80fcaf3855009297eab6a0a9f8543834744ba05" +dependencies = [ + "bytes", + "futures-channel", + "futures-util", + "http", + "http-body", + "httparse", + "httpdate", + "itoa", + "pin-project-lite", + "smallvec", + "tokio", +] + +[[package]] +name = "hyper-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" +dependencies = [ + "bytes", + "futures-util", + "http", + "http-body", + "hyper", + "pin-project-lite", + "tokio", + "tower-service", +] + [[package]] name = "idna" version = "0.5.0" @@ -689,6 +926,21 @@ version = "0.4.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24" +[[package]] +name = "matchers" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8263075bb86c5a1b1427b5ae862e8889656f126e9f77c484496e8b47cf5c5558" +dependencies = [ + "regex-automata 0.1.10", +] + +[[package]] +name = "matchit" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e7465ac9959cc2b1404e8e2367b43684a6d13790fe23056cc8c6c5a6b7bcb94" + [[package]] name = "md-5" version = "0.10.6" @@ -705,6 +957,12 @@ version = "2.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" +[[package]] +name = "mime" +version = "0.3.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -768,6 +1026,26 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nu-ansi-term" +version = "0.46.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" +dependencies = [ + "overload", + "winapi", +] + +[[package]] +name = "num-bigint" +version = "0.4.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" +dependencies = [ + "num-integer", + "num-traits", +] + [[package]] name = "num-bigint-dig" version = "0.8.4" @@ -890,6 +1168,12 @@ dependencies = [ "vcpkg", ] +[[package]] +name = "overload" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" + [[package]] name = "parking_lot" version = "0.12.3" @@ -1055,6 +1339,29 @@ dependencies = [ "url", ] +[[package]] +name = "redis" +version = "0.27.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc6baebe319ef5e4b470f248335620098d1c2e9261e995be05f56f719ca4bdb2" +dependencies = [ + "arc-swap", + "async-trait", + "bytes", + "combine", + "futures-util", + "itoa", + "num-bigint", + "percent-encoding", + "pin-project-lite", + "ryu", + "sha1_smol", + "socket2", + "tokio", + "tokio-util", + "url", +] + [[package]] name = "redox_syscall" version = "0.4.1" @@ -1073,12 +1380,72 @@ dependencies = [ "bitflags 2.6.0", ] +[[package]] +name = "regex" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38200e5ee88914975b69f657f0801b6f6dccafd44fd9326302a4aaeecfacb1d8" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata 0.4.8", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-automata" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132" +dependencies = [ + "regex-syntax 0.6.29", +] + +[[package]] +name = "regex-automata" +version = "0.4.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "368758f23274712b504848e9d5a6f010445cc8b87a7cdb4d7cbee666c1288da3" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.5", +] + +[[package]] +name = "regex-syntax" +version = "0.6.29" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" + +[[package]] +name = "regex-syntax" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" + [[package]] name = "retain_mut" version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4389f1d5789befaf6029ebd9f7dac4af7f7e3d61b69d4f30e2ac02b57e7712b0" +[[package]] +name = "rikka-mq" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7839049a440c6c96eb8acfffcc65c580276d6ea6f8c029af36f29bd57b8401f9" +dependencies = [ + "deadpool-redis 0.18.0", + "destructure", + "redis 0.27.4", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", +] + [[package]] name = "rsa" version = "0.9.6" @@ -1127,6 +1494,12 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustversion" +version = "1.0.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "955d28af4278de8121b7ebeb796b6a45735dc01436d898801014aced2773a3d6" + [[package]] name = "ryu" version = "1.0.18" @@ -1197,6 +1570,19 @@ dependencies = [ "syn 2.0.72", ] +[[package]] +name = "serde_html_form" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de514ef58196f1fc96dcaef80fe6170a1ce6215df9687a93fe8300e773fefc5" +dependencies = [ + "form_urlencoded", + "indexmap", + "itoa", + "ryu", + "serde", +] + [[package]] name = "serde_json" version = "1.0.122" @@ -1209,13 +1595,46 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_path_to_error" +version = "0.1.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6" +dependencies = [ + "itoa", + "serde", +] + +[[package]] +name = "serde_urlencoded" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3491c14715ca2294c4d6a88f15e84739788c1d030eed8c110436aafdaa2f3fd" +dependencies = [ + "form_urlencoded", + "itoa", + "ryu", + "serde", +] + [[package]] name = "server" version = "0.1.0" dependencies = [ "application", + "axum", + "axum-extra", "driver", + "error-stack", "kernel", + "rikka-mq", + "tokio", + "tower-http", + "tracing", + "tracing-appender", + "tracing-subscriber", + "uuid", + "vodca", ] [[package]] @@ -1229,6 +1648,12 @@ dependencies = [ "digest", ] +[[package]] +name = "sha1_smol" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" + [[package]] name = "sha2" version = "0.10.8" @@ -1240,6 +1665,15 @@ dependencies = [ "digest", ] +[[package]] +name = "sharded-slab" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" +dependencies = [ + "lazy_static", +] + [[package]] name = "signature" version = "2.2.0" @@ -1546,6 +1980,18 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "sync_wrapper" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" + +[[package]] +name = "sync_wrapper" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" + [[package]] name = "tempfile" version = "3.12.0" @@ -1579,6 +2025,16 @@ dependencies = [ "syn 2.0.72", ] +[[package]] +name = "thread_local" +version = "1.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" +dependencies = [ + "cfg-if", + "once_cell", +] + [[package]] name = "time" version = "0.3.36" @@ -1676,6 +2132,51 @@ dependencies = [ "tokio", ] +[[package]] +name = "tower" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +dependencies = [ + "futures-core", + "futures-util", + "pin-project-lite", + "sync_wrapper 0.1.2", + "tokio", + "tower-layer", + "tower-service", + "tracing", +] + +[[package]] +name = "tower-http" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" +dependencies = [ + "bitflags 2.6.0", + "bytes", + "http", + "http-body", + "http-body-util", + "pin-project-lite", + "tokio", + "tower-layer", + "tower-service", +] + +[[package]] +name = "tower-layer" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" + +[[package]] +name = "tower-service" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" + [[package]] name = "tracing" version = "0.1.40" @@ -1688,6 +2189,18 @@ dependencies = [ "tracing-core", ] +[[package]] +name = "tracing-appender" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" +dependencies = [ + "crossbeam-channel", + "thiserror", + "time", + "tracing-subscriber", +] + [[package]] name = "tracing-attributes" version = "0.1.27" @@ -1706,6 +2219,36 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" dependencies = [ "once_cell", + "valuable", +] + +[[package]] +name = "tracing-log" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" +dependencies = [ + "log", + "once_cell", + "tracing-core", +] + +[[package]] +name = "tracing-subscriber" +version = "0.3.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad0f048c97dbd9faa9b7df56362b8ebcaa52adb06b498c050d2f4e32f90a7a8b" +dependencies = [ + "matchers", + "nu-ansi-term", + "once_cell", + "regex", + "sharded-slab", + "smallvec", + "thread_local", + "tracing", + "tracing-core", + "tracing-log", ] [[package]] @@ -1780,6 +2323,12 @@ dependencies = [ "serde", ] +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + [[package]] name = "vcpkg" version = "0.2.15" @@ -1825,6 +2374,28 @@ dependencies = [ "wasite", ] +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + [[package]] name = "windows-sys" version = "0.48.0" diff --git a/Cargo.toml b/Cargo.toml index 0818774..3b9d273 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -13,7 +13,11 @@ uuid = { version = "1.4", features = ["serde", "v7"] } time = { version = "0.3", features = ["serde"] } nanoid = "0.4.0" +vodca = "0.1.8" + error-stack = "0.4.1" +tracing = "0.1.40" +tracing-subscriber = { version = "0.3.18", features = ["registry", "env-filter", "fmt"] } [workspace.package] version = "0.1.0" diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml index c3b4a58..c250409 100644 --- a/kernel/Cargo.toml +++ b/kernel/Cargo.toml @@ -9,7 +9,7 @@ authors.workspace = true [dependencies] rand = "0.8" destructure = "0.5.6" -vodca = "0.1.8" +vodca = { workspace = true } serde = { workspace = true } uuid = { workspace = true } time = { workspace = true } diff --git a/server/Cargo.toml b/server/Cargo.toml index a35a846..580ceea 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -7,6 +7,22 @@ authors.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +uuid = { workspace = true } + +tracing = { workspace = true } +tracing-appender = "0.2.3" +tracing-subscriber = { workspace = true } + +axum = { version = "0.7.4", features = ["json", "tracing"] } +axum-extra = { version = "0.9.2", features = ["typed-header", "query"] } +tower-http = { version = "0.5.1", features = ["tokio", "cors"] } +tokio = { workspace = true, features = ["rt", "rt-multi-thread", "macros"] } + +rikka-mq = { version = "0.1.1", features = ["redis", "tracing"] } +error-stack = { workspace = true } + +vodca = { workspace = true } + application = { path = "../application" } driver = { path = "../driver" } kernel = { path = "../kernel" } diff --git a/server/src/error.rs b/server/src/error.rs new file mode 100644 index 0000000..1a48472 --- /dev/null +++ b/server/src/error.rs @@ -0,0 +1,40 @@ +use axum::http::StatusCode; +use axum::response::IntoResponse; +use error_stack::Report; +use kernel::KernelError; +use std::process::{ExitCode, Termination}; + +#[derive(Debug)] +pub struct StackTrace(Report); + +impl From> for StackTrace { + fn from(e: Report) -> Self { + StackTrace(e) + } +} + +impl Termination for StackTrace { + fn report(self) -> ExitCode { + self.0.report() + } +} + +#[derive(Debug)] +pub struct ErrorStatus(Report); + +impl From> for ErrorStatus { + fn from(e: Report) -> Self { + ErrorStatus(e) + } +} + +impl IntoResponse for ErrorStatus { + fn into_response(self) -> axum::response::Response { + match self.0.current_context() { + KernelError::Concurrency => StatusCode::CONFLICT, + KernelError::Timeout => StatusCode::REQUEST_TIMEOUT, + KernelError::Internal => StatusCode::INTERNAL_SERVER_ERROR, + } + .into_response() + } +} diff --git a/server/src/handler.rs b/server/src/handler.rs new file mode 100644 index 0000000..4a4ed6e --- /dev/null +++ b/server/src/handler.rs @@ -0,0 +1,45 @@ +use driver::database::PostgresDatabase; +use kernel::KernelError; +use rikka_mq::define::redis::mq::RedisMessageQueue; +use std::sync::Arc; +use vodca::References; + +#[derive(Clone, References)] +pub struct AppModule { + handler: Arc, + worker: Arc, +} + +impl AppModule { + pub async fn new() -> error_stack::Result { + let handler = Arc::new(Handler::init().await?); + let worker = Arc::new(Worker::new(&handler)); + Ok(Self { handler, worker }) + } +} + +#[derive(References)] +pub struct Handler { + pgpool: PostgresDatabase, +} + +impl Handler { + pub async fn init() -> error_stack::Result { + let pgpool = PostgresDatabase::new().await?; + + Ok(Self { pgpool }) + } +} + +#[derive(References)] +pub struct Worker { + // command: RedisMessageQueue, CommandOperation>, +} + +impl Worker { + pub fn new(handler: &Arc) -> Self { + // let command = init_command_worker(handler); + // Self { command } + Self + } +} diff --git a/server/src/main.rs b/server/src/main.rs index e7a11a9..4f3647e 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,3 +1,56 @@ -fn main() { - println!("Hello, world!"); +mod error; +mod handler; + +use crate::error::StackTrace; +use crate::handler::AppModule; +use axum::ServiceExt; +use error_stack::ResultExt; +use kernel::KernelError; +use std::net::SocketAddr; +use tokio::net::TcpListener; +use tower_http::cors::CorsLayer; +use tracing_subscriber::layer::SubscriberExt; +use tracing_subscriber::util::SubscriberInitExt; +use tracing_subscriber::Layer; + +#[tokio::main] +async fn main() -> Result<(), StackTrace> { + let appender = tracing_appender::rolling::daily(std::path::Path::new("./logs/"), "debug.log"); + let (non_blocking_appender, _guard) = tracing_appender::non_blocking(appender); + tracing_subscriber::registry() + .with( + tracing_subscriber::fmt::layer() + .with_filter(tracing_subscriber::EnvFilter::new( + std::env::var("RUST_LOG").unwrap_or_else(|_| { + "driver=debug,server=debug,tower_http=debug,hyper=debug,sqlx=debug".into() + }), + )) + .with_filter(tracing_subscriber::filter::LevelFilter::DEBUG), + ) + .with( + tracing_subscriber::fmt::Layer::default() + .with_writer(non_blocking_appender) + .with_ansi(false) + .with_filter(tracing_subscriber::filter::LevelFilter::DEBUG), + ) + .init(); + + let app = AppModule::new().await?; + + let router = axum::Router::new() + // TODO + .layer(CorsLayer::new()) + .with_state(app); + + let bind = SocketAddr::from(([0, 0, 0, 0], 8080)); + let tcp = TcpListener::bind(bind) + .await + .change_context_lazy(|| KernelError::Internal) + .attach_printable_lazy(|| "Failed to bind to port 8080")?; + + axum::serve(tcp, router.into_make_service()) + .await + .change_context_lazy(|| KernelError::Internal)?; + + Ok(()) } From d42fe2386f11399843b2a04746bd5314ae178fd5 Mon Sep 17 00:00:00 2001 From: turtton Date: Fri, 8 Nov 2024 00:18:38 +0900 Subject: [PATCH 03/59] :construction: Add get accounts api --- Cargo.lock | 4 +- application/Cargo.toml | 4 ++ application/src/lib.rs | 2 + application/src/main.rs | 3 -- application/src/service.rs | 28 ++++++++++ application/src/service/account.rs | 21 ++++++++ application/src/transfer.rs | 1 + application/src/transfer/account.rs | 9 ++++ server/Cargo.toml | 3 +- server/src/error.rs | 34 +++++++++--- server/src/handler.rs | 3 +- server/src/main.rs | 4 +- server/src/route.rs | 21 ++++++++ server/src/route/account.rs | 82 +++++++++++++++++++++++++++++ 14 files changed, 204 insertions(+), 15 deletions(-) create mode 100644 application/src/lib.rs delete mode 100644 application/src/main.rs create mode 100644 application/src/service.rs create mode 100644 application/src/service/account.rs create mode 100644 application/src/transfer.rs create mode 100644 application/src/transfer/account.rs create mode 100644 server/src/route.rs create mode 100644 server/src/route/account.rs diff --git a/Cargo.lock b/Cargo.lock index 109ada6..1b6ae9c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -60,6 +60,7 @@ dependencies = [ "kernel", "time", "uuid", + "vodca", ] [[package]] @@ -1628,12 +1629,13 @@ dependencies = [ "error-stack", "kernel", "rikka-mq", + "serde", + "time", "tokio", "tower-http", "tracing", "tracing-appender", "tracing-subscriber", - "uuid", "vodca", ] diff --git a/application/Cargo.toml b/application/Cargo.toml index 5f48561..a00e2b7 100644 --- a/application/Cargo.toml +++ b/application/Cargo.toml @@ -7,6 +7,10 @@ authors.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +uuid = { workspace = true } +time = { workspace = true } +vodca = { workspace = true } + async-trait = "0.1" error-stack = { workspace = true } diff --git a/application/src/lib.rs b/application/src/lib.rs new file mode 100644 index 0000000..bef0d9e --- /dev/null +++ b/application/src/lib.rs @@ -0,0 +1,2 @@ +pub mod service; +pub mod transfer; diff --git a/application/src/main.rs b/application/src/main.rs deleted file mode 100644 index e7a11a9..0000000 --- a/application/src/main.rs +++ /dev/null @@ -1,3 +0,0 @@ -fn main() { - println!("Hello, world!"); -} diff --git a/application/src/service.rs b/application/src/service.rs new file mode 100644 index 0000000..2f6d6ce --- /dev/null +++ b/application/src/service.rs @@ -0,0 +1,28 @@ +use vodca::Nameln; + +pub mod account; + +#[derive(Debug, Nameln)] +#[vodca(snake_case)] +pub enum Direction { + NEXT, + PREV, +} + +impl Default for Direction { + fn default() -> Self { + Self::NEXT + } +} + +impl TryFrom for Direction { + type Error = String; + + fn try_from(value: String) -> Result { + match value.as_str() { + "next" => Ok(Self::NEXT), + "prev" => Ok(Self::PREV), + other => Err(format!("Invalid direction: {}", other)), + } + } +} diff --git a/application/src/service/account.rs b/application/src/service/account.rs new file mode 100644 index 0000000..13368d9 --- /dev/null +++ b/application/src/service/account.rs @@ -0,0 +1,21 @@ +use crate::service::Direction; +use crate::transfer::account::AccountDto; +use kernel::interfaces::query::DependOnAccountQuery; +use kernel::KernelError; +use std::future::Future; + +pub trait GetAccountService: 'static + Sync + Send + DependOnAccountQuery { + fn get_all_accounts( + &self, + limit: Option, + cursor: Option, + direction: Option, //TODO error_stackやめてResponseに載せれる情報を返す + ) -> impl Future, KernelError>> { + async { + todo!("get stellar id") + // self.account_query().find_by_stellar_id(stellar_id).await + } + } +} + +impl GetAccountService for T where T: DependOnAccountQuery + 'static {} diff --git a/application/src/transfer.rs b/application/src/transfer.rs new file mode 100644 index 0000000..b0edc6c --- /dev/null +++ b/application/src/transfer.rs @@ -0,0 +1 @@ +pub mod account; diff --git a/application/src/transfer/account.rs b/application/src/transfer/account.rs new file mode 100644 index 0000000..4856260 --- /dev/null +++ b/application/src/transfer/account.rs @@ -0,0 +1,9 @@ +use time::OffsetDateTime; + +pub struct AccountDto { + pub nanoid: String, + pub name: String, + pub public_key: String, + pub is_bot: bool, + pub created_at: OffsetDateTime, +} diff --git a/server/Cargo.toml b/server/Cargo.toml index 580ceea..67f7bf4 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -7,7 +7,7 @@ authors.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -uuid = { workspace = true } +time = { workspace = true } tracing = { workspace = true } tracing-appender = "0.2.3" @@ -22,6 +22,7 @@ rikka-mq = { version = "0.1.1", features = ["redis", "tracing"] } error-stack = { workspace = true } vodca = { workspace = true } +serde = { workspace = true } application = { path = "../application" } driver = { path = "../driver" } diff --git a/server/src/error.rs b/server/src/error.rs index 1a48472..ad13373 100644 --- a/server/src/error.rs +++ b/server/src/error.rs @@ -20,21 +20,41 @@ impl Termination for StackTrace { } #[derive(Debug)] -pub struct ErrorStatus(Report); +pub enum ErrorStatus { + Report(Report), + StatusCode(StatusCode), + StatusCodeWithMessage(StatusCode, String), +} impl From> for ErrorStatus { fn from(e: Report) -> Self { - ErrorStatus(e) + ErrorStatus::Report(e) + } +} + +impl From for ErrorStatus { + fn from(code: StatusCode) -> Self { + ErrorStatus::StatusCode(code) + } +} + +impl From<(StatusCode, String)> for ErrorStatus { + fn from((code, message): (StatusCode, String)) -> Self { + ErrorStatus::StatusCodeWithMessage(code, message) } } impl IntoResponse for ErrorStatus { fn into_response(self) -> axum::response::Response { - match self.0.current_context() { - KernelError::Concurrency => StatusCode::CONFLICT, - KernelError::Timeout => StatusCode::REQUEST_TIMEOUT, - KernelError::Internal => StatusCode::INTERNAL_SERVER_ERROR, + match self { + ErrorStatus::Report(e) => match e.current_context() { + KernelError::Concurrency => StatusCode::CONFLICT, + KernelError::Timeout => StatusCode::REQUEST_TIMEOUT, + KernelError::Internal => StatusCode::INTERNAL_SERVER_ERROR, + } + .into_response(), + ErrorStatus::StatusCode(code) => code.into_response(), + ErrorStatus::StatusCodeWithMessage(code, message) => (code, message).into_response(), } - .into_response() } } diff --git a/server/src/handler.rs b/server/src/handler.rs index 4a4ed6e..8e1fe3e 100644 --- a/server/src/handler.rs +++ b/server/src/handler.rs @@ -1,6 +1,5 @@ use driver::database::PostgresDatabase; use kernel::KernelError; -use rikka_mq::define::redis::mq::RedisMessageQueue; use std::sync::Arc; use vodca::References; @@ -40,6 +39,6 @@ impl Worker { pub fn new(handler: &Arc) -> Self { // let command = init_command_worker(handler); // Self { command } - Self + Self {} } } diff --git a/server/src/main.rs b/server/src/main.rs index 4f3647e..072f6d6 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,8 +1,10 @@ mod error; mod handler; +mod route; use crate::error::StackTrace; use crate::handler::AppModule; +use crate::route::account::AccountRouter; use axum::ServiceExt; use error_stack::ResultExt; use kernel::KernelError; @@ -38,7 +40,7 @@ async fn main() -> Result<(), StackTrace> { let app = AppModule::new().await?; let router = axum::Router::new() - // TODO + .route_account() .layer(CorsLayer::new()) .with_state(app); diff --git a/server/src/route.rs b/server/src/route.rs new file mode 100644 index 0000000..b875493 --- /dev/null +++ b/server/src/route.rs @@ -0,0 +1,21 @@ +use crate::error::ErrorStatus; +use application::service::Direction; +use axum::http::StatusCode; + +pub mod account; + +trait DirectionConverter { + fn convert_to_direction(self) -> Result, ErrorStatus>; +} + +impl DirectionConverter for Option { + fn convert_to_direction(self) -> Result, ErrorStatus> { + match self { + Some(d) => match Direction::try_from(d) { + Ok(d) => Ok(Some(d)), + Err(message) => Err((StatusCode::BAD_REQUEST, message).into()), + }, + None => Ok(None), + } + } +} diff --git a/server/src/route/account.rs b/server/src/route/account.rs new file mode 100644 index 0000000..08ae4cc --- /dev/null +++ b/server/src/route/account.rs @@ -0,0 +1,82 @@ +use crate::error::ErrorStatus; +use crate::handler::AppModule; +use crate::route::DirectionConverter; +use application::service::account::GetAccountService; +use axum::extract::{Query, State}; +use axum::http::StatusCode; +use axum::routing::get; +use axum::{Json, Router}; +use serde::{Deserialize, Serialize}; +use time::OffsetDateTime; + +#[derive(Debug, Deserialize)] +struct GetAllAccountQuery { + limit: Option, + cursor: Option, + direction: Option, +} + +#[derive(Debug, Serialize)] +struct AccountResponse { + id: String, + name: String, + public_key: String, + is_bot: bool, + created_at: OffsetDateTime, +} + +#[derive(Debug, Serialize)] +struct AccountsResponse { + first: String, + last: String, + items: Vec, +} + +pub trait AccountRouter { + fn route_account(self) -> Self; +} + +impl AccountRouter for Router { + fn route_account(self) -> Self { + self.route( + "/accounts", + get( + |State(module): State, + Query(GetAllAccountQuery { + direction, + limit, + cursor, + }): Query| async move { + let direction = direction.convert_to_direction()?; + let result = module + .handler() + .pgpool() + .get_all_accounts(limit, cursor, direction) + .await + .map_err(ErrorStatus::from)?; + if result.is_empty() { + return Err(ErrorStatus::from(StatusCode::NOT_FOUND)); + } + let response = AccountsResponse { + first: result + .first() + .map(|account| account.nanoid.clone()) + .unwrap(), + last: result.last().map(|account| account.nanoid.clone()).unwrap(), + items: result + .into_iter() + .map(|account| AccountResponse { + id: account.nanoid, + name: account.name, + public_key: account.public_key, + is_bot: account.is_bot, + created_at: account.created_at, + }) + .collect(), + }; + Ok(Json(response)) + }, + ), + ) + } +} From 6aa47ab8337d14e2d188736a9ff1644ffb349319 Mon Sep 17 00:00:00 2001 From: turtton Date: Fri, 29 Nov 2024 01:15:50 +0900 Subject: [PATCH 04/59] :memo: Add keycloak start command --- README.md | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/README.md b/README.md index 829a4e3..965df11 100644 --- a/README.md +++ b/README.md @@ -3,6 +3,13 @@ +# Keycloak + +```shell +podman run --rm -p 18080:8080 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin --name emumet-keycloak docker.io/keycloak/keycloak start-dev +``` +> Url: http://localhost:18080 + # DB Podman(docker)にて環境構築が可能です From bbf7d7e7bd4b430f0dce35bf2b996f38f0307055 Mon Sep 17 00:00:00 2001 From: turtton Date: Thu, 5 Dec 2024 23:03:00 +0900 Subject: [PATCH 05/59] :construction: Add basic permission system --- server/src/main.rs | 1 + server/src/permission.rs | 114 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 115 insertions(+) create mode 100644 server/src/permission.rs diff --git a/server/src/main.rs b/server/src/main.rs index 072f6d6..a95bad2 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,6 +1,7 @@ mod error; mod handler; mod route; +mod permission; use crate::error::StackTrace; use crate::handler::AppModule; diff --git a/server/src/permission.rs b/server/src/permission.rs new file mode 100644 index 0000000..4671f63 --- /dev/null +++ b/server/src/permission.rs @@ -0,0 +1,114 @@ +use std::collections::HashSet; +use std::hash::Hash; +use std::ops::{Add, BitAnd}; + +struct Request(HashSet); + +impl Request { + pub fn new(objects: HashSet) -> Self { + Request(objects) + } +} + +pub trait ToRequest { + fn to_request(&self, target: &T) -> Request; +} + +#[derive(Debug, Clone)] +pub struct Permission(Vec>); + +impl Permission { + pub fn new(perm: HashSet) -> Self { + Permission(vec![perm]) + } + + pub fn allows(&self, req: &Request) -> bool { + let request = &req.0; + self.0.iter().all(|p| !(p & request).is_empty()) + } +} + +impl Add for Permission { + type Output = Self; + + fn add(self, rhs: Self) -> Self::Output { + let mut perm = self.0; + perm.extend(rhs.0); + Permission(perm) + } +} + +pub trait ToPermission { + fn to_permission(&self, target: T) -> Permission; +} + +#[cfg(test)] +mod test { + use super::*; + use std::collections::HashMap; + + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + enum Role { + Admin, + User, + Guest, + } + + #[derive(Debug, Clone, PartialEq, Eq, Hash)] + struct User { + id: u64, + } + + #[derive(Debug, Clone)] + struct RoleManager(HashMap); + + impl ToRequest for RoleManager { + fn to_request(&self, target: &User) -> Request { + Request( + self.0.get(&target.id).map_or_else( + || HashSet::new(), + |role| vec![role.clone()].into_iter().collect::>(), + ), + ) + } + } + + #[derive(Debug, Clone, Hash, PartialEq, Eq)] + enum Action { + Read, + Write, + Delete, + } + + #[derive(Debug, Clone)] + struct ActionRoles(HashMap>); + + impl ToPermission for ActionRoles { + fn to_permission(&self, target: Action) -> Permission { + self.0.get(&target).map_or_else( + || Permission::new(HashSet::new()), + |roles| Permission::new(roles.clone()), + ) + } + } + + #[test] + fn test() { + let mut role_manager = RoleManager(HashMap::new()); + let mut action_roles = ActionRoles(HashMap::new()); + action_roles.0.insert(Action::Read, vec![Role::Admin, Role::User, Role::Guest].into_iter().collect::>()); + action_roles.0.insert(Action::Write, vec![Role::Admin, Role::User].into_iter().collect::>()); + action_roles.0.insert(Action::Delete, vec![Role::Admin].into_iter().collect::>()); + + let user = User { id: 1 }; + role_manager.0.insert(user.id, Role::User); + + let request = role_manager.to_request(&user); + let read = action_roles.to_permission(Action::Read); + let write = action_roles.to_permission(Action::Delete); + + let new_perm = read + write; + + assert!(new_perm.allows(&request)); + } +} From e8afaffecca4923e642e8fc5afd431dd464f5e69 Mon Sep 17 00:00:00 2001 From: turtton Date: Sat, 25 Jan 2025 11:52:55 +0900 Subject: [PATCH 06/59] :pushpin: Share destructure/dotenvy --- Cargo.lock | 284 ++++++++++++++++++++++++++++++++++++++++++++-- Cargo.toml | 2 + driver/Cargo.toml | 2 +- kernel/Cargo.toml | 2 +- server/Cargo.toml | 6 + 5 files changed, 287 insertions(+), 9 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 1b6ae9c..d9bf44a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -439,6 +439,17 @@ dependencies = [ "subtle", ] +[[package]] +name = "displaydoc" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + [[package]] name = "dotenvy" version = "0.15.7" @@ -832,14 +843,143 @@ dependencies = [ "tower-service", ] +[[package]] +name = "icu_collections" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" +dependencies = [ + "displaydoc", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_locid" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" +dependencies = [ + "displaydoc", + "litemap", + "tinystr", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_locid_transform" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_locid_transform_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_locid_transform_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fdc8ff3388f852bede6b579ad4e978ab004f139284d7b28715f773507b946f6e" + +[[package]] +name = "icu_normalizer" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_normalizer_data", + "icu_properties", + "icu_provider", + "smallvec", + "utf16_iter", + "utf8_iter", + "write16", + "zerovec", +] + +[[package]] +name = "icu_normalizer_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cafbf7aa791e9b22bec55a167906f9e1215fd475cd22adfcf660e03e989516" + +[[package]] +name = "icu_properties" +version = "1.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" +dependencies = [ + "displaydoc", + "icu_collections", + "icu_locid_transform", + "icu_properties_data", + "icu_provider", + "tinystr", + "zerovec", +] + +[[package]] +name = "icu_properties_data" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67a8effbc3dd3e4ba1afa8ad918d5684b8868b3b26500753effea8d2eed19569" + +[[package]] +name = "icu_provider" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" +dependencies = [ + "displaydoc", + "icu_locid", + "icu_provider_macros", + "stable_deref_trait", + "tinystr", + "writeable", + "yoke", + "zerofrom", + "zerovec", +] + +[[package]] +name = "icu_provider_macros" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + [[package]] name = "idna" -version = "0.5.0" +version = "1.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" dependencies = [ - "unicode-bidi", - "unicode-normalization", + "idna_adapter", + "smallvec", + "utf8_iter", +] + +[[package]] +name = "idna_adapter" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" +dependencies = [ + "icu_normalizer", + "icu_properties", ] [[package]] @@ -911,6 +1051,12 @@ version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" +[[package]] +name = "litemap" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ee93343901ab17bd981295f2cf0026d4ad018c7c31ba84549a4ddbb47a45104" + [[package]] name = "lock_api" version = "0.4.12" @@ -1625,17 +1771,23 @@ dependencies = [ "application", "axum", "axum-extra", + "destructure", + "dotenvy", "driver", "error-stack", "kernel", + "rand", + "rand_chacha", "rikka-mq", "serde", + "sha2", "time", "tokio", "tower-http", "tracing", "tracing-appender", "tracing-subscriber", + "url", "vodca", ] @@ -1943,6 +2095,12 @@ dependencies = [ "uuid", ] +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "stringprep" version = "0.1.5" @@ -1994,6 +2152,17 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +[[package]] +name = "synstructure" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] + [[package]] name = "tempfile" version = "3.12.0" @@ -2068,6 +2237,16 @@ dependencies = [ "time-core", ] +[[package]] +name = "tinystr" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" +dependencies = [ + "displaydoc", + "zerovec", +] + [[package]] name = "tinyvec" version = "1.8.0" @@ -2300,9 +2479,9 @@ checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" [[package]] name = "url" -version = "2.5.2" +version = "2.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c" +checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" dependencies = [ "form_urlencoded", "idna", @@ -2315,6 +2494,18 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + +[[package]] +name = "utf8_iter" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" + [[package]] name = "uuid" version = "1.10.0" @@ -2546,6 +2737,42 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + +[[package]] +name = "writeable" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" + +[[package]] +name = "yoke" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" +dependencies = [ + "serde", + "stable_deref_trait", + "yoke-derive", + "zerofrom", +] + +[[package]] +name = "yoke-derive" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", + "synstructure", +] + [[package]] name = "zerocopy" version = "0.7.35" @@ -2567,8 +2794,51 @@ dependencies = [ "syn 2.0.72", ] +[[package]] +name = "zerofrom" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cff3ee08c995dee1859d998dea82f7374f2826091dd9cd47def953cae446cd2e" +dependencies = [ + "zerofrom-derive", +] + +[[package]] +name = "zerofrom-derive" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", + "synstructure", +] + [[package]] name = "zeroize" version = "1.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" + +[[package]] +name = "zerovec" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" +dependencies = [ + "yoke", + "zerofrom", + "zerovec-derive", +] + +[[package]] +name = "zerovec-derive" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.72", +] diff --git a/Cargo.toml b/Cargo.toml index 3b9d273..4a9cd3f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,8 +12,10 @@ serde = { version = "1", features = ["derive"] } uuid = { version = "1.4", features = ["serde", "v7"] } time = { version = "0.3", features = ["serde"] } nanoid = "0.4.0" +dotenvy = "0.15.7" vodca = "0.1.8" +destructure = "0.5.6" error-stack = "0.4.1" tracing = "0.1.40" diff --git a/driver/Cargo.toml b/driver/Cargo.toml index e2c90a4..79c90e1 100644 --- a/driver/Cargo.toml +++ b/driver/Cargo.toml @@ -7,7 +7,7 @@ authors.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -dotenvy = "0.15.7" +dotenvy = { workspace = true } deadpool-redis = "0.12.0" sqlx = { version = "0.7", features = ["uuid", "time", "postgres", "runtime-tokio-native-tls", "json"] } serde_json = "1" diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml index c250409..f39275b 100644 --- a/kernel/Cargo.toml +++ b/kernel/Cargo.toml @@ -8,7 +8,7 @@ authors.workspace = true [dependencies] rand = "0.8" -destructure = "0.5.6" +destructure = { workspace = true } vodca = { workspace = true } serde = { workspace = true } uuid = { workspace = true } diff --git a/server/Cargo.toml b/server/Cargo.toml index 67f7bf4..1b5b770 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -8,6 +8,11 @@ authors.workspace = true [dependencies] time = { workspace = true } +rand = "0.8" +rand_chacha = "0.3" +sha2 = "0.10" +url = { version = "2.5.4", features = [] } +dotenvy = { workspace = true} tracing = { workspace = true } tracing-appender = "0.2.3" @@ -22,6 +27,7 @@ rikka-mq = { version = "0.1.1", features = ["redis", "tracing"] } error-stack = { workspace = true } vodca = { workspace = true } +destructure = { workspace = true } serde = { workspace = true } application = { path = "../application" } From f83646e39a43c055da40aaa4e69dad17a4dcf345 Mon Sep 17 00:00:00 2001 From: turtton Date: Sat, 25 Jan 2025 11:53:19 +0900 Subject: [PATCH 07/59] :memo: Update keycloak script --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 965df11..b300923 100644 --- a/README.md +++ b/README.md @@ -6,7 +6,7 @@ # Keycloak ```shell -podman run --rm -p 18080:8080 -e KEYCLOAK_USER=admin -e KEYCLOAK_PASSWORD=admin --name emumet-keycloak docker.io/keycloak/keycloak start-dev +podman run --rm -p 18080:8080 -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin --name emumet-keycloak docker.io/keycloak/keycloak:26.0.7 start-dev ``` > Url: http://localhost:18080 From c7a2c4596eff9b97a4d5f496e736c9427a952c64 Mon Sep 17 00:00:00 2001 From: turtton Date: Sat, 25 Jan 2025 13:14:57 +0900 Subject: [PATCH 08/59] :recycle: Change stellar related structure as auth --- application/src/service/account.rs | 4 +- driver/src/database/postgres.rs | 4 +- driver/src/database/postgres/account.rs | 20 +- driver/src/database/postgres/auth_account.rs | 338 ++++++++++++++++ driver/src/database/postgres/auth_host.rs | 250 ++++++++++++ .../src/database/postgres/stellar_account.rs | 363 ------------------ driver/src/database/postgres/stellar_host.rs | 250 ------------ kernel/src/entity.rs | 8 +- kernel/src/entity/auth_account.rs | 144 +++++++ .../client_id.rs | 2 +- kernel/src/entity/auth_account/id.rs | 13 + .../entity/{stellar_host.rs => auth_host.rs} | 6 +- .../entity/{stellar_host => auth_host}/id.rs | 2 +- .../entity/{stellar_host => auth_host}/url.rs | 2 +- kernel/src/entity/stellar_account.rs | 250 ------------ .../entity/stellar_account/access_token.rs | 5 - kernel/src/entity/stellar_account/id.rs | 13 - .../entity/stellar_account/refresh_token.rs | 5 - kernel/src/modify.rs | 8 +- .../{stellar_account.rs => auth_account.rs} | 16 +- .../modify/{stellar_host.rs => auth_host.rs} | 14 +- kernel/src/query.rs | 8 +- kernel/src/query/account.rs | 6 +- kernel/src/query/auth_account.rs | 22 ++ kernel/src/query/auth_host.rs | 28 ++ kernel/src/query/stellar_account.rs | 22 -- kernel/src/query/stellar_host.rs | 28 -- migrations/20230707210300_init.dbml | 36 +- migrations/20230707210300_init.sql | 20 +- migrations/gensql.sh | 2 +- 30 files changed, 872 insertions(+), 1017 deletions(-) create mode 100644 driver/src/database/postgres/auth_account.rs create mode 100644 driver/src/database/postgres/auth_host.rs delete mode 100644 driver/src/database/postgres/stellar_account.rs delete mode 100644 driver/src/database/postgres/stellar_host.rs create mode 100644 kernel/src/entity/auth_account.rs rename kernel/src/entity/{stellar_account => auth_account}/client_id.rs (79%) create mode 100644 kernel/src/entity/auth_account/id.rs rename kernel/src/entity/{stellar_host.rs => auth_host.rs} (74%) rename kernel/src/entity/{stellar_host => auth_host}/id.rs (85%) rename kernel/src/entity/{stellar_host => auth_host}/url.rs (82%) delete mode 100644 kernel/src/entity/stellar_account.rs delete mode 100644 kernel/src/entity/stellar_account/access_token.rs delete mode 100644 kernel/src/entity/stellar_account/id.rs delete mode 100644 kernel/src/entity/stellar_account/refresh_token.rs rename kernel/src/modify/{stellar_account.rs => auth_account.rs} (61%) rename kernel/src/modify/{stellar_host.rs => auth_host.rs} (61%) create mode 100644 kernel/src/query/auth_account.rs create mode 100644 kernel/src/query/auth_host.rs delete mode 100644 kernel/src/query/stellar_account.rs delete mode 100644 kernel/src/query/stellar_host.rs diff --git a/application/src/service/account.rs b/application/src/service/account.rs index 13368d9..b5f00a4 100644 --- a/application/src/service/account.rs +++ b/application/src/service/account.rs @@ -12,8 +12,8 @@ pub trait GetAccountService: 'static + Sync + Send + DependOnAccountQuery { direction: Option, //TODO error_stackやめてResponseに載せれる情報を返す ) -> impl Future, KernelError>> { async { - todo!("get stellar id") - // self.account_query().find_by_stellar_id(stellar_id).await + todo!("get auth id") + // self.account_query().find_by_auth_id(auth_id).await } } } diff --git a/driver/src/database/postgres.rs b/driver/src/database/postgres.rs index bef3deb..5c37728 100644 --- a/driver/src/database/postgres.rs +++ b/driver/src/database/postgres.rs @@ -1,12 +1,12 @@ mod account; +mod auth_account; +mod auth_host; mod event; mod follow; mod image; mod metadata; mod profile; mod remote_account; -mod stellar_account; -mod stellar_host; use crate::database::env; use crate::ConvertError; diff --git a/driver/src/database/postgres/account.rs b/driver/src/database/postgres/account.rs index d65ee66..5ce5744 100644 --- a/driver/src/database/postgres/account.rs +++ b/driver/src/database/postgres/account.rs @@ -3,8 +3,8 @@ use crate::ConvertError; use kernel::interfaces::modify::{AccountModifier, DependOnAccountModifier}; use kernel::interfaces::query::{AccountQuery, DependOnAccountQuery}; use kernel::prelude::entity::{ - Account, AccountId, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, DeletedAt, - EventVersion, Nanoid, StellarAccountId, + Account, AccountId, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, + AuthAccountId, DeletedAt, EventVersion, Nanoid, }; use kernel::KernelError; use sqlx::types::time::OffsetDateTime; @@ -64,10 +64,10 @@ impl AccountQuery for PostgresAccountRepository { .map(|option| option.map(Account::from)) } - async fn find_by_stellar_id( + async fn find_by_auth_id( &self, transaction: &mut Self::Transaction, - stellar_id: &StellarAccountId, + auth_id: &AuthAccountId, ) -> error_stack::Result, KernelError> { let con: &mut PgConnection = transaction; sqlx::query_as::<_, AccountRow>( @@ -75,11 +75,11 @@ impl AccountQuery for PostgresAccountRepository { r#" SELECT id, name, private_key, public_key, is_bot, deleted_at, version, nanoid FROM accounts - INNER JOIN stellar_emumet_accounts ON stellar_emumet_accounts.emumet_id = accounts.id - WHERE stellar_emumet_accounts.stellar_id = $1 AND deleted_at IS NULL + INNER JOIN auth_emumet_accounts ON auth_emumet_accounts.emumet_id = accounts.id + WHERE auth_emumet_accounts.auth_id = $1 AND deleted_at IS NULL "#, ) - .bind(stellar_id.as_ref()) + .bind(auth_id.as_ref()) .fetch_all(con) .await .convert_error() @@ -210,7 +210,7 @@ mod test { use kernel::interfaces::query::{AccountQuery, DependOnAccountQuery}; use kernel::prelude::entity::{ Account, AccountId, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, - EventVersion, Nanoid, StellarAccountId, + AuthAccountId, EventVersion, Nanoid, }; use sqlx::types::Uuid; @@ -244,13 +244,13 @@ mod test { } #[tokio::test] - async fn find_by_stellar_id() { + async fn find_by_auth_id() { let database = PostgresDatabase::new().await.unwrap(); let mut transaction = database.begin_transaction().await.unwrap(); let accounts = database .account_query() - .find_by_stellar_id(&mut transaction, &StellarAccountId::new(Uuid::now_v7())) + .find_by_auth_id(&mut transaction, &AuthAccountId::new(Uuid::now_v7())) .await .unwrap(); assert!(accounts.is_empty()); diff --git a/driver/src/database/postgres/auth_account.rs b/driver/src/database/postgres/auth_account.rs new file mode 100644 index 0000000..7e692ae --- /dev/null +++ b/driver/src/database/postgres/auth_account.rs @@ -0,0 +1,338 @@ +use crate::database::{PostgresConnection, PostgresDatabase}; +use crate::ConvertError; +use kernel::interfaces::modify::{AuthAccountModifier, DependOnAuthAccountModifier}; +use kernel::interfaces::query::{AuthAccountQuery, DependOnAuthAccountQuery}; +use kernel::prelude::entity::{ + AuthAccount, AuthAccountClientId, AuthAccountId, AuthHostId, EventVersion, +}; +use kernel::KernelError; +use sqlx::PgConnection; +use uuid::Uuid; + +#[derive(sqlx::FromRow)] +struct AuthAccountRow { + id: Uuid, + host_id: Uuid, + client_id: String, + version: Uuid, +} + +impl From for AuthAccount { + fn from(value: AuthAccountRow) -> Self { + AuthAccount::new( + AuthAccountId::new(value.id), + AuthHostId::new(value.host_id), + AuthAccountClientId::new(value.client_id), + EventVersion::new(value.version), + ) + } +} + +pub struct PostgresAuthAccountRepository; + +impl AuthAccountQuery for PostgresAuthAccountRepository { + type Transaction = PostgresConnection; + + async fn find_by_id( + &self, + transaction: &mut Self::Transaction, + account_id: &AuthAccountId, + ) -> error_stack::Result, KernelError> { + let con: &mut PgConnection = transaction; + sqlx::query_as::<_, AuthAccountRow>( + //language=postgresql + r#" + SELECT id, host_id, client_id, version + FROM auth_accounts + WHERE id = $1 + "#, + ) + .bind(account_id.as_ref()) + .fetch_optional(con) + .await + .convert_error() + .map(|option| option.map(|row| row.into())) + } +} + +impl DependOnAuthAccountQuery for PostgresDatabase { + type AuthAccountQuery = PostgresAuthAccountRepository; + + fn auth_account_query(&self) -> &Self::AuthAccountQuery { + &PostgresAuthAccountRepository + } +} + +impl AuthAccountModifier for PostgresAuthAccountRepository { + type Transaction = PostgresConnection; + + async fn create( + &self, + transaction: &mut Self::Transaction, + auth_account: &AuthAccount, + ) -> error_stack::Result<(), KernelError> { + let con: &mut PgConnection = transaction; + sqlx::query( + //language=postgresql + r#" + INSERT INTO auth_accounts (id, host_id, client_id, version) VALUES ($1, $2, $3, $4) + "#, + ) + .bind(auth_account.id().as_ref()) + .bind(auth_account.host().as_ref()) + .bind(auth_account.client_id().as_ref()) + .bind(auth_account.version().as_ref()) + .execute(con) + .await + .convert_error()?; + Ok(()) + } + + async fn update( + &self, + transaction: &mut Self::Transaction, + auth_account: &AuthAccount, + ) -> error_stack::Result<(), KernelError> { + let con: &mut PgConnection = transaction; + sqlx::query( + //language=postgresql + r#" + UPDATE auth_accounts SET host_id = $2, client_id = $3, version = $6 + WHERE id = $1 + "#, + ) + .bind(auth_account.id().as_ref()) + .bind(auth_account.host().as_ref()) + .bind(auth_account.client_id().as_ref()) + .bind(auth_account.version().as_ref()) + .execute(con) + .await + .convert_error()?; + Ok(()) + } + + async fn delete( + &self, + transaction: &mut Self::Transaction, + account_id: &AuthAccountId, + ) -> error_stack::Result<(), KernelError> { + let con: &mut PgConnection = transaction; + sqlx::query( + //language=postgresql + r#" + DELETE FROM auth_accounts WHERE id = $1 + "#, + ) + .bind(account_id.as_ref()) + .execute(con) + .await + .convert_error()?; + Ok(()) + } +} + +impl DependOnAuthAccountModifier for PostgresDatabase { + type AuthAccountModifier = PostgresAuthAccountRepository; + + fn auth_account_modifier(&self) -> &Self::AuthAccountModifier { + &PostgresAuthAccountRepository + } +} + +#[cfg(test)] +mod test { + mod query { + use crate::database::PostgresDatabase; + use kernel::interfaces::database::DatabaseConnection; + use kernel::interfaces::modify::{ + AuthAccountModifier, AuthHostModifier, DependOnAuthAccountModifier, + DependOnAuthHostModifier, + }; + use kernel::interfaces::query::{AuthAccountQuery, DependOnAuthAccountQuery}; + use kernel::prelude::entity::{ + AuthAccount, AuthAccountClientId, AuthAccountId, AuthHost, AuthHostId, AuthHostUrl, + EventVersion, + }; + use uuid::Uuid; + + #[tokio::test] + async fn find_by_id() { + let database = PostgresDatabase::new().await.unwrap(); + let mut transaction = database.begin_transaction().await.unwrap(); + + let auth_host_id = AuthHostId::new(Uuid::now_v7()); + let auth_host = AuthHost::new(auth_host_id.clone(), AuthHostUrl::new(Uuid::now_v7())); + database + .auth_host_modifier() + .create(&mut transaction, &auth_host) + .await + .unwrap(); + let account_id = AuthAccountId::new(Uuid::now_v7()); + let auth_account = AuthAccount::new( + account_id.clone(), + auth_host_id, + AuthAccountClientId::new("client_id".to_string()), + EventVersion::new(Uuid::now_v7()), + ); + + database + .auth_account_modifier() + .create(&mut transaction, &auth_account) + .await + .unwrap(); + let result = database + .auth_account_query() + .find_by_id(&mut transaction, &account_id) + .await + .unwrap(); + assert_eq!(result, Some(auth_account.clone())); + database + .auth_account_modifier() + .delete(&mut transaction, auth_account.id()) + .await + .unwrap(); + } + } + + mod modify { + use crate::database::PostgresDatabase; + use kernel::interfaces::database::DatabaseConnection; + use kernel::interfaces::modify::{ + AuthAccountModifier, AuthHostModifier, DependOnAuthAccountModifier, + DependOnAuthHostModifier, + }; + use kernel::interfaces::query::{AuthAccountQuery, DependOnAuthAccountQuery}; + use kernel::prelude::entity::{ + AuthAccount, AuthAccountClientId, AuthAccountId, AuthHost, AuthHostId, AuthHostUrl, + EventVersion, + }; + use uuid::Uuid; + + #[tokio::test] + async fn create() { + let database = PostgresDatabase::new().await.unwrap(); + let mut transaction = database.begin_transaction().await.unwrap(); + + let host_id = AuthHostId::new(Uuid::now_v7()); + let account_id = AuthAccountId::new(Uuid::now_v7()); + let auth_host = AuthHost::new(host_id.clone(), AuthHostUrl::new(Uuid::now_v7())); + database + .auth_host_modifier() + .create(&mut transaction, &auth_host) + .await + .unwrap(); + let auth_account = AuthAccount::new( + account_id.clone(), + host_id, + AuthAccountClientId::new("client_id".to_string()), + EventVersion::new(Uuid::now_v7()), + ); + database + .auth_account_modifier() + .create(&mut transaction, &auth_account) + .await + .unwrap(); + let result = database + .auth_account_query() + .find_by_id(&mut transaction, &account_id) + .await + .unwrap(); + assert_eq!(result, Some(auth_account.clone())); + database + .auth_account_modifier() + .delete(&mut transaction, auth_account.id()) + .await + .unwrap(); + } + + #[tokio::test] + async fn update() { + let database = PostgresDatabase::new().await.unwrap(); + let mut transaction = database.begin_transaction().await.unwrap(); + + let host_id = AuthHostId::new(Uuid::now_v7()); + let account_id = AuthAccountId::new(Uuid::now_v7()); + let auth_host = AuthHost::new(host_id.clone(), AuthHostUrl::new(Uuid::now_v7())); + database + .auth_host_modifier() + .create(&mut transaction, &auth_host) + .await + .unwrap(); + let auth_account = AuthAccount::new( + account_id.clone(), + host_id.clone(), + AuthAccountClientId::new("client_id".to_string()), + EventVersion::new(Uuid::now_v7()), + ); + database + .auth_account_modifier() + .create(&mut transaction, &auth_account) + .await + .unwrap(); + let updated_auth_account = AuthAccount::new( + account_id.clone(), + host_id, + AuthAccountClientId::new("updated_client_id".to_string()), + EventVersion::new(Uuid::now_v7()), + ); + database + .auth_account_modifier() + .update(&mut transaction, &updated_auth_account) + .await + .unwrap(); + let result = database + .auth_account_query() + .find_by_id(&mut transaction, &account_id) + .await + .unwrap(); + assert_eq!(result, Some(updated_auth_account)); + database + .auth_account_modifier() + .delete(&mut transaction, auth_account.id()) + .await + .unwrap(); + } + + #[tokio::test] + async fn delete() { + let database = PostgresDatabase::new().await.unwrap(); + let mut transaction = database.begin_transaction().await.unwrap(); + + let host_id = AuthHostId::new(Uuid::now_v7()); + let auth_host = AuthHost::new(host_id.clone(), AuthHostUrl::new(Uuid::now_v7())); + database + .auth_host_modifier() + .create(&mut transaction, &auth_host) + .await + .unwrap(); + let account_id = AuthAccountId::new(Uuid::now_v7()); + let auth_account = AuthAccount::new( + account_id.clone(), + host_id, + AuthAccountClientId::new("client_id".to_string()), + EventVersion::new(Uuid::now_v7()), + ); + database + .auth_account_modifier() + .create(&mut transaction, &auth_account) + .await + .unwrap(); + database + .auth_account_modifier() + .delete(&mut transaction, &account_id) + .await + .unwrap(); + let result = database + .auth_account_query() + .find_by_id(&mut transaction, &account_id) + .await + .unwrap(); + assert_eq!(result, None); + database + .auth_account_modifier() + .delete(&mut transaction, auth_account.id()) + .await + .unwrap(); + } + } +} diff --git a/driver/src/database/postgres/auth_host.rs b/driver/src/database/postgres/auth_host.rs new file mode 100644 index 0000000..a13315c --- /dev/null +++ b/driver/src/database/postgres/auth_host.rs @@ -0,0 +1,250 @@ +use crate::database::{PostgresConnection, PostgresDatabase}; +use crate::ConvertError; +use kernel::interfaces::modify::{AuthHostModifier, DependOnAuthHostModifier}; +use kernel::interfaces::query::{AuthHostQuery, DependOnAuthHostQuery}; +use kernel::prelude::entity::{AuthHost, AuthHostId, AuthHostUrl}; +use kernel::KernelError; +use sqlx::PgConnection; +use uuid::Uuid; + +#[derive(sqlx::FromRow)] +struct AuthHostRow { + id: Uuid, + url: String, +} + +impl From for AuthHost { + fn from(row: AuthHostRow) -> Self { + AuthHost::new(AuthHostId::new(row.id), AuthHostUrl::new(row.url)) + } +} + +pub struct PostgresAuthHostRepository; + +impl AuthHostQuery for PostgresAuthHostRepository { + type Transaction = PostgresConnection; + async fn find_by_id( + &self, + transaction: &mut Self::Transaction, + id: &AuthHostId, + ) -> error_stack::Result, KernelError> { + let con: &mut PgConnection = transaction; + sqlx::query_as::<_, AuthHostRow>( + // language=postgresql + r#" + SELECT id, url + FROM auth_hosts + WHERE id = $1 + "#, + ) + .bind(id.as_ref()) + .fetch_optional(con) + .await + .convert_error() + .map(|row| row.map(AuthHost::from)) + } + + async fn find_by_url( + &self, + transaction: &mut Self::Transaction, + domain: &AuthHostUrl, + ) -> error_stack::Result, KernelError> { + let con: &mut PgConnection = transaction; + sqlx::query_as::<_, AuthHostRow>( + // language=postgresql + r#" + SELECT id, url + FROM auth_hosts + WHERE url = $1 + "#, + ) + .bind(domain.as_ref()) + .fetch_optional(con) + .await + .convert_error() + .map(|row| row.map(AuthHost::from)) + } +} + +impl DependOnAuthHostQuery for PostgresDatabase { + type AuthHostQuery = PostgresAuthHostRepository; + + fn auth_host_query(&self) -> &Self::AuthHostQuery { + &PostgresAuthHostRepository + } +} + +impl AuthHostModifier for PostgresAuthHostRepository { + type Transaction = PostgresConnection; + async fn create( + &self, + transaction: &mut Self::Transaction, + auth_host: &AuthHost, + ) -> error_stack::Result<(), KernelError> { + let con: &mut PgConnection = transaction; + sqlx::query( + // language=postgresql + r#" + INSERT INTO auth_hosts (id, url) + VALUES ($1, $2) + "#, + ) + .bind(auth_host.id().as_ref()) + .bind(auth_host.url().as_ref()) + .execute(con) + .await + .convert_error() + .map(|_| ()) + } + + async fn update( + &self, + transaction: &mut Self::Transaction, + auth_host: &AuthHost, + ) -> error_stack::Result<(), KernelError> { + let con: &mut PgConnection = transaction; + sqlx::query( + // language=postgresql + r#" + UPDATE auth_hosts + SET url = $2 + WHERE id = $1 + "#, + ) + .bind(auth_host.id().as_ref()) + .bind(auth_host.url().as_ref()) + .execute(con) + .await + .convert_error() + .map(|_| ()) + } +} + +impl DependOnAuthHostModifier for PostgresDatabase { + type AuthHostModifier = PostgresAuthHostRepository; + + fn auth_host_modifier(&self) -> &Self::AuthHostModifier { + &PostgresAuthHostRepository + } +} + +#[cfg(test)] +mod test { + use kernel::prelude::entity::AuthHostUrl; + use uuid::Uuid; + + fn url() -> AuthHostUrl { + AuthHostUrl::new(format!("https://{}.example.com", Uuid::now_v7())) + } + + mod query { + use crate::database::postgres::auth_host::test::url; + use crate::database::PostgresDatabase; + use kernel::interfaces::database::DatabaseConnection; + use kernel::interfaces::modify::{AuthHostModifier, DependOnAuthHostModifier}; + use kernel::interfaces::query::{AuthHostQuery, DependOnAuthHostQuery}; + use kernel::prelude::entity::{AuthHost, AuthHostId}; + use uuid::Uuid; + + #[tokio::test] + async fn find_by_id() { + let database = PostgresDatabase::new().await.unwrap(); + let mut transaction = database.begin_transaction().await.unwrap(); + + let auth_host = AuthHost::new(AuthHostId::new(Uuid::now_v7()), url()); + database + .auth_host_modifier() + .create(&mut transaction, &auth_host) + .await + .unwrap(); + + let found_auth_host = database + .auth_host_query() + .find_by_id(&mut transaction, auth_host.id()) + .await + .unwrap() + .unwrap(); + assert_eq!(auth_host, found_auth_host); + } + + #[tokio::test] + async fn find_by_url() { + let database = PostgresDatabase::new().await.unwrap(); + let mut transaction = database.begin_transaction().await.unwrap(); + + let auth_host = AuthHost::new(AuthHostId::new(Uuid::now_v7()), url()); + database + .auth_host_modifier() + .create(&mut transaction, &auth_host) + .await + .unwrap(); + + let found_auth_host = database + .auth_host_query() + .find_by_url(&mut transaction, auth_host.url()) + .await + .unwrap() + .unwrap(); + assert_eq!(auth_host, found_auth_host); + } + } + + mod modify { + use crate::database::postgres::auth_host::test::url; + use crate::database::PostgresDatabase; + use kernel::interfaces::database::DatabaseConnection; + use kernel::interfaces::modify::{AuthHostModifier, DependOnAuthHostModifier}; + use kernel::interfaces::query::{AuthHostQuery, DependOnAuthHostQuery}; + use kernel::prelude::entity::{AuthHost, AuthHostId}; + use uuid::Uuid; + + #[tokio::test] + async fn create() { + let database = PostgresDatabase::new().await.unwrap(); + let mut transaction = database.begin_transaction().await.unwrap(); + + let auth_host = AuthHost::new(AuthHostId::new(Uuid::now_v7()), url()); + database + .auth_host_modifier() + .create(&mut transaction, &auth_host) + .await + .unwrap(); + + let found_auth_host = database + .auth_host_query() + .find_by_id(&mut transaction, auth_host.id()) + .await + .unwrap() + .unwrap(); + assert_eq!(auth_host, found_auth_host); + } + + #[tokio::test] + async fn update() { + let database = PostgresDatabase::new().await.unwrap(); + let mut transaction = database.begin_transaction().await.unwrap(); + + let auth_host = AuthHost::new(AuthHostId::new(Uuid::now_v7()), url()); + database + .auth_host_modifier() + .create(&mut transaction, &auth_host) + .await + .unwrap(); + + let updated_auth_host = AuthHost::new(auth_host.id().clone(), url()); + database + .auth_host_modifier() + .update(&mut transaction, &updated_auth_host) + .await + .unwrap(); + + let found_auth_host = database + .auth_host_query() + .find_by_id(&mut transaction, auth_host.id()) + .await + .unwrap() + .unwrap(); + assert_eq!(updated_auth_host, found_auth_host); + } + } +} diff --git a/driver/src/database/postgres/stellar_account.rs b/driver/src/database/postgres/stellar_account.rs deleted file mode 100644 index 06ec468..0000000 --- a/driver/src/database/postgres/stellar_account.rs +++ /dev/null @@ -1,363 +0,0 @@ -use crate::database::{PostgresConnection, PostgresDatabase}; -use crate::ConvertError; -use kernel::interfaces::modify::{DependOnStellarAccountModifier, StellarAccountModifier}; -use kernel::interfaces::query::{DependOnStellarAccountQuery, StellarAccountQuery}; -use kernel::prelude::entity::{ - EventVersion, StellarAccount, StellarAccountAccessToken, StellarAccountClientId, - StellarAccountId, StellarAccountRefreshToken, StellarHostId, -}; -use kernel::KernelError; -use sqlx::PgConnection; -use uuid::Uuid; - -#[derive(sqlx::FromRow)] -struct StellarAccountRow { - id: Uuid, - host_id: Uuid, - client_id: String, - access_token: String, - refresh_token: String, - version: Uuid, -} - -impl From for StellarAccount { - fn from(value: StellarAccountRow) -> Self { - StellarAccount::new( - StellarAccountId::new(value.id), - StellarHostId::new(value.host_id), - StellarAccountClientId::new(value.client_id), - StellarAccountAccessToken::new(value.access_token), - StellarAccountRefreshToken::new(value.refresh_token), - EventVersion::new(value.version), - ) - } -} - -pub struct PostgresStellarAccountRepository; - -impl StellarAccountQuery for PostgresStellarAccountRepository { - type Transaction = PostgresConnection; - - async fn find_by_id( - &self, - transaction: &mut Self::Transaction, - account_id: &StellarAccountId, - ) -> error_stack::Result, KernelError> { - let con: &mut PgConnection = transaction; - sqlx::query_as::<_, StellarAccountRow>( - //language=postgresql - r#" - SELECT id, host_id, client_id, access_token, refresh_token, version - FROM stellar_accounts - WHERE id = $1 - "#, - ) - .bind(account_id.as_ref()) - .fetch_optional(con) - .await - .convert_error() - .map(|option| option.map(|row| row.into())) - } -} - -impl DependOnStellarAccountQuery for PostgresDatabase { - type StellarAccountQuery = PostgresStellarAccountRepository; - - fn stellar_account_query(&self) -> &Self::StellarAccountQuery { - &PostgresStellarAccountRepository - } -} - -impl StellarAccountModifier for PostgresStellarAccountRepository { - type Transaction = PostgresConnection; - - async fn create( - &self, - transaction: &mut Self::Transaction, - stellar_account: &StellarAccount, - ) -> error_stack::Result<(), KernelError> { - let con: &mut PgConnection = transaction; - sqlx::query( - //language=postgresql - r#" - INSERT INTO stellar_accounts (id, host_id, client_id, access_token, refresh_token, version) VALUES ($1, $2, $3, $4, $5, $6) - "# - ) - .bind(stellar_account.id().as_ref()) - .bind(stellar_account.host().as_ref()) - .bind(stellar_account.client_id().as_ref()) - .bind(stellar_account.access_token().as_ref()) - .bind(stellar_account.refresh_token().as_ref()) - .bind(stellar_account.version().as_ref()) - .execute(con) - .await - .convert_error()?; - Ok(()) - } - - async fn update( - &self, - transaction: &mut Self::Transaction, - stellar_account: &StellarAccount, - ) -> error_stack::Result<(), KernelError> { - let con: &mut PgConnection = transaction; - sqlx::query( - //language=postgresql - r#" - UPDATE stellar_accounts SET host_id = $2, client_id = $3, access_token = $4, refresh_token = $5, version = $6 - WHERE id = $1 - "# - ) - .bind(stellar_account.id().as_ref()) - .bind(stellar_account.host().as_ref()) - .bind(stellar_account.client_id().as_ref()) - .bind(stellar_account.access_token().as_ref()) - .bind(stellar_account.refresh_token().as_ref()) - .bind(stellar_account.version().as_ref()) - .execute(con) - .await - .convert_error()?; - Ok(()) - } - - async fn delete( - &self, - transaction: &mut Self::Transaction, - account_id: &StellarAccountId, - ) -> error_stack::Result<(), KernelError> { - let con: &mut PgConnection = transaction; - sqlx::query( - //language=postgresql - r#" - DELETE FROM stellar_accounts WHERE id = $1 - "#, - ) - .bind(account_id.as_ref()) - .execute(con) - .await - .convert_error()?; - Ok(()) - } -} - -impl DependOnStellarAccountModifier for PostgresDatabase { - type StellarAccountModifier = PostgresStellarAccountRepository; - - fn stellar_account_modifier(&self) -> &Self::StellarAccountModifier { - &PostgresStellarAccountRepository - } -} - -#[cfg(test)] -mod test { - mod query { - use crate::database::PostgresDatabase; - use kernel::interfaces::database::DatabaseConnection; - use kernel::interfaces::modify::{ - DependOnStellarAccountModifier, DependOnStellarHostModifier, StellarAccountModifier, - StellarHostModifier, - }; - use kernel::interfaces::query::{DependOnStellarAccountQuery, StellarAccountQuery}; - use kernel::prelude::entity::{ - EventVersion, StellarAccount, StellarAccountAccessToken, StellarAccountClientId, - StellarAccountId, StellarAccountRefreshToken, StellarHost, StellarHostId, - StellarHostUrl, - }; - use uuid::Uuid; - - #[tokio::test] - async fn find_by_id() { - let database = PostgresDatabase::new().await.unwrap(); - let mut transaction = database.begin_transaction().await.unwrap(); - - let stellar_host_id = StellarHostId::new(Uuid::now_v7()); - let stellar_host = - StellarHost::new(stellar_host_id.clone(), StellarHostUrl::new(Uuid::now_v7())); - database - .stellar_host_modifier() - .create(&mut transaction, &stellar_host) - .await - .unwrap(); - let account_id = StellarAccountId::new(Uuid::now_v7()); - let stellar_account = StellarAccount::new( - account_id.clone(), - stellar_host_id, - StellarAccountClientId::new("client_id".to_string()), - StellarAccountAccessToken::new("access_token".to_string()), - StellarAccountRefreshToken::new("refresh_token".to_string()), - EventVersion::new(Uuid::now_v7()), - ); - - database - .stellar_account_modifier() - .create(&mut transaction, &stellar_account) - .await - .unwrap(); - let result = database - .stellar_account_query() - .find_by_id(&mut transaction, &account_id) - .await - .unwrap(); - assert_eq!(result, Some(stellar_account.clone())); - database - .stellar_account_modifier() - .delete(&mut transaction, stellar_account.id()) - .await - .unwrap(); - } - } - - mod modify { - use crate::database::PostgresDatabase; - use kernel::interfaces::database::DatabaseConnection; - use kernel::interfaces::modify::{ - DependOnStellarAccountModifier, DependOnStellarHostModifier, StellarAccountModifier, - StellarHostModifier, - }; - use kernel::interfaces::query::{DependOnStellarAccountQuery, StellarAccountQuery}; - use kernel::prelude::entity::{ - EventVersion, StellarAccount, StellarAccountAccessToken, StellarAccountClientId, - StellarAccountId, StellarAccountRefreshToken, StellarHost, StellarHostId, - StellarHostUrl, - }; - use uuid::Uuid; - - #[tokio::test] - async fn create() { - let database = PostgresDatabase::new().await.unwrap(); - let mut transaction = database.begin_transaction().await.unwrap(); - - let host_id = StellarHostId::new(Uuid::now_v7()); - let account_id = StellarAccountId::new(Uuid::now_v7()); - let stellar_host = - StellarHost::new(host_id.clone(), StellarHostUrl::new(Uuid::now_v7())); - database - .stellar_host_modifier() - .create(&mut transaction, &stellar_host) - .await - .unwrap(); - let stellar_account = StellarAccount::new( - account_id.clone(), - host_id, - StellarAccountClientId::new("client_id".to_string()), - StellarAccountAccessToken::new("access_token".to_string()), - StellarAccountRefreshToken::new("refresh_token".to_string()), - EventVersion::new(Uuid::now_v7()), - ); - database - .stellar_account_modifier() - .create(&mut transaction, &stellar_account) - .await - .unwrap(); - let result = database - .stellar_account_query() - .find_by_id(&mut transaction, &account_id) - .await - .unwrap(); - assert_eq!(result, Some(stellar_account.clone())); - database - .stellar_account_modifier() - .delete(&mut transaction, stellar_account.id()) - .await - .unwrap(); - } - - #[tokio::test] - async fn update() { - let database = PostgresDatabase::new().await.unwrap(); - let mut transaction = database.begin_transaction().await.unwrap(); - - let host_id = StellarHostId::new(Uuid::now_v7()); - let account_id = StellarAccountId::new(Uuid::now_v7()); - let stellar_host = - StellarHost::new(host_id.clone(), StellarHostUrl::new(Uuid::now_v7())); - database - .stellar_host_modifier() - .create(&mut transaction, &stellar_host) - .await - .unwrap(); - let stellar_account = StellarAccount::new( - account_id.clone(), - host_id.clone(), - StellarAccountClientId::new("client_id".to_string()), - StellarAccountAccessToken::new("access_token".to_string()), - StellarAccountRefreshToken::new("refresh_token".to_string()), - EventVersion::new(Uuid::now_v7()), - ); - database - .stellar_account_modifier() - .create(&mut transaction, &stellar_account) - .await - .unwrap(); - let updated_stellar_account = StellarAccount::new( - account_id.clone(), - host_id, - StellarAccountClientId::new("updated_client_id".to_string()), - StellarAccountAccessToken::new("updated_access_token".to_string()), - StellarAccountRefreshToken::new("updated_refresh_token".to_string()), - EventVersion::new(Uuid::now_v7()), - ); - database - .stellar_account_modifier() - .update(&mut transaction, &updated_stellar_account) - .await - .unwrap(); - let result = database - .stellar_account_query() - .find_by_id(&mut transaction, &account_id) - .await - .unwrap(); - assert_eq!(result, Some(updated_stellar_account)); - database - .stellar_account_modifier() - .delete(&mut transaction, stellar_account.id()) - .await - .unwrap(); - } - - #[tokio::test] - async fn delete() { - let database = PostgresDatabase::new().await.unwrap(); - let mut transaction = database.begin_transaction().await.unwrap(); - - let host_id = StellarHostId::new(Uuid::now_v7()); - let stellar_host = - StellarHost::new(host_id.clone(), StellarHostUrl::new(Uuid::now_v7())); - database - .stellar_host_modifier() - .create(&mut transaction, &stellar_host) - .await - .unwrap(); - let account_id = StellarAccountId::new(Uuid::now_v7()); - let stellar_account = StellarAccount::new( - account_id.clone(), - host_id, - StellarAccountClientId::new("client_id".to_string()), - StellarAccountAccessToken::new("access_token".to_string()), - StellarAccountRefreshToken::new("refresh_token".to_string()), - EventVersion::new(Uuid::now_v7()), - ); - database - .stellar_account_modifier() - .create(&mut transaction, &stellar_account) - .await - .unwrap(); - database - .stellar_account_modifier() - .delete(&mut transaction, &account_id) - .await - .unwrap(); - let result = database - .stellar_account_query() - .find_by_id(&mut transaction, &account_id) - .await - .unwrap(); - assert_eq!(result, None); - database - .stellar_account_modifier() - .delete(&mut transaction, stellar_account.id()) - .await - .unwrap(); - } - } -} diff --git a/driver/src/database/postgres/stellar_host.rs b/driver/src/database/postgres/stellar_host.rs deleted file mode 100644 index 2cdfccb..0000000 --- a/driver/src/database/postgres/stellar_host.rs +++ /dev/null @@ -1,250 +0,0 @@ -use crate::database::{PostgresConnection, PostgresDatabase}; -use crate::ConvertError; -use kernel::interfaces::modify::{DependOnStellarHostModifier, StellarHostModifier}; -use kernel::interfaces::query::{DependOnStellarHostQuery, StellarHostQuery}; -use kernel::prelude::entity::{StellarHost, StellarHostId, StellarHostUrl}; -use kernel::KernelError; -use sqlx::PgConnection; -use uuid::Uuid; - -#[derive(sqlx::FromRow)] -struct StellarHostRow { - id: Uuid, - url: String, -} - -impl From for StellarHost { - fn from(row: StellarHostRow) -> Self { - StellarHost::new(StellarHostId::new(row.id), StellarHostUrl::new(row.url)) - } -} - -pub struct PostgresStellarHostRepository; - -impl StellarHostQuery for PostgresStellarHostRepository { - type Transaction = PostgresConnection; - async fn find_by_id( - &self, - transaction: &mut Self::Transaction, - id: &StellarHostId, - ) -> error_stack::Result, KernelError> { - let con: &mut PgConnection = transaction; - sqlx::query_as::<_, StellarHostRow>( - // language=postgresql - r#" - SELECT id, url - FROM stellar_hosts - WHERE id = $1 - "#, - ) - .bind(id.as_ref()) - .fetch_optional(con) - .await - .convert_error() - .map(|row| row.map(StellarHost::from)) - } - - async fn find_by_url( - &self, - transaction: &mut Self::Transaction, - domain: &StellarHostUrl, - ) -> error_stack::Result, KernelError> { - let con: &mut PgConnection = transaction; - sqlx::query_as::<_, StellarHostRow>( - // language=postgresql - r#" - SELECT id, url - FROM stellar_hosts - WHERE url = $1 - "#, - ) - .bind(domain.as_ref()) - .fetch_optional(con) - .await - .convert_error() - .map(|row| row.map(StellarHost::from)) - } -} - -impl DependOnStellarHostQuery for PostgresDatabase { - type StellarHostQuery = PostgresStellarHostRepository; - - fn stellar_host_query(&self) -> &Self::StellarHostQuery { - &PostgresStellarHostRepository - } -} - -impl StellarHostModifier for PostgresStellarHostRepository { - type Transaction = PostgresConnection; - async fn create( - &self, - transaction: &mut Self::Transaction, - stellar_host: &StellarHost, - ) -> error_stack::Result<(), KernelError> { - let con: &mut PgConnection = transaction; - sqlx::query( - // language=postgresql - r#" - INSERT INTO stellar_hosts (id, url) - VALUES ($1, $2) - "#, - ) - .bind(stellar_host.id().as_ref()) - .bind(stellar_host.url().as_ref()) - .execute(con) - .await - .convert_error() - .map(|_| ()) - } - - async fn update( - &self, - transaction: &mut Self::Transaction, - stellar_host: &StellarHost, - ) -> error_stack::Result<(), KernelError> { - let con: &mut PgConnection = transaction; - sqlx::query( - // language=postgresql - r#" - UPDATE stellar_hosts - SET url = $2 - WHERE id = $1 - "#, - ) - .bind(stellar_host.id().as_ref()) - .bind(stellar_host.url().as_ref()) - .execute(con) - .await - .convert_error() - .map(|_| ()) - } -} - -impl DependOnStellarHostModifier for PostgresDatabase { - type StellarHostModifier = PostgresStellarHostRepository; - - fn stellar_host_modifier(&self) -> &Self::StellarHostModifier { - &PostgresStellarHostRepository - } -} - -#[cfg(test)] -mod test { - use kernel::prelude::entity::StellarHostUrl; - use uuid::Uuid; - - fn url() -> StellarHostUrl { - StellarHostUrl::new(format!("https://{}.example.com", Uuid::now_v7())) - } - - mod query { - use crate::database::postgres::stellar_host::test::url; - use crate::database::PostgresDatabase; - use kernel::interfaces::database::DatabaseConnection; - use kernel::interfaces::modify::{DependOnStellarHostModifier, StellarHostModifier}; - use kernel::interfaces::query::{DependOnStellarHostQuery, StellarHostQuery}; - use kernel::prelude::entity::{StellarHost, StellarHostId}; - use uuid::Uuid; - - #[tokio::test] - async fn find_by_id() { - let database = PostgresDatabase::new().await.unwrap(); - let mut transaction = database.begin_transaction().await.unwrap(); - - let stellar_host = StellarHost::new(StellarHostId::new(Uuid::now_v7()), url()); - database - .stellar_host_modifier() - .create(&mut transaction, &stellar_host) - .await - .unwrap(); - - let found_stellar_host = database - .stellar_host_query() - .find_by_id(&mut transaction, stellar_host.id()) - .await - .unwrap() - .unwrap(); - assert_eq!(stellar_host, found_stellar_host); - } - - #[tokio::test] - async fn find_by_url() { - let database = PostgresDatabase::new().await.unwrap(); - let mut transaction = database.begin_transaction().await.unwrap(); - - let stellar_host = StellarHost::new(StellarHostId::new(Uuid::now_v7()), url()); - database - .stellar_host_modifier() - .create(&mut transaction, &stellar_host) - .await - .unwrap(); - - let found_stellar_host = database - .stellar_host_query() - .find_by_url(&mut transaction, stellar_host.url()) - .await - .unwrap() - .unwrap(); - assert_eq!(stellar_host, found_stellar_host); - } - } - - mod modify { - use crate::database::postgres::stellar_host::test::url; - use crate::database::PostgresDatabase; - use kernel::interfaces::database::DatabaseConnection; - use kernel::interfaces::modify::{DependOnStellarHostModifier, StellarHostModifier}; - use kernel::interfaces::query::{DependOnStellarHostQuery, StellarHostQuery}; - use kernel::prelude::entity::{StellarHost, StellarHostId}; - use uuid::Uuid; - - #[tokio::test] - async fn create() { - let database = PostgresDatabase::new().await.unwrap(); - let mut transaction = database.begin_transaction().await.unwrap(); - - let stellar_host = StellarHost::new(StellarHostId::new(Uuid::now_v7()), url()); - database - .stellar_host_modifier() - .create(&mut transaction, &stellar_host) - .await - .unwrap(); - - let found_stellar_host = database - .stellar_host_query() - .find_by_id(&mut transaction, stellar_host.id()) - .await - .unwrap() - .unwrap(); - assert_eq!(stellar_host, found_stellar_host); - } - - #[tokio::test] - async fn update() { - let database = PostgresDatabase::new().await.unwrap(); - let mut transaction = database.begin_transaction().await.unwrap(); - - let stellar_host = StellarHost::new(StellarHostId::new(Uuid::now_v7()), url()); - database - .stellar_host_modifier() - .create(&mut transaction, &stellar_host) - .await - .unwrap(); - - let updated_stellar_host = StellarHost::new(stellar_host.id().clone(), url()); - database - .stellar_host_modifier() - .update(&mut transaction, &updated_stellar_host) - .await - .unwrap(); - - let found_stellar_host = database - .stellar_host_query() - .find_by_id(&mut transaction, stellar_host.id()) - .await - .unwrap() - .unwrap(); - assert_eq!(updated_stellar_host, found_stellar_host); - } - } -} diff --git a/kernel/src/entity.rs b/kernel/src/entity.rs index ab6c058..58f1652 100644 --- a/kernel/src/entity.rs +++ b/kernel/src/entity.rs @@ -1,4 +1,6 @@ mod account; +mod auth_account; +mod auth_host; mod common; mod event; mod follow; @@ -6,10 +8,10 @@ mod image; mod metadata; mod profile; mod remote_account; -mod stellar_account; -mod stellar_host; pub use self::account::*; +pub use self::auth_account::*; +pub use self::auth_host::*; pub use self::common::*; pub use self::event::*; pub use self::follow::*; @@ -17,5 +19,3 @@ pub use self::image::*; pub use self::metadata::*; pub use self::profile::*; pub use self::remote_account::*; -pub use self::stellar_account::*; -pub use self::stellar_host::*; diff --git a/kernel/src/entity/auth_account.rs b/kernel/src/entity/auth_account.rs new file mode 100644 index 0000000..eb87c07 --- /dev/null +++ b/kernel/src/entity/auth_account.rs @@ -0,0 +1,144 @@ +mod client_id; +mod id; + +pub use self::client_id::*; +pub use self::id::*; +use crate::entity::{ + AuthHostId, CommandEnvelope, EventEnvelope, EventId, EventVersion, KnownEventVersion, +}; +use crate::event::EventApplier; +use crate::KernelError; +use destructure::Destructure; +use error_stack::Report; +use serde::Deserialize; +use serde::Serialize; +use vodca::{Nameln, Newln, References}; + +#[derive( + Debug, Clone, Hash, Eq, PartialEq, References, Newln, Serialize, Deserialize, Destructure, +)] +pub struct AuthAccount { + id: AuthAccountId, + host: AuthHostId, + client_id: AuthAccountClientId, + version: EventVersion, +} + +#[derive(Debug, Clone, Eq, PartialEq, Nameln, Serialize, Deserialize)] +#[serde(tag = "type", rename_all_fields = "snake_case")] +#[vodca(prefix = "auth_account", snake_case)] +pub enum AuthAccountEvent { + Created { + host: AuthHostId, + client_id: AuthAccountClientId, + }, + Deleted, +} + +impl AuthAccount { + pub fn create( + id: AuthAccountId, + host: AuthHostId, + client_id: AuthAccountClientId, + ) -> CommandEnvelope { + let event = AuthAccountEvent::Created { host, client_id }; + CommandEnvelope::new( + EventId::from(id), + event.name(), + event, + Some(KnownEventVersion::Nothing), + ) + } + + pub fn delete(id: AuthAccountId) -> CommandEnvelope { + let event = AuthAccountEvent::Deleted; + CommandEnvelope::new(EventId::from(id), event.name(), event, None) + } +} + +impl EventApplier for AuthAccount { + type Event = AuthAccountEvent; + const ENTITY_NAME: &'static str = "AuthAccount"; + + fn apply( + entity: &mut Option, + event: EventEnvelope, + ) -> error_stack::Result<(), KernelError> + where + Self: Sized, + { + match event.event { + AuthAccountEvent::Created { host, client_id } => { + if let Some(entity) = entity { + return Err(Report::new(KernelError::Internal) + .attach_printable(Self::already_exists(entity))); + } + *entity = Some(AuthAccount { + id: AuthAccountId::new(event.id), + host, + client_id, + version: event.version, + }); + } + AuthAccountEvent::Deleted => { + if entity.is_none() { + return Err(Report::new(KernelError::Internal) + .attach_printable(Self::not_exists(event.id.as_ref()))); + } + *entity = None; + } + } + Ok(()) + } +} + +#[cfg(test)] +mod test { + use crate::entity::{ + AuthAccount, AuthAccountClientId, AuthAccountId, AuthHostId, EventEnvelope, EventVersion, + }; + use crate::event::EventApplier; + use uuid::Uuid; + + #[test] + fn create_auth_account() { + let id = AuthAccountId::new(Uuid::now_v7()); + let host = AuthHostId::new(Uuid::now_v7()); + let client_id = AuthAccountClientId::new(Uuid::now_v7()); + let create_account = AuthAccount::create(id.clone(), host.clone(), client_id.clone()); + let envelope = EventEnvelope::new( + create_account.id().clone(), + create_account.event().clone(), + EventVersion::new(Uuid::now_v7()), + ); + let mut account = None; + AuthAccount::apply(&mut account, envelope).unwrap(); + assert!(account.is_some()); + let account = account.unwrap(); + assert_eq!(account.id(), &id); + assert_eq!(account.host(), &host); + assert_eq!(account.client_id(), &client_id); + } + + #[test] + fn delete_auth_account() { + let id = AuthAccountId::new(Uuid::now_v7()); + let host = AuthHostId::new(Uuid::now_v7()); + let client_id = AuthAccountClientId::new(Uuid::now_v7()); + let account = AuthAccount::new( + id.clone(), + host.clone(), + client_id.clone(), + EventVersion::new(Uuid::now_v7()), + ); + let delete_account = AuthAccount::delete(id.clone()); + let envelope = EventEnvelope::new( + delete_account.id().clone(), + delete_account.event().clone(), + EventVersion::new(Uuid::now_v7()), + ); + let mut account = Some(account); + AuthAccount::apply(&mut account, envelope).unwrap(); + assert!(account.is_none()); + } +} diff --git a/kernel/src/entity/stellar_account/client_id.rs b/kernel/src/entity/auth_account/client_id.rs similarity index 79% rename from kernel/src/entity/stellar_account/client_id.rs rename to kernel/src/entity/auth_account/client_id.rs index 3baa401..e197180 100644 --- a/kernel/src/entity/stellar_account/client_id.rs +++ b/kernel/src/entity/auth_account/client_id.rs @@ -2,4 +2,4 @@ use serde::{Deserialize, Serialize}; use vodca::{AsRefln, Fromln, Newln}; #[derive(Debug, Clone, PartialEq, Eq, Hash, Fromln, AsRefln, Newln, Serialize, Deserialize)] -pub struct StellarAccountClientId(String); +pub struct AuthAccountClientId(String); diff --git a/kernel/src/entity/auth_account/id.rs b/kernel/src/entity/auth_account/id.rs new file mode 100644 index 0000000..9c6d074 --- /dev/null +++ b/kernel/src/entity/auth_account/id.rs @@ -0,0 +1,13 @@ +use crate::entity::{AuthAccount, AuthAccountEvent, EventId}; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; +use vodca::{AsRefln, Fromln, Newln}; + +#[derive(Debug, Clone, PartialEq, Eq, Hash, Fromln, AsRefln, Newln, Serialize, Deserialize)] +pub struct AuthAccountId(Uuid); + +impl From for EventId { + fn from(auth_account_id: AuthAccountId) -> Self { + EventId::new(auth_account_id.0) + } +} diff --git a/kernel/src/entity/stellar_host.rs b/kernel/src/entity/auth_host.rs similarity index 74% rename from kernel/src/entity/stellar_host.rs rename to kernel/src/entity/auth_host.rs index fcf209b..f453864 100644 --- a/kernel/src/entity/stellar_host.rs +++ b/kernel/src/entity/auth_host.rs @@ -6,7 +6,7 @@ use serde::{Deserialize, Serialize}; use vodca::{Newln, References}; #[derive(Debug, Clone, PartialEq, Eq, Hash, References, Newln, Serialize, Deserialize)] -pub struct StellarHost { - id: StellarHostId, - url: StellarHostUrl, +pub struct AuthHost { + id: AuthHostId, + url: AuthHostUrl, } diff --git a/kernel/src/entity/stellar_host/id.rs b/kernel/src/entity/auth_host/id.rs similarity index 85% rename from kernel/src/entity/stellar_host/id.rs rename to kernel/src/entity/auth_host/id.rs index c485211..34fb6b5 100644 --- a/kernel/src/entity/stellar_host/id.rs +++ b/kernel/src/entity/auth_host/id.rs @@ -3,4 +3,4 @@ use uuid::Uuid; use vodca::{AsRefln, Fromln, Newln}; #[derive(Debug, Clone, PartialEq, Eq, Hash, Fromln, AsRefln, Newln, Serialize, Deserialize)] -pub struct StellarHostId(Uuid); +pub struct AuthHostId(Uuid); diff --git a/kernel/src/entity/stellar_host/url.rs b/kernel/src/entity/auth_host/url.rs similarity index 82% rename from kernel/src/entity/stellar_host/url.rs rename to kernel/src/entity/auth_host/url.rs index 00b2cee..00e8c22 100644 --- a/kernel/src/entity/stellar_host/url.rs +++ b/kernel/src/entity/auth_host/url.rs @@ -2,4 +2,4 @@ use serde::{Deserialize, Serialize}; use vodca::{AsRefln, Fromln, Newln}; #[derive(Debug, Clone, PartialEq, Eq, Hash, Fromln, AsRefln, Newln, Serialize, Deserialize)] -pub struct StellarHostUrl(String); +pub struct AuthHostUrl(String); diff --git a/kernel/src/entity/stellar_account.rs b/kernel/src/entity/stellar_account.rs deleted file mode 100644 index f9c5c3a..0000000 --- a/kernel/src/entity/stellar_account.rs +++ /dev/null @@ -1,250 +0,0 @@ -mod access_token; -mod client_id; -mod id; -mod refresh_token; - -pub use self::access_token::*; -pub use self::client_id::*; -pub use self::id::*; -pub use self::refresh_token::*; -use crate::entity::{ - CommandEnvelope, EventEnvelope, EventId, EventVersion, KnownEventVersion, StellarHostId, -}; -use crate::event::EventApplier; -use crate::KernelError; -use destructure::Destructure; -use error_stack::Report; -use serde::Deserialize; -use serde::Serialize; -use vodca::{Nameln, Newln, References}; - -#[derive( - Debug, Clone, Hash, Eq, PartialEq, References, Newln, Serialize, Deserialize, Destructure, -)] -pub struct StellarAccount { - id: StellarAccountId, - host: StellarHostId, - client_id: StellarAccountClientId, - access_token: StellarAccountAccessToken, - refresh_token: StellarAccountRefreshToken, - version: EventVersion, -} - -#[derive(Debug, Clone, Eq, PartialEq, Nameln, Serialize, Deserialize)] -#[serde(tag = "type", rename_all_fields = "snake_case")] -#[vodca(prefix = "stellar_account", snake_case)] -pub enum StellarAccountEvent { - Created { - host: StellarHostId, - client_id: StellarAccountClientId, - access_token: StellarAccountAccessToken, - refresh_token: StellarAccountRefreshToken, - }, - Updated { - access_token: StellarAccountAccessToken, - refresh_token: StellarAccountRefreshToken, - }, - Deleted, -} - -impl StellarAccount { - pub fn create( - id: StellarAccountId, - host: StellarHostId, - client_id: StellarAccountClientId, - access_token: StellarAccountAccessToken, - refresh_token: StellarAccountRefreshToken, - ) -> CommandEnvelope { - let event = StellarAccountEvent::Created { - host, - client_id, - access_token, - refresh_token, - }; - CommandEnvelope::new( - EventId::from(id), - event.name(), - event, - Some(KnownEventVersion::Nothing), - ) - } - - pub fn update( - id: StellarAccountId, - access_token: StellarAccountAccessToken, - refresh_token: StellarAccountRefreshToken, - ) -> CommandEnvelope { - let event = StellarAccountEvent::Updated { - access_token, - refresh_token, - }; - CommandEnvelope::new(EventId::from(id), event.name(), event, None) - } - - pub fn delete(id: StellarAccountId) -> CommandEnvelope { - let event = StellarAccountEvent::Deleted; - CommandEnvelope::new(EventId::from(id), event.name(), event, None) - } -} - -impl EventApplier for StellarAccount { - type Event = StellarAccountEvent; - const ENTITY_NAME: &'static str = "StellarAccount"; - - fn apply( - entity: &mut Option, - event: EventEnvelope, - ) -> error_stack::Result<(), KernelError> - where - Self: Sized, - { - match event.event { - StellarAccountEvent::Created { - host, - client_id, - access_token, - refresh_token, - } => { - if let Some(entity) = entity { - return Err(Report::new(KernelError::Internal) - .attach_printable(Self::already_exists(entity))); - } - *entity = Some(StellarAccount { - id: StellarAccountId::new(event.id), - host, - client_id, - access_token, - refresh_token, - version: event.version, - }); - } - StellarAccountEvent::Updated { - access_token, - refresh_token, - } => { - if let Some(entity) = entity { - entity.access_token = access_token; - entity.refresh_token = refresh_token; - entity.version = event.version; - } else { - return Err(Report::new(KernelError::Internal) - .attach_printable(Self::not_exists(event.id.as_ref()))); - } - } - StellarAccountEvent::Deleted => { - if entity.is_none() { - return Err(Report::new(KernelError::Internal) - .attach_printable(Self::not_exists(event.id.as_ref()))); - } - *entity = None; - } - } - Ok(()) - } -} - -#[cfg(test)] -mod test { - use crate::entity::{ - EventEnvelope, EventVersion, StellarAccount, StellarAccountAccessToken, - StellarAccountClientId, StellarAccountId, StellarAccountRefreshToken, StellarHostId, - }; - use crate::event::EventApplier; - use uuid::Uuid; - - #[test] - fn create_stellar_account() { - let id = StellarAccountId::new(Uuid::now_v7()); - let host = StellarHostId::new(Uuid::now_v7()); - let client_id = StellarAccountClientId::new(Uuid::now_v7()); - let access_token = StellarAccountAccessToken::new(Uuid::now_v7()); - let refresh_token = StellarAccountRefreshToken::new(Uuid::now_v7()); - let create_account = StellarAccount::create( - id.clone(), - host.clone(), - client_id.clone(), - access_token.clone(), - refresh_token.clone(), - ); - let envelope = EventEnvelope::new( - create_account.id().clone(), - create_account.event().clone(), - EventVersion::new(Uuid::now_v7()), - ); - let mut account = None; - StellarAccount::apply(&mut account, envelope).unwrap(); - assert!(account.is_some()); - let account = account.unwrap(); - assert_eq!(account.id(), &id); - assert_eq!(account.host(), &host); - assert_eq!(account.client_id(), &client_id); - assert_eq!(account.access_token(), &access_token); - assert_eq!(account.refresh_token(), &refresh_token); - } - - #[test] - fn update_stellar_account() { - let id = StellarAccountId::new(Uuid::now_v7()); - let host = StellarHostId::new(Uuid::now_v7()); - let client_id = StellarAccountClientId::new(Uuid::now_v7()); - let access_token = StellarAccountAccessToken::new(Uuid::now_v7()); - let refresh_token = StellarAccountRefreshToken::new(Uuid::now_v7()); - let account = StellarAccount::new( - id.clone(), - host.clone(), - client_id.clone(), - access_token.clone(), - refresh_token.clone(), - EventVersion::new(Uuid::now_v7()), - ); - let new_access_token = StellarAccountAccessToken::new(Uuid::now_v7()); - let new_refresh_token = StellarAccountRefreshToken::new(Uuid::now_v7()); - let update_account = StellarAccount::update( - id.clone(), - new_access_token.clone(), - new_refresh_token.clone(), - ); - let version = EventVersion::new(Uuid::now_v7()); - let envelope = EventEnvelope::new( - update_account.id().clone(), - update_account.event().clone(), - version.clone(), - ); - let mut account = Some(account); - StellarAccount::apply(&mut account, envelope).unwrap(); - assert!(account.is_some()); - let account = account.unwrap(); - assert_eq!(account.id(), &id); - assert_eq!(account.host(), &host); - assert_eq!(account.client_id(), &client_id); - assert_eq!(account.access_token(), &new_access_token); - assert_eq!(account.refresh_token(), &new_refresh_token); - assert_eq!(account.version(), &version); - } - - #[test] - fn delete_stellar_account() { - let id = StellarAccountId::new(Uuid::now_v7()); - let host = StellarHostId::new(Uuid::now_v7()); - let client_id = StellarAccountClientId::new(Uuid::now_v7()); - let access_token = StellarAccountAccessToken::new(Uuid::now_v7()); - let refresh_token = StellarAccountRefreshToken::new(Uuid::now_v7()); - let account = StellarAccount::new( - id.clone(), - host.clone(), - client_id.clone(), - access_token.clone(), - refresh_token.clone(), - EventVersion::new(Uuid::now_v7()), - ); - let delete_account = StellarAccount::delete(id.clone()); - let envelope = EventEnvelope::new( - delete_account.id().clone(), - delete_account.event().clone(), - EventVersion::new(Uuid::now_v7()), - ); - let mut account = Some(account); - StellarAccount::apply(&mut account, envelope).unwrap(); - assert!(account.is_none()); - } -} diff --git a/kernel/src/entity/stellar_account/access_token.rs b/kernel/src/entity/stellar_account/access_token.rs deleted file mode 100644 index 28da751..0000000 --- a/kernel/src/entity/stellar_account/access_token.rs +++ /dev/null @@ -1,5 +0,0 @@ -use serde::{Deserialize, Serialize}; -use vodca::{AsRefln, Fromln, Newln}; - -#[derive(Debug, Clone, PartialEq, Eq, Hash, Fromln, AsRefln, Newln, Serialize, Deserialize)] -pub struct StellarAccountAccessToken(String); diff --git a/kernel/src/entity/stellar_account/id.rs b/kernel/src/entity/stellar_account/id.rs deleted file mode 100644 index afbf46f..0000000 --- a/kernel/src/entity/stellar_account/id.rs +++ /dev/null @@ -1,13 +0,0 @@ -use crate::entity::{EventId, StellarAccount, StellarAccountEvent}; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; -use vodca::{AsRefln, Fromln, Newln}; - -#[derive(Debug, Clone, PartialEq, Eq, Hash, Fromln, AsRefln, Newln, Serialize, Deserialize)] -pub struct StellarAccountId(Uuid); - -impl From for EventId { - fn from(stellar_account_id: StellarAccountId) -> Self { - EventId::new(stellar_account_id.0) - } -} diff --git a/kernel/src/entity/stellar_account/refresh_token.rs b/kernel/src/entity/stellar_account/refresh_token.rs deleted file mode 100644 index 61ab683..0000000 --- a/kernel/src/entity/stellar_account/refresh_token.rs +++ /dev/null @@ -1,5 +0,0 @@ -use serde::{Deserialize, Serialize}; -use vodca::{AsRefln, Fromln, Newln}; - -#[derive(Debug, Clone, PartialEq, Eq, Hash, Fromln, AsRefln, Newln, Serialize, Deserialize)] -pub struct StellarAccountRefreshToken(String); diff --git a/kernel/src/modify.rs b/kernel/src/modify.rs index 8893e6f..a79e3a1 100644 --- a/kernel/src/modify.rs +++ b/kernel/src/modify.rs @@ -1,14 +1,14 @@ mod account; +mod auth_account; +mod auth_host; mod event; mod follow; mod image; mod metadata; mod profile; mod remote_account; -mod stellar_account; -mod stellar_host; pub use self::{ - account::*, event::*, follow::*, image::*, metadata::*, profile::*, remote_account::*, - stellar_account::*, stellar_host::*, + account::*, auth_account::*, auth_host::*, event::*, follow::*, image::*, metadata::*, + profile::*, remote_account::*, }; diff --git a/kernel/src/modify/stellar_account.rs b/kernel/src/modify/auth_account.rs similarity index 61% rename from kernel/src/modify/stellar_account.rs rename to kernel/src/modify/auth_account.rs index 912c741..7242853 100644 --- a/kernel/src/modify/stellar_account.rs +++ b/kernel/src/modify/auth_account.rs @@ -1,34 +1,34 @@ use crate::database::{DependOnDatabaseConnection, Transaction}; -use crate::entity::{StellarAccount, StellarAccountId}; +use crate::entity::{AuthAccount, AuthAccountId}; use crate::KernelError; use std::future::Future; -pub trait StellarAccountModifier: Sync + Send + 'static { +pub trait AuthAccountModifier: Sync + Send + 'static { type Transaction: Transaction; fn create( &self, transaction: &mut Self::Transaction, - stellar_account: &StellarAccount, + auth_account: &AuthAccount, ) -> impl Future> + Send; fn update( &self, transaction: &mut Self::Transaction, - stellar_account: &StellarAccount, + auth_account: &AuthAccount, ) -> impl Future> + Send; fn delete( &self, transaction: &mut Self::Transaction, - account_id: &StellarAccountId, + account_id: &AuthAccountId, ) -> impl Future> + Send; } -pub trait DependOnStellarAccountModifier: Sync + Send + DependOnDatabaseConnection { - type StellarAccountModifier: StellarAccountModifier< +pub trait DependOnAuthAccountModifier: Sync + Send + DependOnDatabaseConnection { + type AuthAccountModifier: AuthAccountModifier< Transaction = ::Transaction, >; - fn stellar_account_modifier(&self) -> &Self::StellarAccountModifier; + fn auth_account_modifier(&self) -> &Self::AuthAccountModifier; } diff --git a/kernel/src/modify/stellar_host.rs b/kernel/src/modify/auth_host.rs similarity index 61% rename from kernel/src/modify/stellar_host.rs rename to kernel/src/modify/auth_host.rs index 8dffeba..0d7fa99 100644 --- a/kernel/src/modify/stellar_host.rs +++ b/kernel/src/modify/auth_host.rs @@ -1,28 +1,28 @@ use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Transaction}; -use crate::entity::StellarHost; +use crate::entity::AuthHost; use crate::KernelError; use std::future::Future; -pub trait StellarHostModifier: Sync + Send + 'static { +pub trait AuthHostModifier: Sync + Send + 'static { type Transaction: Transaction; fn create( &self, transaction: &mut Self::Transaction, - stellar_host: &StellarHost, + auth_host: &AuthHost, ) -> impl Future> + Send; fn update( &self, transaction: &mut Self::Transaction, - stellar_host: &StellarHost, + auth_host: &AuthHost, ) -> impl Future> + Send; } -pub trait DependOnStellarHostModifier: Sync + Send + DependOnDatabaseConnection { - type StellarHostModifier: StellarHostModifier< +pub trait DependOnAuthHostModifier: Sync + Send + DependOnDatabaseConnection { + type AuthHostModifier: AuthHostModifier< Transaction = ::Transaction, >; - fn stellar_host_modifier(&self) -> &Self::StellarHostModifier; + fn auth_host_modifier(&self) -> &Self::AuthHostModifier; } diff --git a/kernel/src/query.rs b/kernel/src/query.rs index 8893e6f..a79e3a1 100644 --- a/kernel/src/query.rs +++ b/kernel/src/query.rs @@ -1,14 +1,14 @@ mod account; +mod auth_account; +mod auth_host; mod event; mod follow; mod image; mod metadata; mod profile; mod remote_account; -mod stellar_account; -mod stellar_host; pub use self::{ - account::*, event::*, follow::*, image::*, metadata::*, profile::*, remote_account::*, - stellar_account::*, stellar_host::*, + account::*, auth_account::*, auth_host::*, event::*, follow::*, image::*, metadata::*, + profile::*, remote_account::*, }; diff --git a/kernel/src/query/account.rs b/kernel/src/query/account.rs index a69d39a..3ec4682 100644 --- a/kernel/src/query/account.rs +++ b/kernel/src/query/account.rs @@ -1,5 +1,5 @@ use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Transaction}; -use crate::entity::{Account, AccountId, AccountName, StellarAccountId}; +use crate::entity::{Account, AccountId, AccountName, AuthAccountId}; use crate::KernelError; use std::future::Future; @@ -12,10 +12,10 @@ pub trait AccountQuery: Sync + Send + 'static { id: &AccountId, ) -> impl Future, KernelError>> + Send; - fn find_by_stellar_id( + fn find_by_auth_id( &self, transaction: &mut Self::Transaction, - stellar_id: &StellarAccountId, + auth_id: &AuthAccountId, ) -> impl Future, KernelError>> + Send; fn find_by_name( diff --git a/kernel/src/query/auth_account.rs b/kernel/src/query/auth_account.rs new file mode 100644 index 0000000..8ad25d8 --- /dev/null +++ b/kernel/src/query/auth_account.rs @@ -0,0 +1,22 @@ +use crate::database::{DependOnDatabaseConnection, Transaction}; +use crate::entity::{AuthAccount, AuthAccountId}; +use crate::KernelError; +use std::future::Future; + +pub trait AuthAccountQuery: Sync + Send + 'static { + type Transaction: Transaction; + + fn find_by_id( + &self, + transaction: &mut Self::Transaction, + account_id: &AuthAccountId, + ) -> impl Future, KernelError>> + Send; +} + +pub trait DependOnAuthAccountQuery: Sync + Send + DependOnDatabaseConnection { + type AuthAccountQuery: AuthAccountQuery< + Transaction = ::Transaction, + >; + + fn auth_account_query(&self) -> &Self::AuthAccountQuery; +} diff --git a/kernel/src/query/auth_host.rs b/kernel/src/query/auth_host.rs new file mode 100644 index 0000000..8f7ed19 --- /dev/null +++ b/kernel/src/query/auth_host.rs @@ -0,0 +1,28 @@ +use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Transaction}; +use crate::entity::{AuthHost, AuthHostId, AuthHostUrl}; +use crate::KernelError; +use std::future::Future; + +pub trait AuthHostQuery: Sync + Send + 'static { + type Transaction: Transaction; + + fn find_by_id( + &self, + transaction: &mut Self::Transaction, + id: &AuthHostId, + ) -> impl Future, KernelError>> + Send; + + fn find_by_url( + &self, + transaction: &mut Self::Transaction, + domain: &AuthHostUrl, + ) -> impl Future, KernelError>> + Send; +} + +pub trait DependOnAuthHostQuery: Sync + Send + DependOnDatabaseConnection { + type AuthHostQuery: AuthHostQuery< + Transaction = ::Transaction, + >; + + fn auth_host_query(&self) -> &Self::AuthHostQuery; +} diff --git a/kernel/src/query/stellar_account.rs b/kernel/src/query/stellar_account.rs deleted file mode 100644 index 4d91ee3..0000000 --- a/kernel/src/query/stellar_account.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::database::{DependOnDatabaseConnection, Transaction}; -use crate::entity::{StellarAccount, StellarAccountId}; -use crate::KernelError; -use std::future::Future; - -pub trait StellarAccountQuery: Sync + Send + 'static { - type Transaction: Transaction; - - fn find_by_id( - &self, - transaction: &mut Self::Transaction, - account_id: &StellarAccountId, - ) -> impl Future, KernelError>> + Send; -} - -pub trait DependOnStellarAccountQuery: Sync + Send + DependOnDatabaseConnection { - type StellarAccountQuery: StellarAccountQuery< - Transaction = ::Transaction, - >; - - fn stellar_account_query(&self) -> &Self::StellarAccountQuery; -} diff --git a/kernel/src/query/stellar_host.rs b/kernel/src/query/stellar_host.rs deleted file mode 100644 index d8692fa..0000000 --- a/kernel/src/query/stellar_host.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Transaction}; -use crate::entity::{StellarHost, StellarHostId, StellarHostUrl}; -use crate::KernelError; -use std::future::Future; - -pub trait StellarHostQuery: Sync + Send + 'static { - type Transaction: Transaction; - - fn find_by_id( - &self, - transaction: &mut Self::Transaction, - id: &StellarHostId, - ) -> impl Future, KernelError>> + Send; - - fn find_by_url( - &self, - transaction: &mut Self::Transaction, - domain: &StellarHostUrl, - ) -> impl Future, KernelError>> + Send; -} - -pub trait DependOnStellarHostQuery: Sync + Send + DependOnDatabaseConnection { - type StellarHostQuery: StellarHostQuery< - Transaction = ::Transaction, - >; - - fn stellar_host_query(&self) -> &Self::StellarHostQuery; -} diff --git a/migrations/20230707210300_init.dbml b/migrations/20230707210300_init.dbml index 6fbdf25..a1200e0 100644 --- a/migrations/20230707210300_init.dbml +++ b/migrations/20230707210300_init.dbml @@ -53,31 +53,29 @@ Table metadatas { Ref: metadatas.account_id > accounts.id [delete: cascade] -Table stellar_hosts { +Table auth_hosts { id UUID [not null, pk] url TEXT [not null, unique] } -Table stellar_accounts { +Table auth_accounts { id UUID [not null, pk] host_id UUID [not null] client_id TEXT [not null] - access_token TEXT [not null] - refresh_token TEXT [not null] version UUID [not null] } -Ref: stellar_accounts.host_id > stellar_hosts.id [delete: cascade] +Ref: auth_accounts.host_id > auth_hosts.id [delete: cascade] -Table stellar_emumet_accounts { +Table auth_emumet_accounts { emumet_id UUID [not null] - stellar_id UUID [not null, ref: > stellar_accounts.id] + auth_id UUID [not null, ref: > auth_accounts.id] Indexes { - (emumet_id, stellar_id) [pk] + (emumet_id, auth_id) [pk] } } -Ref: stellar_emumet_accounts.emumet_id > accounts.id [delete: cascade] +Ref: auth_emumet_accounts.emumet_id > accounts.id [delete: cascade] Table follows { id UUID [not null, pk] @@ -107,14 +105,14 @@ Table images { // } // // Table moderators { -// stellar_id UUID [not null, pk, ref: > stellar_accounts.id] +// auth_id UUID [not null, pk, ref: > auth_accounts.id] // role_id UUID [not null, pk, ref: > moderator_roles.id] // } // // Table account_reports { // id UUID [not null, pk] // target_id UUID [not null, ref: > accounts.id] -// reported_id UUID [not null, ref: > stellar_accounts.id] +// reported_id UUID [not null, ref: > auth_accounts.id] // type TEXT [not null] // comment TEXT [not null] // created_at TIMESTAMPTZ [not null] @@ -125,7 +123,7 @@ Table images { // Table account_moderations { // id UUID [not null, pk] // target_id UUID [not null, ref: > accounts.id] -// moderator_id UUID [not null, ref: > stellar_accounts.id] +// moderator_id UUID [not null, ref: > auth_accounts.id] // type TEXT [not null] // comment TEXT [not null] // created_at TIMESTAMPTZ [not null] @@ -139,17 +137,17 @@ Table images { // } // } // -// Table stellar_account_moderations { +// Table auth_account_moderations { // id UUID [not null, pk] -// target_id UUID [not null, ref: > stellar_accounts.id] -// moderator_id UUID [not null, ref: > stellar_accounts.id] +// target_id UUID [not null, ref: > auth_accounts.id] +// moderator_id UUID [not null, ref: > auth_accounts.id] // type TEXT [not null] // comment TEXT [not null] // created_at TIMESTAMPTZ [not null] // closed_at TIMESTAMPTZ // } -// Table stellar_account_moderation_reports { -// moderation_id UUID [not null, ref: > stellar_account_moderations.id] +// Table auth_account_moderation_reports { +// moderation_id UUID [not null, ref: > auth_account_moderations.id] // report_id UUID [not null, ref: > account_reports.id] // Indexes { // (moderation_id, report_id) [pk] @@ -158,8 +156,8 @@ Table images { // // Table host_moderations { // id UUID [not null, pk] -// host_id UUID [not null, ref: > stellar_hosts.id] -// moderator_id UUID [not null, ref: > stellar_accounts.id] +// host_id UUID [not null, ref: > auth_hosts.id] +// moderator_id UUID [not null, ref: > auth_accounts.id] // type TEXT [not null] // comment TEXT [not null] // created_at TIMESTAMPTZ [not null] diff --git a/migrations/20230707210300_init.sql b/migrations/20230707210300_init.sql index f5256dd..cc11775 100644 --- a/migrations/20230707210300_init.sql +++ b/migrations/20230707210300_init.sql @@ -1,6 +1,6 @@ -- SQL dump generated using DBML (dbml.dbdiagram.io) -- Database: PostgreSQL --- Generated at: 2024-10-24T12:48:43.363Z +-- Generated at: 2025-01-25T03:59:23.453Z CREATE TABLE "event_streams" ( "version" UUID NOT NULL, @@ -48,24 +48,22 @@ CREATE TABLE "metadatas" ( "nanoid" TEXT UNIQUE NOT NULL ); -CREATE TABLE "stellar_hosts" ( +CREATE TABLE "auth_hosts" ( "id" UUID PRIMARY KEY NOT NULL, "url" TEXT UNIQUE NOT NULL ); -CREATE TABLE "stellar_accounts" ( +CREATE TABLE "auth_accounts" ( "id" UUID PRIMARY KEY NOT NULL, "host_id" UUID NOT NULL, "client_id" TEXT NOT NULL, - "access_token" TEXT NOT NULL, - "refresh_token" TEXT NOT NULL, "version" UUID NOT NULL ); -CREATE TABLE "stellar_emumet_accounts" ( +CREATE TABLE "auth_emumet_accounts" ( "emumet_id" UUID NOT NULL, - "stellar_id" UUID NOT NULL, - PRIMARY KEY ("emumet_id", "stellar_id") + "auth_id" UUID NOT NULL, + PRIMARY KEY ("emumet_id", "auth_id") ); CREATE TABLE "follows" ( @@ -94,11 +92,11 @@ ALTER TABLE "profiles" ADD FOREIGN KEY ("banner_id") REFERENCES "images" ("id") ALTER TABLE "metadatas" ADD FOREIGN KEY ("account_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; -ALTER TABLE "stellar_accounts" ADD FOREIGN KEY ("host_id") REFERENCES "stellar_hosts" ("id") ON DELETE CASCADE; +ALTER TABLE "auth_accounts" ADD FOREIGN KEY ("host_id") REFERENCES "auth_hosts" ("id") ON DELETE CASCADE; -ALTER TABLE "stellar_emumet_accounts" ADD FOREIGN KEY ("stellar_id") REFERENCES "stellar_accounts" ("id"); +ALTER TABLE "auth_emumet_accounts" ADD FOREIGN KEY ("auth_id") REFERENCES "auth_accounts" ("id"); -ALTER TABLE "stellar_emumet_accounts" ADD FOREIGN KEY ("emumet_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; +ALTER TABLE "auth_emumet_accounts" ADD FOREIGN KEY ("emumet_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; ALTER TABLE "follows" ADD FOREIGN KEY ("follower_local_id") REFERENCES "accounts" ("id") ON DELETE CASCADE; diff --git a/migrations/gensql.sh b/migrations/gensql.sh index 7d7cb79..cd464f7 100755 --- a/migrations/gensql.sh +++ b/migrations/gensql.sh @@ -1,3 +1,3 @@ #!/usr/bin/env sh -curl -o 20230707210300_init.dbml https://raw.githubusercontent.com/ShuttlePub/document/afd96a4bf572786e336ed43ec7c8ad5cc0696bad/packages/document/dbml/emumet.dbml +curl -o 20230707210300_init.dbml https://raw.githubusercontent.com/ShuttlePub/document/756e913e1eb03859118386681dace01001e74586/packages/document/dbml/emumet.dbml pnpm --package=@dbml/cli dlx dbml2sql 20230707210300_init.dbml -o 20230707210300_init.sql \ No newline at end of file From ed37f2cbc0e7485f6086f8d07989b3f51a4db273 Mon Sep 17 00:00:00 2001 From: turtton Date: Sat, 25 Jan 2025 17:24:05 +0900 Subject: [PATCH 09/59] :hammer: Add some functions --- driver/src/database/postgres/account.rs | 21 ++++++++++++++++++++ driver/src/database/postgres/auth_account.rs | 21 ++++++++++++++++++++ kernel/src/entity/account.rs | 12 +++++++++++ kernel/src/entity/account/id.rs | 15 +++++++++++++- kernel/src/query/account.rs | 8 +++++++- kernel/src/query/auth_account.rs | 8 +++++++- 6 files changed, 82 insertions(+), 3 deletions(-) diff --git a/driver/src/database/postgres/account.rs b/driver/src/database/postgres/account.rs index 5ce5744..44cd649 100644 --- a/driver/src/database/postgres/account.rs +++ b/driver/src/database/postgres/account.rs @@ -106,6 +106,27 @@ impl AccountQuery for PostgresAccountRepository { .convert_error() .map(|option| option.map(Account::from)) } + + async fn find_by_nanoid( + &self, + transaction: &mut Self::Transaction, + nanoid: &Nanoid, + ) -> error_stack::Result, KernelError> { + let con: &mut PgConnection = transaction; + sqlx::query_as::<_, AccountRow>( + //language=postgresql + r#" + SELECT id, name, private_key, public_key, is_bot, deleted_at, version, nanoid + FROM accounts + WHERE nanoid = $1 AND deleted_at IS NULL + "#, + ) + .bind(nanoid.as_ref()) + .fetch_optional(con) + .await + .convert_error() + .map(|option| option.map(Account::from)) + } } impl DependOnAccountQuery for PostgresDatabase { diff --git a/driver/src/database/postgres/auth_account.rs b/driver/src/database/postgres/auth_account.rs index 7e692ae..22df7fe 100644 --- a/driver/src/database/postgres/auth_account.rs +++ b/driver/src/database/postgres/auth_account.rs @@ -53,6 +53,27 @@ impl AuthAccountQuery for PostgresAuthAccountRepository { .convert_error() .map(|option| option.map(|row| row.into())) } + + async fn find_by_client_id( + &self, + transaction: &mut Self::Transaction, + client_id: &AuthAccountClientId, + ) -> error_stack::Result, KernelError> { + let con: &mut PgConnection = transaction; + sqlx::query_as::<_, AuthAccountRow>( + //language=postgresql + r#" + SELECT id, host_id, client_id, version + FROM auth_accounts + WHERE client_id = $1 + "#, + ) + .bind(client_id.as_ref()) + .fetch_optional(con) + .await + .convert_error() + .map(|option| option.map(|row| row.into())) + } } impl DependOnAuthAccountQuery for PostgresDatabase { diff --git a/kernel/src/entity/account.rs b/kernel/src/entity/account.rs index c4b3438..2cd67ec 100644 --- a/kernel/src/entity/account.rs +++ b/kernel/src/entity/account.rs @@ -88,6 +88,18 @@ impl Account { } } +impl PartialOrd for Account { + fn partial_cmp(&self, other: &Self) -> Option { + Some(self.cmp(other)) + } +} + +impl Ord for Account { + fn cmp(&self, other: &Self) -> std::cmp::Ordering { + self.id.cmp(&other.id) + } +} + impl EventApplier for Account { type Event = AccountEvent; const ENTITY_NAME: &'static str = "Account"; diff --git a/kernel/src/entity/account/id.rs b/kernel/src/entity/account/id.rs index 85cd4f4..ed87d86 100644 --- a/kernel/src/entity/account/id.rs +++ b/kernel/src/entity/account/id.rs @@ -3,7 +3,20 @@ use serde::{Deserialize, Serialize}; use uuid::Uuid; use vodca::{AsRefln, Fromln, Newln}; -#[derive(Debug, Clone, PartialEq, Eq, Hash, Fromln, AsRefln, Newln, Serialize, Deserialize)] +#[derive( + Debug, + Clone, + PartialEq, + Eq, + Hash, + Ord, + PartialOrd, + Fromln, + AsRefln, + Newln, + Serialize, + Deserialize, +)] pub struct AccountId(Uuid); impl From for EventId { diff --git a/kernel/src/query/account.rs b/kernel/src/query/account.rs index 3ec4682..bd5decf 100644 --- a/kernel/src/query/account.rs +++ b/kernel/src/query/account.rs @@ -1,5 +1,5 @@ use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Transaction}; -use crate::entity::{Account, AccountId, AccountName, AuthAccountId}; +use crate::entity::{Account, AccountId, AccountName, AuthAccountId, Nanoid}; use crate::KernelError; use std::future::Future; @@ -23,6 +23,12 @@ pub trait AccountQuery: Sync + Send + 'static { transaction: &mut Self::Transaction, name: &AccountName, ) -> impl Future, KernelError>> + Send; + + fn find_by_nanoid( + &self, + transaction: &mut Self::Transaction, + nanoid: &Nanoid, + ) -> impl Future, KernelError>> + Send; } pub trait DependOnAccountQuery: Sync + Send + DependOnDatabaseConnection { diff --git a/kernel/src/query/auth_account.rs b/kernel/src/query/auth_account.rs index 8ad25d8..9cc594d 100644 --- a/kernel/src/query/auth_account.rs +++ b/kernel/src/query/auth_account.rs @@ -1,5 +1,5 @@ use crate::database::{DependOnDatabaseConnection, Transaction}; -use crate::entity::{AuthAccount, AuthAccountId}; +use crate::entity::{AuthAccount, AuthAccountClientId, AuthAccountId}; use crate::KernelError; use std::future::Future; @@ -11,6 +11,12 @@ pub trait AuthAccountQuery: Sync + Send + 'static { transaction: &mut Self::Transaction, account_id: &AuthAccountId, ) -> impl Future, KernelError>> + Send; + + fn find_by_client_id( + &self, + transaction: &mut Self::Transaction, + client_id: &AuthAccountClientId, + ) -> impl Future, KernelError>> + Send; } pub trait DependOnAuthAccountQuery: Sync + Send + DependOnDatabaseConnection { From 13ed900f0400f1486e8bcbade71d859cfc7c0196 Mon Sep 17 00:00:00 2001 From: turtton Date: Sat, 25 Jan 2025 19:15:01 +0900 Subject: [PATCH 10/59] :hammer: Add created_at field in Account --- driver/src/database/postgres/account.rs | 66 ++++++++++++++++++++---- driver/src/database/postgres/follow.rs | 14 ++++- driver/src/database/postgres/metadata.rs | 9 +++- driver/src/database/postgres/profile.rs | 9 +++- kernel/src/entity/account.rs | 28 +++++++--- kernel/src/entity/common/created_at.rs | 17 ++++++ kernel/src/error.rs | 2 +- migrations/20230707210300_init.dbml | 1 + migrations/20230707210300_init.sql | 5 +- migrations/gensql.sh | 2 +- 10 files changed, 125 insertions(+), 28 deletions(-) diff --git a/driver/src/database/postgres/account.rs b/driver/src/database/postgres/account.rs index 44cd649..6c3ffed 100644 --- a/driver/src/database/postgres/account.rs +++ b/driver/src/database/postgres/account.rs @@ -4,7 +4,7 @@ use kernel::interfaces::modify::{AccountModifier, DependOnAccountModifier}; use kernel::interfaces::query::{AccountQuery, DependOnAccountQuery}; use kernel::prelude::entity::{ Account, AccountId, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, - AuthAccountId, DeletedAt, EventVersion, Nanoid, + AuthAccountId, CreatedAt, DeletedAt, EventVersion, Nanoid, }; use kernel::KernelError; use sqlx::types::time::OffsetDateTime; @@ -21,6 +21,7 @@ struct AccountRow { deleted_at: Option, version: Uuid, nanoid: String, + created_at: OffsetDateTime, } impl From for Account { @@ -34,6 +35,7 @@ impl From for Account { value.deleted_at.map(DeletedAt::new), EventVersion::new(value.version), Nanoid::new(value.nanoid), + CreatedAt::new(value.created_at), ) } } @@ -52,7 +54,7 @@ impl AccountQuery for PostgresAccountRepository { sqlx::query_as::<_, AccountRow>( //language=postgresql r#" - SELECT id, name, private_key, public_key, is_bot, deleted_at, version, nanoid + SELECT id, name, private_key, public_key, is_bot, deleted_at, version, nanoid, created_at FROM accounts WHERE id = $1 AND deleted_at IS NULL "#, @@ -73,7 +75,7 @@ impl AccountQuery for PostgresAccountRepository { sqlx::query_as::<_, AccountRow>( //language=postgresql r#" - SELECT id, name, private_key, public_key, is_bot, deleted_at, version, nanoid + SELECT id, name, private_key, public_key, is_bot, deleted_at, version, nanoid, created_at FROM accounts INNER JOIN auth_emumet_accounts ON auth_emumet_accounts.emumet_id = accounts.id WHERE auth_emumet_accounts.auth_id = $1 AND deleted_at IS NULL @@ -95,7 +97,7 @@ impl AccountQuery for PostgresAccountRepository { sqlx::query_as::<_, AccountRow>( //language=postgresql r#" - SELECT id, name, private_key, public_key, is_bot, deleted_at, version, nanoid + SELECT id, name, private_key, public_key, is_bot, deleted_at, version, nanoid, created_at FROM accounts WHERE name = $1 AND deleted_at IS NULL "#, @@ -116,7 +118,7 @@ impl AccountQuery for PostgresAccountRepository { sqlx::query_as::<_, AccountRow>( //language=postgresql r#" - SELECT id, name, private_key, public_key, is_bot, deleted_at, version, nanoid + SELECT id, name, private_key, public_key, is_bot, deleted_at, version, nanoid, created_at FROM accounts WHERE nanoid = $1 AND deleted_at IS NULL "#, @@ -149,8 +151,8 @@ impl AccountModifier for PostgresAccountRepository { sqlx::query( //language=postgresql r#" - INSERT INTO accounts (id, name, private_key, public_key, is_bot, version, nanoid) - VALUES ($1, $2, $3, $4, $5, $6, $7) + INSERT INTO accounts (id, name, private_key, public_key, is_bot, version, nanoid, created_at) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8) "#, ) .bind(account.id().as_ref()) @@ -160,6 +162,7 @@ impl AccountModifier for PostgresAccountRepository { .bind(account.is_bot().as_ref()) .bind(account.version().as_ref()) .bind(account.nanoid().as_ref()) + .bind(account.created_at().as_ref()) .execute(con) .await .convert_error()?; @@ -231,7 +234,7 @@ mod test { use kernel::interfaces::query::{AccountQuery, DependOnAccountQuery}; use kernel::prelude::entity::{ Account, AccountId, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, - AuthAccountId, EventVersion, Nanoid, + AuthAccountId, CreatedAt, EventVersion, Nanoid, }; use sqlx::types::Uuid; @@ -250,6 +253,7 @@ mod test { None, EventVersion::new(Uuid::now_v7()), Nanoid::default(), + CreatedAt::now(), ); database .account_modifier() @@ -282,7 +286,7 @@ mod test { let database = PostgresDatabase::new().await.unwrap(); let mut transaction = database.begin_transaction().await.unwrap(); - let name = AccountName::new("findbynametest"); + let name = AccountName::new(Uuid::now_v7().to_string()); let account = Account::new( AccountId::new(Uuid::now_v7()), name.clone(), @@ -292,6 +296,7 @@ mod test { None, EventVersion::new(Uuid::now_v7()), Nanoid::default(), + CreatedAt::now(), ); database .account_modifier() @@ -311,6 +316,42 @@ mod test { .await .unwrap(); } + + #[tokio::test] + async fn find_by_nanoid() { + let database = PostgresDatabase::new().await.unwrap(); + let mut transaction = database.begin_transaction().await.unwrap(); + + let nanoid = Nanoid::default(); + let account = Account::new( + AccountId::new(Uuid::now_v7()), + AccountName::new("test"), + AccountPrivateKey::new("test"), + AccountPublicKey::new("test"), + AccountIsBot::new(false), + None, + EventVersion::new(Uuid::now_v7()), + nanoid.clone(), + CreatedAt::now(), + ); + database + .account_modifier() + .create(&mut transaction, &account) + .await + .unwrap(); + + let result = database + .account_query() + .find_by_nanoid(&mut transaction, &nanoid) + .await + .unwrap(); + assert_eq!(result.as_ref().map(Account::id), Some(account.id())); + database + .account_modifier() + .delete(&mut transaction, account.id()) + .await + .unwrap(); + } } mod modify { @@ -320,7 +361,7 @@ mod test { use kernel::interfaces::query::{AccountQuery, DependOnAccountQuery}; use kernel::prelude::entity::{ Account, AccountId, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, - DeletedAt, EventVersion, Nanoid, + CreatedAt, DeletedAt, EventVersion, Nanoid, }; use sqlx::types::time::OffsetDateTime; use sqlx::types::Uuid; @@ -339,6 +380,7 @@ mod test { None, EventVersion::new(Uuid::now_v7()), Nanoid::default(), + CreatedAt::now(), ); database .account_modifier() @@ -368,6 +410,7 @@ mod test { None, EventVersion::new(Uuid::now_v7()), Nanoid::default(), + CreatedAt::now(), ); database .account_modifier() @@ -383,6 +426,7 @@ mod test { None, EventVersion::new(Uuid::now_v7()), Nanoid::default(), + CreatedAt::now(), ); database .account_modifier() @@ -411,6 +455,7 @@ mod test { None, EventVersion::new(Uuid::now_v7()), Nanoid::default(), + CreatedAt::now(), ); database .account_modifier() @@ -440,6 +485,7 @@ mod test { Some(DeletedAt::new(OffsetDateTime::now_utc())), EventVersion::new(Uuid::now_v7()), Nanoid::default(), + CreatedAt::now(), ); database .account_modifier() diff --git a/driver/src/database/postgres/follow.rs b/driver/src/database/postgres/follow.rs index 8c90e87..43779b8 100644 --- a/driver/src/database/postgres/follow.rs +++ b/driver/src/database/postgres/follow.rs @@ -242,7 +242,7 @@ mod test { use kernel::interfaces::query::{DependOnFollowQuery, FollowQuery}; use kernel::prelude::entity::{ Account, AccountId, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, - EventVersion, Follow, FollowApprovedAt, FollowId, FollowTargetId, Nanoid, + CreatedAt, EventVersion, Follow, FollowApprovedAt, FollowId, FollowTargetId, Nanoid, }; use uuid::Uuid; @@ -260,6 +260,7 @@ mod test { None, EventVersion::new(Uuid::now_v7()), Nanoid::default(), + CreatedAt::now(), ); database .account_modifier() @@ -276,6 +277,7 @@ mod test { None, EventVersion::new(Uuid::now_v7()), Nanoid::default(), + CreatedAt::now(), ); database .account_modifier() @@ -340,6 +342,7 @@ mod test { None, EventVersion::new(Uuid::now_v7()), Nanoid::default(), + CreatedAt::now(), ); database .account_modifier() @@ -356,6 +359,7 @@ mod test { None, EventVersion::new(Uuid::now_v7()), Nanoid::default(), + CreatedAt::now(), ); database .account_modifier() @@ -416,7 +420,7 @@ mod test { use kernel::interfaces::query::{DependOnFollowQuery, FollowQuery}; use kernel::prelude::entity::{ Account, AccountId, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, - EventVersion, Follow, FollowApprovedAt, FollowId, FollowTargetId, Nanoid, + CreatedAt, EventVersion, Follow, FollowApprovedAt, FollowId, FollowTargetId, Nanoid, }; use uuid::Uuid; @@ -434,6 +438,7 @@ mod test { None, EventVersion::new(Uuid::now_v7()), Nanoid::default(), + CreatedAt::now(), ); database .account_modifier() @@ -450,6 +455,7 @@ mod test { None, EventVersion::new(Uuid::now_v7()), Nanoid::default(), + CreatedAt::now(), ); database .account_modifier() @@ -501,6 +507,7 @@ mod test { None, EventVersion::new(Uuid::now_v7()), Nanoid::default(), + CreatedAt::now(), ); database .account_modifier() @@ -517,6 +524,7 @@ mod test { None, EventVersion::new(Uuid::now_v7()), Nanoid::default(), + CreatedAt::now(), ); database .account_modifier() @@ -594,6 +602,7 @@ mod test { None, EventVersion::new(Uuid::now_v7()), Nanoid::default(), + CreatedAt::now(), ); database .account_modifier() @@ -610,6 +619,7 @@ mod test { None, EventVersion::new(Uuid::now_v7()), Nanoid::default(), + CreatedAt::now(), ); database .account_modifier() diff --git a/driver/src/database/postgres/metadata.rs b/driver/src/database/postgres/metadata.rs index fed3f07..a20ea93 100644 --- a/driver/src/database/postgres/metadata.rs +++ b/driver/src/database/postgres/metadata.rs @@ -181,7 +181,7 @@ mod test { use kernel::interfaces::query::{DependOnMetadataQuery, MetadataQuery}; use kernel::prelude::entity::{ Account, AccountId, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, - EventVersion, Metadata, MetadataContent, MetadataId, MetadataLabel, Nanoid, + CreatedAt, EventVersion, Metadata, MetadataContent, MetadataId, MetadataLabel, Nanoid, }; use uuid::Uuid; @@ -201,6 +201,7 @@ mod test { None, EventVersion::new(Uuid::now_v7()), Nanoid::default(), + CreatedAt::now(), ); database @@ -246,6 +247,7 @@ mod test { None, EventVersion::new(Uuid::now_v7()), Nanoid::default(), + CreatedAt::now(), ); database @@ -301,7 +303,7 @@ mod test { use kernel::interfaces::query::{DependOnMetadataQuery, MetadataQuery}; use kernel::prelude::entity::{ Account, AccountId, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, - EventVersion, Metadata, MetadataContent, MetadataId, MetadataLabel, Nanoid, + CreatedAt, EventVersion, Metadata, MetadataContent, MetadataId, MetadataLabel, Nanoid, }; use uuid::Uuid; @@ -320,6 +322,7 @@ mod test { None, EventVersion::new(Uuid::now_v7()), Nanoid::default(), + CreatedAt::now(), ); database @@ -365,6 +368,7 @@ mod test { None, EventVersion::new(Uuid::now_v7()), Nanoid::default(), + CreatedAt::now(), ); database @@ -428,6 +432,7 @@ mod test { None, EventVersion::new(Uuid::now_v7()), Nanoid::default(), + CreatedAt::now(), ); database diff --git a/driver/src/database/postgres/profile.rs b/driver/src/database/postgres/profile.rs index a59ed0f..eef3900 100644 --- a/driver/src/database/postgres/profile.rs +++ b/driver/src/database/postgres/profile.rs @@ -154,7 +154,8 @@ mod test { use kernel::interfaces::query::{DependOnProfileQuery, ProfileQuery}; use kernel::prelude::entity::{ Account, AccountId, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, - EventVersion, Nanoid, Profile, ProfileDisplayName, ProfileId, ProfileSummary, + CreatedAt, EventVersion, Nanoid, Profile, ProfileDisplayName, ProfileId, + ProfileSummary, }; use crate::database::PostgresDatabase; @@ -175,6 +176,7 @@ mod test { None, EventVersion::new(Uuid::now_v7()), Nanoid::default(), + CreatedAt::now(), ); let profile = Profile::new( profile_id.clone(), @@ -221,7 +223,8 @@ mod test { use kernel::interfaces::query::{DependOnProfileQuery, ProfileQuery}; use kernel::prelude::entity::{ Account, AccountId, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, - EventVersion, Nanoid, Profile, ProfileDisplayName, ProfileId, ProfileSummary, + CreatedAt, EventVersion, Nanoid, Profile, ProfileDisplayName, ProfileId, + ProfileSummary, }; use crate::database::PostgresDatabase; @@ -242,6 +245,7 @@ mod test { None, EventVersion::new(Uuid::now_v7()), Nanoid::default(), + CreatedAt::now(), ); let profile = Profile::new( profile_id, @@ -286,6 +290,7 @@ mod test { None, EventVersion::new(Uuid::now_v7()), Nanoid::default(), + CreatedAt::now(), ); let profile = Profile::new( profile_id.clone(), diff --git a/kernel/src/entity/account.rs b/kernel/src/entity/account.rs index 2cd67ec..dda13c6 100644 --- a/kernel/src/entity/account.rs +++ b/kernel/src/entity/account.rs @@ -5,7 +5,8 @@ use serde::Serialize; use vodca::{Nameln, Newln, References}; use crate::entity::{ - CommandEnvelope, DeletedAt, EventEnvelope, EventId, EventVersion, KnownEventVersion, Nanoid, + CommandEnvelope, CreatedAt, DeletedAt, EventEnvelope, EventId, EventVersion, KnownEventVersion, + Nanoid, }; use crate::event::EventApplier; use crate::KernelError; @@ -34,6 +35,7 @@ pub struct Account { deleted_at: Option>, version: EventVersion, nanoid: Nanoid, + created_at: CreatedAt, } #[derive(Debug, Clone, Eq, PartialEq, Nameln, Serialize, Deserialize)] @@ -120,6 +122,11 @@ impl EventApplier for Account { return Err(Report::new(KernelError::Internal) .attach_printable(Self::already_exists(entity))); } + let created_at = if let Some(timestamp) = event.id.as_ref().get_timestamp() { + CreatedAt::try_from(timestamp)? + } else { + CreatedAt::now() + }; *entity = Some(Account { id: AccountId::new(event.id), name, @@ -129,6 +136,7 @@ impl EventApplier for Account { deleted_at: None, version: event.version, nanoid: nano_id, + created_at, }); } AccountEvent::Updated { is_bot } => { @@ -158,9 +166,10 @@ impl EventApplier for Account { mod test { use crate::entity::{ Account, AccountId, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, - EventEnvelope, EventVersion, Nanoid, + CreatedAt, EventEnvelope, EventVersion, Nanoid, }; use crate::event::EventApplier; + use crate::KernelError; use uuid::Uuid; #[test] @@ -197,7 +206,6 @@ mod test { } #[test] - #[should_panic] fn create_exist_account() { let id = AccountId::new(Uuid::now_v7()); let name = AccountName::new("test"); @@ -214,6 +222,7 @@ mod test { None, EventVersion::new(Uuid::now_v7()), nano_id.clone(), + CreatedAt::now(), ); let event = Account::create( id.clone(), @@ -229,7 +238,8 @@ mod test { EventVersion::new(Uuid::now_v7()), ); let mut account = Some(account); - Account::apply(&mut account, envelope).unwrap(); + assert!(Account::apply(&mut account, envelope) + .is_err_and(|e| e.current_context() == &KernelError::Internal)); } #[test] @@ -249,6 +259,7 @@ mod test { None, EventVersion::new(Uuid::now_v7()), nano_id.clone(), + CreatedAt::now(), ); let event = Account::update(id.clone(), AccountIsBot::new(true)); let envelope = EventEnvelope::new( @@ -265,7 +276,6 @@ mod test { } #[test] - #[should_panic] fn update_not_exist_account() { let id = AccountId::new(Uuid::now_v7()); let event = Account::update(id.clone(), AccountIsBot::new(true)); @@ -275,7 +285,8 @@ mod test { EventVersion::new(Uuid::now_v7()), ); let mut account = None; - Account::apply(&mut account, envelope).unwrap(); + assert!(Account::apply(&mut account, envelope) + .is_err_and(|e| e.current_context() == &KernelError::Internal)); } #[test] @@ -295,6 +306,7 @@ mod test { None, EventVersion::new(Uuid::now_v7()), nano_id.clone(), + CreatedAt::now(), ); let event = Account::delete(id.clone()); let envelope = EventEnvelope::new( @@ -311,7 +323,6 @@ mod test { } #[test] - #[should_panic] fn delete_not_exist_account() { let id = AccountId::new(Uuid::now_v7()); let event = Account::delete(id.clone()); @@ -321,6 +332,7 @@ mod test { EventVersion::new(Uuid::now_v7()), ); let mut account = None; - Account::apply(&mut account, envelope).unwrap(); + assert!(Account::apply(&mut account, envelope) + .is_err_and(|e| e.current_context() == &KernelError::Internal)); } } diff --git a/kernel/src/entity/common/created_at.rs b/kernel/src/entity/common/created_at.rs index 8044f0d..e15dc06 100644 --- a/kernel/src/entity/common/created_at.rs +++ b/kernel/src/entity/common/created_at.rs @@ -1,6 +1,9 @@ +use crate::KernelError; +use error_stack::{Report, ResultExt}; use serde::{Deserialize, Deserializer, Serialize}; use std::marker::PhantomData; use time::OffsetDateTime; +use uuid::Timestamp; use vodca::{AsRefln, Fromln}; #[derive(Debug, Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Fromln, AsRefln)] @@ -17,6 +20,20 @@ impl CreatedAt { } } +impl TryFrom for CreatedAt { + type Error = Report; + fn try_from(value: Timestamp) -> Result { + let (seconds, nanos) = value.to_unix(); + let datetime = OffsetDateTime::from_unix_timestamp(seconds as i64) + .change_context_lazy(|| KernelError::Internal) + .attach_printable_lazy(|| format!("Invalid seconds: {seconds}"))? + .replace_nanosecond(nanos) + .change_context_lazy(|| KernelError::Internal) + .attach_printable_lazy(|| format!("Invalid nanos: {nanos}"))?; + Ok(Self::new(datetime)) + } +} + impl Serialize for CreatedAt { fn serialize(&self, serializer: S) -> Result where diff --git a/kernel/src/error.rs b/kernel/src/error.rs index 9b3591c..e0f40b9 100644 --- a/kernel/src/error.rs +++ b/kernel/src/error.rs @@ -1,7 +1,7 @@ use error_stack::Context; use std::fmt::{Display, Formatter}; -#[derive(Debug)] +#[derive(Debug, Eq, PartialEq)] pub enum KernelError { Concurrency, Timeout, diff --git a/migrations/20230707210300_init.dbml b/migrations/20230707210300_init.dbml index a1200e0..b91a4ff 100644 --- a/migrations/20230707210300_init.dbml +++ b/migrations/20230707210300_init.dbml @@ -16,6 +16,7 @@ Table accounts { deleted_at TIMESTAMPTZ version UUID [not null] nanoid TEXT [not null, unique] + created_at TIMESTAMPTZ [not null] } Table remote_accounts { diff --git a/migrations/20230707210300_init.sql b/migrations/20230707210300_init.sql index cc11775..8d3ec73 100644 --- a/migrations/20230707210300_init.sql +++ b/migrations/20230707210300_init.sql @@ -1,6 +1,6 @@ -- SQL dump generated using DBML (dbml.dbdiagram.io) -- Database: PostgreSQL --- Generated at: 2025-01-25T03:59:23.453Z +-- Generated at: 2025-01-25T10:07:34.134Z CREATE TABLE "event_streams" ( "version" UUID NOT NULL, @@ -18,7 +18,8 @@ CREATE TABLE "accounts" ( "is_bot" BOOLEAN NOT NULL, "deleted_at" TIMESTAMPTZ, "version" UUID NOT NULL, - "nanoid" TEXT UNIQUE NOT NULL + "nanoid" TEXT UNIQUE NOT NULL, + "created_at" TIMESTAMPTZ NOT NULL ); CREATE TABLE "remote_accounts" ( diff --git a/migrations/gensql.sh b/migrations/gensql.sh index cd464f7..d7adc28 100755 --- a/migrations/gensql.sh +++ b/migrations/gensql.sh @@ -1,3 +1,3 @@ #!/usr/bin/env sh -curl -o 20230707210300_init.dbml https://raw.githubusercontent.com/ShuttlePub/document/756e913e1eb03859118386681dace01001e74586/packages/document/dbml/emumet.dbml +curl -o 20230707210300_init.dbml https://raw.githubusercontent.com/ShuttlePub/document/6f36e9cd0eb1d2ec46f06eb9daab52c108e625fd/packages/document/dbml/emumet.dbml pnpm --package=@dbml/cli dlx dbml2sql 20230707210300_init.dbml -o 20230707210300_init.sql \ No newline at end of file From ee0056229920eac622c9526cee8303d5ed817731 Mon Sep 17 00:00:00 2001 From: turtton Date: Sat, 25 Jan 2025 19:15:23 +0900 Subject: [PATCH 11/59] :bug: Fix invalid param --- driver/src/database/postgres/auth_account.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/driver/src/database/postgres/auth_account.rs b/driver/src/database/postgres/auth_account.rs index 22df7fe..0bdcb8a 100644 --- a/driver/src/database/postgres/auth_account.rs +++ b/driver/src/database/postgres/auth_account.rs @@ -118,7 +118,7 @@ impl AuthAccountModifier for PostgresAuthAccountRepository { sqlx::query( //language=postgresql r#" - UPDATE auth_accounts SET host_id = $2, client_id = $3, version = $6 + UPDATE auth_accounts SET host_id = $2, client_id = $3, version = $4 WHERE id = $1 "#, ) From cb0b393af1bb9fb0fa14d3971f1b60b1feaa769e Mon Sep 17 00:00:00 2001 From: turtton Date: Sat, 25 Jan 2025 19:16:08 +0900 Subject: [PATCH 12/59] :hammer: Implement get_all_accounts --- application/Cargo.toml | 2 - application/src/service.rs | 28 +-------- application/src/service/account.rs | 46 +++++++++++--- application/src/service/auth_account.rs | 19 ++++++ application/src/transfer.rs | 1 + application/src/transfer/account.rs | 14 +++++ application/src/transfer/pagination.rs | 81 +++++++++++++++++++++++++ 7 files changed, 152 insertions(+), 39 deletions(-) create mode 100644 application/src/service/auth_account.rs create mode 100644 application/src/transfer/pagination.rs diff --git a/application/Cargo.toml b/application/Cargo.toml index a00e2b7..8f74d2c 100644 --- a/application/Cargo.toml +++ b/application/Cargo.toml @@ -7,11 +7,9 @@ authors.workspace = true # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -uuid = { workspace = true } time = { workspace = true } vodca = { workspace = true } -async-trait = "0.1" error-stack = { workspace = true } kernel = { path = "../kernel" } diff --git a/application/src/service.rs b/application/src/service.rs index 2f6d6ce..a3f9ff7 100644 --- a/application/src/service.rs +++ b/application/src/service.rs @@ -1,28 +1,2 @@ -use vodca::Nameln; - pub mod account; - -#[derive(Debug, Nameln)] -#[vodca(snake_case)] -pub enum Direction { - NEXT, - PREV, -} - -impl Default for Direction { - fn default() -> Self { - Self::NEXT - } -} - -impl TryFrom for Direction { - type Error = String; - - fn try_from(value: String) -> Result { - match value.as_str() { - "next" => Ok(Self::NEXT), - "prev" => Ok(Self::PREV), - other => Err(format!("Invalid direction: {}", other)), - } - } -} +pub mod auth_account; diff --git a/application/src/service/account.rs b/application/src/service/account.rs index b5f00a4..36378ec 100644 --- a/application/src/service/account.rs +++ b/application/src/service/account.rs @@ -1,21 +1,47 @@ -use crate::service::Direction; +use crate::service::auth_account::get_auth_account; use crate::transfer::account::AccountDto; -use kernel::interfaces::query::DependOnAccountQuery; +use crate::transfer::pagination::{apply_pagination, Pagination}; +use kernel::interfaces::database::DatabaseConnection; +use kernel::interfaces::query::{AccountQuery, DependOnAccountQuery, DependOnAuthAccountQuery}; +use kernel::prelude::entity::{Account, Nanoid}; use kernel::KernelError; use std::future::Future; -pub trait GetAccountService: 'static + Sync + Send + DependOnAccountQuery { +pub trait GetAccountService: + 'static + Sync + Send + DependOnAccountQuery + DependOnAuthAccountQuery +{ fn get_all_accounts( &self, - limit: Option, - cursor: Option, - direction: Option, //TODO error_stackやめてResponseに載せれる情報を返す + subject: String, + Pagination { + direction, + cursor, + limit, + }: Pagination, ) -> impl Future, KernelError>> { - async { - todo!("get auth id") - // self.account_query().find_by_auth_id(auth_id).await + async move { + let auth_account = if let Some(auth_account) = get_auth_account(self, subject).await? { + auth_account + } else { + return Ok(vec![]); + }; + let mut transaction = self.database_connection().begin_transaction().await?; + let accounts = self + .account_query() + .find_by_auth_id(&mut transaction, auth_account.id()) + .await?; + let cursor = if let Some(cursor) = cursor { + let id: Nanoid = Nanoid::new(cursor); + self.account_query() + .find_by_nanoid(&mut transaction, &id) + .await? + } else { + None + }; + let accounts = apply_pagination(accounts, limit, cursor, direction); + Ok(accounts.into_iter().map(AccountDto::from).collect()) } } } -impl GetAccountService for T where T: DependOnAccountQuery + 'static {} +impl GetAccountService for T where T: 'static + DependOnAccountQuery + DependOnAuthAccountQuery {} diff --git a/application/src/service/auth_account.rs b/application/src/service/auth_account.rs new file mode 100644 index 0000000..299bfad --- /dev/null +++ b/application/src/service/auth_account.rs @@ -0,0 +1,19 @@ +use kernel::interfaces::database::{DatabaseConnection, DependOnDatabaseConnection}; +use kernel::interfaces::query::{AuthAccountQuery, DependOnAuthAccountQuery}; +use kernel::prelude::entity::{AuthAccount, AuthAccountClientId}; +use kernel::KernelError; + +pub(crate) async fn get_auth_account< + S: DependOnDatabaseConnection + DependOnAuthAccountQuery + ?Sized, +>( + service: &S, + client_id: String, +) -> error_stack::Result, KernelError> { + let client_id = AuthAccountClientId::new(client_id); + let mut transaction = service.database_connection().begin_transaction().await?; + let auth_account = service + .auth_account_query() + .find_by_client_id(&mut transaction, &client_id) + .await?; + Ok(auth_account) +} diff --git a/application/src/transfer.rs b/application/src/transfer.rs index b0edc6c..971413f 100644 --- a/application/src/transfer.rs +++ b/application/src/transfer.rs @@ -1 +1,2 @@ pub mod account; +pub mod pagination; diff --git a/application/src/transfer/account.rs b/application/src/transfer/account.rs index 4856260..008bfef 100644 --- a/application/src/transfer/account.rs +++ b/application/src/transfer/account.rs @@ -1,5 +1,7 @@ +use kernel::prelude::entity::Account; use time::OffsetDateTime; +#[derive(Debug)] pub struct AccountDto { pub nanoid: String, pub name: String, @@ -7,3 +9,15 @@ pub struct AccountDto { pub is_bot: bool, pub created_at: OffsetDateTime, } + +impl From for AccountDto { + fn from(account: Account) -> Self { + Self { + nanoid: account.nanoid().as_ref().to_string(), + name: account.name().as_ref().to_string(), + public_key: account.public_key().as_ref().to_string(), + is_bot: *account.is_bot().as_ref(), + created_at: *account.created_at().as_ref(), + } + } +} diff --git a/application/src/transfer/pagination.rs b/application/src/transfer/pagination.rs new file mode 100644 index 0000000..b914664 --- /dev/null +++ b/application/src/transfer/pagination.rs @@ -0,0 +1,81 @@ +use vodca::Nameln; + +#[derive(Debug, Nameln)] +#[vodca(snake_case)] +pub enum Direction { + NEXT, + PREV, +} + +impl Default for Direction { + fn default() -> Self { + Self::NEXT + } +} + +impl TryFrom for Direction { + type Error = String; + + fn try_from(value: String) -> Result { + match value.as_str() { + "next" => Ok(Self::NEXT), + "prev" => Ok(Self::PREV), + other => Err(format!("Invalid direction: {}", other)), + } + } +} + +#[derive(Debug)] +pub struct Pagination { + pub limit: u32, + pub cursor: Option, + pub direction: Direction, +} + +impl Pagination { + pub fn new(limit: Option, cursor: Option, direction: Direction) -> Self { + Self { + limit: limit.unwrap_or(5), + cursor, + direction, + } + } +} + +pub(crate) fn apply_pagination( + vec: Vec, + limit: u32, + cursor_data: Option, + direction: Direction, +) -> Vec { + let mut vec = vec; + match direction { + Direction::NEXT => { + vec.sort(); + vec = vec + .into_iter() + .filter(|x| { + cursor_data + .as_ref() + .map(|cursor_data| x > cursor_data) + .unwrap_or(true) + }) + .take(limit as usize) + .collect(); + } + Direction::PREV => { + vec.sort_by(|a, b| b.cmp(a)); + vec = vec + .into_iter() + .filter(|x| { + cursor_data + .as_ref() + .map(|cursor_data| x < cursor_data) + .unwrap_or(true) + }) + .take(limit as usize) + .collect(); + } + }; + vec +} From 812bf330a074a30674f5036c40aee8caf298a965 Mon Sep 17 00:00:00 2001 From: turtton Date: Sat, 25 Jan 2025 19:52:00 +0900 Subject: [PATCH 13/59] :white_check_mark: Fix test codes --- server/src/permission.rs | 33 ++++++++++++++++++++++----------- 1 file changed, 22 insertions(+), 11 deletions(-) diff --git a/server/src/permission.rs b/server/src/permission.rs index 4671f63..880be65 100644 --- a/server/src/permission.rs +++ b/server/src/permission.rs @@ -1,6 +1,6 @@ use std::collections::HashSet; use std::hash::Hash; -use std::ops::{Add, BitAnd}; +use std::ops::Add; struct Request(HashSet); @@ -64,12 +64,10 @@ mod test { impl ToRequest for RoleManager { fn to_request(&self, target: &User) -> Request { - Request( - self.0.get(&target.id).map_or_else( - || HashSet::new(), - |role| vec![role.clone()].into_iter().collect::>(), - ), - ) + Request(self.0.get(&target.id).map_or_else( + HashSet::new, + |role| vec![role.clone()].into_iter().collect::>(), + )) } } @@ -96,16 +94,29 @@ mod test { fn test() { let mut role_manager = RoleManager(HashMap::new()); let mut action_roles = ActionRoles(HashMap::new()); - action_roles.0.insert(Action::Read, vec![Role::Admin, Role::User, Role::Guest].into_iter().collect::>()); - action_roles.0.insert(Action::Write, vec![Role::Admin, Role::User].into_iter().collect::>()); - action_roles.0.insert(Action::Delete, vec![Role::Admin].into_iter().collect::>()); + action_roles.0.insert( + Action::Read, + vec![Role::Admin, Role::User, Role::Guest] + .into_iter() + .collect::>(), + ); + action_roles.0.insert( + Action::Write, + vec![Role::Admin, Role::User] + .into_iter() + .collect::>(), + ); + action_roles.0.insert( + Action::Delete, + vec![Role::Admin].into_iter().collect::>(), + ); let user = User { id: 1 }; role_manager.0.insert(user.id, Role::User); let request = role_manager.to_request(&user); let read = action_roles.to_permission(Action::Read); - let write = action_roles.to_permission(Action::Delete); + let write = action_roles.to_permission(Action::Write); let new_perm = read + write; From f4c70ac78ab68bf5f817ecc632bf85fb8cb982f0 Mon Sep 17 00:00:00 2001 From: turtton Date: Sat, 25 Jan 2025 19:52:36 +0900 Subject: [PATCH 14/59] :hammer: Add get account path --- Cargo.lock | 1018 ++++++++++++++++++++++++++++++++--- server/Cargo.toml | 2 + server/src/keycloak.rs | 38 ++ server/src/main.rs | 8 +- server/src/route.rs | 38 +- server/src/route/account.rs | 33 +- 6 files changed, 1034 insertions(+), 103 deletions(-) create mode 100644 server/src/keycloak.rs diff --git a/Cargo.lock b/Cargo.lock index d9bf44a..d08678c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -45,6 +45,21 @@ version = "0.2.18" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + +[[package]] +name = "android_system_properties" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "819e7219dbd41043ac279b19830f2efc897156490d7fd6ea916720117ee66311" +dependencies = [ + "libc", +] + [[package]] name = "anyhow" version = "1.0.86" @@ -55,11 +70,9 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" name = "application" version = "0.1.0" dependencies = [ - "async-trait", "error-stack", "kernel", "time", - "uuid", "vodca", ] @@ -77,7 +90,7 @@ checksum = "6e0c28dcc82d7c8ead5cb13beb15405b57b8546e93215673ff8ca0349a028107" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.96", ] [[package]] @@ -89,6 +102,22 @@ dependencies = [ "num-traits", ] +[[package]] +name = "atomic-time" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9622f5c6fb50377516c70f65159e70b25465409760c6bd6d4e581318bf704e83" +dependencies = [ + "once_cell", + "portable-atomic", +] + +[[package]] +name = "atomic-waker" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" + [[package]] name = "autocfg" version = "1.3.0" @@ -121,7 +150,7 @@ dependencies = [ "serde_json", "serde_path_to_error", "serde_urlencoded", - "sync_wrapper 1.0.1", + "sync_wrapper", "tokio", "tower", "tower-layer", @@ -144,7 +173,7 @@ dependencies = [ "mime", "pin-project-lite", "rustversion", - "sync_wrapper 1.0.1", + "sync_wrapper", "tower-layer", "tower-service", "tracing", @@ -174,6 +203,35 @@ dependencies = [ "tracing", ] +[[package]] +name = "axum-keycloak-auth" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "be9f06871201a9be2a42da89f98ad7b588681d7ac6c6b18193e010732f90b8d3" +dependencies = [ + "atomic-time", + "axum", + "educe", + "futures", + "http", + "jsonwebtoken", + "nonempty", + "reqwest", + "serde", + "serde-querystring", + "serde_json", + "serde_with", + "snafu", + "time", + "tokio", + "tower", + "tracing", + "try-again", + "typed-builder", + "url", + "uuid", +] + [[package]] name = "backtrace" version = "0.3.73" @@ -195,6 +253,12 @@ version = "0.21.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" +[[package]] +name = "base64" +version = "0.22.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" + [[package]] name = "base64ct" version = "1.6.0" @@ -225,6 +289,12 @@ dependencies = [ "generic-array", ] +[[package]] +name = "bumpalo" +version = "3.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" + [[package]] name = "byteorder" version = "1.5.0" @@ -249,6 +319,19 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "chrono" +version = "0.4.39" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "num-traits", + "serde", + "windows-targets 0.52.6", +] + [[package]] name = "combine" version = "4.6.7" @@ -343,6 +426,41 @@ dependencies = [ "typenum", ] +[[package]] +name = "darling" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" +dependencies = [ + "darling_core", + "darling_macro", +] + +[[package]] +name = "darling_core" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn 2.0.96", +] + +[[package]] +name = "darling_macro" +version = "0.20.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" +dependencies = [ + "darling_core", + "quote", + "syn 2.0.96", +] + [[package]] name = "deadpool" version = "0.9.5" @@ -424,7 +542,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cfd72fa5c0aa087c0ce4f7039c664b106d638a61a6088663415b34bf4e803881" dependencies = [ "quote", - "syn 2.0.72", + "syn 2.0.96", ] [[package]] @@ -447,7 +565,7 @@ checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.96", ] [[package]] @@ -473,6 +591,18 @@ dependencies = [ "uuid", ] +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "either" version = "1.13.0" @@ -482,6 +612,35 @@ dependencies = [ "serde", ] +[[package]] +name = "encoding_rs" +version = "0.8.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75030f3c4f45dafd7586dd6780965a8c7e8e285a5ecb86713e63a79c5b2766f3" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "enum-ordinalize" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea0dcfa4e54eeb516fe454635a95753ddd39acda650ce703031c6973e315dd5" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "equivalent" version = "1.0.1" @@ -572,11 +731,26 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -584,15 +758,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +checksum = "1e28d1d997f585e54aebc3f97d39e72338912123a67330d723fdbb564d646c9f" dependencies = [ "futures-core", "futures-task", @@ -612,30 +786,43 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" + +[[package]] +name = "futures-macro" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -661,8 +848,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" dependencies = [ "cfg-if", + "js-sys", "libc", "wasi", + "wasm-bindgen", ] [[package]] @@ -671,6 +860,31 @@ version = "0.29.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40ecd4077b5ae9fd2e9e169b102c6c330d0605168eb0e8bf79952b256dbefffd" +[[package]] +name = "h2" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccae279728d634d083c00f6099cb58f01cc99c145b84b8be2f6c74618d79922e" +dependencies = [ + "atomic-waker", + "bytes", + "fnv", + "futures-core", + "futures-sink", + "http", + "indexmap 2.3.0", + "slab", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + [[package]] name = "hashbrown" version = "0.14.5" @@ -687,7 +901,7 @@ version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown", + "hashbrown 0.14.5", ] [[package]] @@ -696,7 +910,7 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" dependencies = [ - "base64", + "base64 0.21.7", "bytes", "headers-core", "http", @@ -764,9 +978,9 @@ dependencies = [ [[package]] name = "http" -version = "1.1.0" +version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21b9ddb458710bc376481b842f5da65cdf31522de232c1ca8146abce2a358258" +checksum = "f16ca2af56261c99fba8bac40a10251ce8188205a4c448fbb745a2e4daa76fea" dependencies = [ "bytes", "fnv", @@ -817,6 +1031,7 @@ dependencies = [ "bytes", "futures-channel", "futures-util", + "h2", "http", "http-body", "httparse", @@ -825,22 +1040,82 @@ dependencies = [ "pin-project-lite", "smallvec", "tokio", + "want", +] + +[[package]] +name = "hyper-rustls" +version = "0.27.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2d191583f3da1305256f22463b9bb0471acad48a4e534a5218b9963e9c1f59b2" +dependencies = [ + "futures-util", + "http", + "hyper", + "hyper-util", + "rustls", + "rustls-pki-types", + "tokio", + "tokio-rustls", + "tower-service", +] + +[[package]] +name = "hyper-tls" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70206fc6890eaca9fde8a0bf71caa2ddfc9fe045ac9e5c70df101a7dbde866e0" +dependencies = [ + "bytes", + "http-body-util", + "hyper", + "hyper-util", + "native-tls", + "tokio", + "tokio-native-tls", + "tower-service", ] [[package]] name = "hyper-util" -version = "0.1.9" +version = "0.1.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41296eb09f183ac68eec06e03cdbea2e759633d4067b2f6552fc2e009bcad08b" +checksum = "df2dcfbe0677734ab2f3ffa7fa7bfd4706bfdc1ef393f2ee30184aed67e631b4" dependencies = [ "bytes", + "futures-channel", "futures-util", "http", "http-body", "hyper", "pin-project-lite", + "socket2", "tokio", "tower-service", + "tracing", +] + +[[package]] +name = "iana-time-zone" +version = "0.1.61" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "235e081f3925a06703c2d0117ea8b91f042756fd6e7a6e5d901e8ca1a996b220" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "wasm-bindgen", + "windows-core", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", ] [[package]] @@ -958,9 +1233,15 @@ checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.96", ] +[[package]] +name = "ident_case" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" + [[package]] name = "idna" version = "1.0.3" @@ -982,6 +1263,17 @@ dependencies = [ "icu_properties", ] +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", + "serde", +] + [[package]] name = "indexmap" version = "2.3.0" @@ -989,15 +1281,47 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.14.5", + "serde", ] +[[package]] +name = "ipnet" +version = "2.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "469fb0b9cefa57e3ef31275ee7cacb78f2fdca44e4765491884a2b119d4eb130" + [[package]] name = "itoa" version = "1.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" +[[package]] +name = "js-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1cfaf33c695fc6e08064efbc1f72ec937429614f25eef83af942d0e227c3a28f" +dependencies = [ + "once_cell", + "wasm-bindgen", +] + +[[package]] +name = "jsonwebtoken" +version = "9.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b9ae10193d25051e74945f1ea2d0b42e03cc3b890f7e4cc5faa44997d808193f" +dependencies = [ + "base64 0.21.7", + "js-sys", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "kernel" version = "0.1.0" @@ -1022,11 +1346,84 @@ dependencies = [ "spin", ] +[[package]] +name = "lexical" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7aefb36fd43fef7003334742cbf77b243fcd36418a1d1bdd480d613a67968f6" +dependencies = [ + "lexical-core", +] + +[[package]] +name = "lexical-core" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2cde5de06e8d4c2faabc400238f9ae1c74d5412d03a7bd067645ccbc47070e46" +dependencies = [ + "lexical-parse-float", + "lexical-parse-integer", + "lexical-util", + "lexical-write-float", + "lexical-write-integer", +] + +[[package]] +name = "lexical-parse-float" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "683b3a5ebd0130b8fb52ba0bdc718cc56815b6a097e28ae5a6997d0ad17dc05f" +dependencies = [ + "lexical-parse-integer", + "lexical-util", + "static_assertions", +] + +[[package]] +name = "lexical-parse-integer" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d0994485ed0c312f6d965766754ea177d07f9c00c9b82a5ee62ed5b47945ee9" +dependencies = [ + "lexical-util", + "static_assertions", +] + +[[package]] +name = "lexical-util" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5255b9ff16ff898710eb9eb63cb39248ea8a5bb036bea8085b1a767ff6c4e3fc" +dependencies = [ + "static_assertions", +] + +[[package]] +name = "lexical-write-float" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accabaa1c4581f05a3923d1b4cfd124c329352288b7b9da09e766b0668116862" +dependencies = [ + "lexical-util", + "lexical-write-integer", + "static_assertions", +] + +[[package]] +name = "lexical-write-integer" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1b6f3d1f4422866b68192d62f77bc5c700bee84f3069f2469d7bc8c77852446" +dependencies = [ + "lexical-util", + "static_assertions", +] + [[package]] name = "libc" -version = "0.2.155" +version = "0.2.169" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" +checksum = "b5aba8db14291edd000dfcc4d620c7ebfb122c613afb886ca8803fa4e128a20a" [[package]] name = "libm" @@ -1173,6 +1570,12 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nonempty" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "303e8749c804ccd6ca3b428de7fe0d86cb86bc7606bc15291f100fd487960bb8" + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1294,7 +1697,7 @@ checksum = "a948666b637a0f465e8564c73e89d4dde00d72d4d473cc972f390fc3dcee7d9c" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.96", ] [[package]] @@ -1350,6 +1753,16 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" +[[package]] +name = "pem" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e459365e590736a54c3fa561947c84837534b8e9af6fc5bf781307e82658fae" +dependencies = [ + "base64 0.22.1", + "serde", +] + [[package]] name = "pem-rfc7468" version = "0.7.0" @@ -1404,6 +1817,12 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "portable-atomic" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" + [[package]] name = "powerfmt" version = "0.2.0" @@ -1421,9 +1840,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.86" +version = "1.0.93" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +checksum = "60946a68e5f9d28b0dc1c21bb8a97ee7d018a8b322fa57838ba31cc878e22d99" dependencies = [ "unicode-ident", ] @@ -1571,6 +1990,50 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "reqwest" +version = "0.12.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43e734407157c3c2034e0258f5e4473ddb361b1e85f95a66690d67264d7cd1da" +dependencies = [ + "base64 0.22.1", + "bytes", + "encoding_rs", + "futures-core", + "futures-util", + "h2", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-rustls", + "hyper-tls", + "hyper-util", + "ipnet", + "js-sys", + "log", + "mime", + "native-tls", + "once_cell", + "percent-encoding", + "pin-project-lite", + "rustls-pemfile", + "serde", + "serde_json", + "serde_urlencoded", + "sync_wrapper", + "system-configuration", + "tokio", + "tokio-native-tls", + "tower", + "tower-service", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "windows-registry", +] + [[package]] name = "retain_mut" version = "0.1.9" @@ -1588,11 +2051,26 @@ dependencies = [ "redis 0.27.4", "serde", "serde_json", - "thiserror", + "thiserror 1.0.63", "tokio", "tracing", ] +[[package]] +name = "ring" +version = "0.17.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c17fa4cb658e3583423e915b9f3acc01cceaee1860e33d59ebae66adc3a2dc0d" +dependencies = [ + "cc", + "cfg-if", + "getrandom", + "libc", + "spin", + "untrusted", + "windows-sys 0.52.0", +] + [[package]] name = "rsa" version = "0.9.6" @@ -1641,6 +2119,45 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rustls" +version = "0.23.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f287924602bf649d949c63dc8ac8b235fa5387d394020705b80c4eb597ce5b8" +dependencies = [ + "once_cell", + "rustls-pki-types", + "rustls-webpki", + "subtle", + "zeroize", +] + +[[package]] +name = "rustls-pemfile" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dce314e5fee3f39953d46bb63bb8a46d40c2f8fb7cc5a3b6cab2bde9721d6e50" +dependencies = [ + "rustls-pki-types", +] + +[[package]] +name = "rustls-pki-types" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2bf47e6ff922db3825eb750c4e2ff784c6ff8fb9e13046ef6a1d1c5401b0b37" + +[[package]] +name = "rustls-webpki" +version = "0.102.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "64ca1bc8749bd4cf37b5ce386cc146580777b4e8572c7b97baf22c83f444bee9" +dependencies = [ + "ring", + "rustls-pki-types", + "untrusted", +] + [[package]] name = "rustversion" version = "1.0.17" @@ -1699,22 +2216,32 @@ checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" [[package]] name = "serde" -version = "1.0.205" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e33aedb1a7135da52b7c21791455563facbbcc43d0f0f66165b42c21b3dfb150" +checksum = "02fc4265df13d6fa1d00ecff087228cc0a2b5f3c0e87e258d8b94a156e984c70" dependencies = [ "serde_derive", ] +[[package]] +name = "serde-querystring" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce88f8e75d0920545b35b78775e0927137337401f65b01326ce299734885fd21" +dependencies = [ + "lexical", + "serde", +] + [[package]] name = "serde_derive" -version = "1.0.205" +version = "1.0.217" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692d6f5ac90220161d6774db30c662202721e64aed9058d2c394f451261420c1" +checksum = "5a9bf7cf98d04a2b28aead066b7496853d4779c9cc183c440dbac457641e19a0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.96", ] [[package]] @@ -1724,7 +2251,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8de514ef58196f1fc96dcaef80fe6170a1ce6215df9687a93fe8300e773fefc5" dependencies = [ "form_urlencoded", - "indexmap", + "indexmap 2.3.0", "itoa", "ryu", "serde", @@ -1732,9 +2259,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.122" +version = "1.0.137" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784b6203951c57ff748476b126ccb5e8e2959a5c19e5c617ab1956be3dbc68da" +checksum = "930cfb6e6abf99298aaad7d29abbef7a9999a9a8806a40088f55f0dcec03146b" dependencies = [ "itoa", "memchr", @@ -1764,6 +2291,36 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_with" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.3.0", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros", + "time", +] + +[[package]] +name = "serde_with_macros" +version = "3.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" +dependencies = [ + "darling", + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "server" version = "0.1.0" @@ -1771,11 +2328,13 @@ dependencies = [ "application", "axum", "axum-extra", + "axum-keycloak-auth", "destructure", "dotenvy", "driver", "error-stack", "kernel", + "log", "rand", "rand_chacha", "rikka-mq", @@ -1828,6 +2387,15 @@ dependencies = [ "lazy_static", ] +[[package]] +name = "signal-hook-registry" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" +dependencies = [ + "libc", +] + [[package]] name = "signature" version = "2.2.0" @@ -1838,6 +2406,18 @@ dependencies = [ "rand_core", ] +[[package]] +name = "simple_asn1" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "297f631f50729c8c99b84667867963997ec0b50f32b2a7dbcab828ef0541e8bb" +dependencies = [ + "num-bigint", + "num-traits", + "thiserror 2.0.11", + "time", +] + [[package]] name = "slab" version = "0.4.9" @@ -1853,6 +2433,27 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +[[package]] +name = "snafu" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "223891c85e2a29c3fe8fb900c1fae5e69c2e42415e3177752e8718475efa5019" +dependencies = [ + "snafu-derive", +] + +[[package]] +name = "snafu-derive" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03c3c6b7927ffe7ecaa769ee0e3994da3b8cafc8f444578982c83ecb161af917" +dependencies = [ + "heck", + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "socket2" version = "0.5.7" @@ -1926,7 +2527,7 @@ dependencies = [ "futures-util", "hashlink", "hex", - "indexmap", + "indexmap 2.3.0", "log", "memchr", "native-tls", @@ -1938,7 +2539,7 @@ dependencies = [ "sha2", "smallvec", "sqlformat", - "thiserror", + "thiserror 1.0.63", "time", "tokio", "tokio-stream", @@ -1993,7 +2594,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1ed31390216d20e538e447a7a9b959e06ed9fc51c37b514b46eb758016ecd418" dependencies = [ "atoi", - "base64", + "base64 0.21.7", "bitflags 2.6.0", "byteorder", "bytes", @@ -2023,7 +2624,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 1.0.63", "time", "tracing", "uuid", @@ -2037,7 +2638,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c824eb80b894f926f89a0b9da0c7f435d27cdd35b8c655b114e58223918577e" dependencies = [ "atoi", - "base64", + "base64 0.21.7", "bitflags 2.6.0", "byteorder", "crc", @@ -2063,7 +2664,7 @@ dependencies = [ "smallvec", "sqlx-core", "stringprep", - "thiserror", + "thiserror 1.0.63", "time", "tracing", "uuid", @@ -2101,6 +2702,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + [[package]] name = "stringprep" version = "0.1.5" @@ -2112,6 +2719,12 @@ dependencies = [ "unicode-properties", ] +[[package]] +name = "strsim" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" + [[package]] name = "subtle" version = "2.6.1" @@ -2131,26 +2744,23 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.72" +version = "2.0.96" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc4b9b9bf2add8093d3f2c0204471e951b2285580335de42f9d2534f3ae7a8af" +checksum = "d5d0adab1ae378d7f53bdebc67a39f1f151407ef230f0ce2883572f5d8985c80" dependencies = [ "proc-macro2", "quote", "unicode-ident", ] -[[package]] -name = "sync_wrapper" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2047c6ded9c721764247e62cd3b03c09ffc529b2ba5b10ec482ae507a4a70160" - [[package]] name = "sync_wrapper" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a7065abeca94b6a8a577f9bd45aa0867a2238b74e8eb67cf10d492bc39351394" +dependencies = [ + "futures-core", +] [[package]] name = "synstructure" @@ -2160,7 +2770,28 @@ checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.96", +] + +[[package]] +name = "system-configuration" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" +dependencies = [ + "bitflags 2.6.0", + "core-foundation", + "system-configuration-sys", +] + +[[package]] +name = "system-configuration-sys" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e1d1b10ced5ca923a1fcb8d03e96b8d3268065d724548c0211415ff6ac6bac4" +dependencies = [ + "core-foundation-sys", + "libc", ] [[package]] @@ -2182,7 +2813,16 @@ version = "1.0.63" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0342370b38b6a11b6cc11d6a805569958d54cfa061a29969c3b5ce2ea405724" dependencies = [ - "thiserror-impl", + "thiserror-impl 1.0.63", +] + +[[package]] +name = "thiserror" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d452f284b73e6d76dd36758a0c8684b1d5be31f92b89d07fd5822175732206fc" +dependencies = [ + "thiserror-impl 2.0.11", ] [[package]] @@ -2193,7 +2833,18 @@ checksum = "a4558b58466b9ad7ca0f102865eccc95938dca1a74a856f2b57b6629050da261" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.96", +] + +[[package]] +name = "thiserror-impl" +version = "2.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26afc1baea8a989337eeb52b6e72a039780ce45c3edfcc9c5b9d112feeb173c2" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", ] [[package]] @@ -2208,9 +2859,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.36" +version = "0.3.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" dependencies = [ "deranged", "itoa", @@ -2229,9 +2880,9 @@ checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" [[package]] name = "time-macros" -version = "0.2.18" +version = "0.2.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" dependencies = [ "num-conv", "time-core", @@ -2264,15 +2915,17 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.39.2" +version = "1.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daa4fb1bc778bd6f04cbfc4bb2d06a7396a8f299dc33ea1900cedaa316f467b1" +checksum = "3d61fa4ffa3de412bfea335c6ecff681de2b609ba3c77ef3e00e521813a9ed9e" dependencies = [ "backtrace", "bytes", "libc", "mio", + "parking_lot", "pin-project-lite", + "signal-hook-registry", "socket2", "tokio-macros", "windows-sys 0.52.0", @@ -2280,13 +2933,33 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693d596312e88961bc67d7f1f97af8a70227d9f90c31bba5806eec004978d752" +checksum = "6e06d43f1345a3bcd39f6a56dbb7dcab2ba47e68e8ac134855e7e2bdbaf8cab8" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.96", +] + +[[package]] +name = "tokio-native-tls" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbae76ab933c85776efabc971569dd6119c580d8f5d448769dec1764bf796ef2" +dependencies = [ + "native-tls", + "tokio", +] + +[[package]] +name = "tokio-rustls" +version = "0.26.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f6d0975eaace0cf0fcadee4e4aaa5da15b5c079146f2cffb67c113be122bf37" +dependencies = [ + "rustls", + "tokio", ] [[package]] @@ -2315,14 +2988,14 @@ dependencies = [ [[package]] name = "tower" -version = "0.5.1" +version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2873938d487c3cfb9aed7546dc9f2711d867c9f90c46b889989a2cb84eba6b4f" +checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9" dependencies = [ "futures-core", "futures-util", "pin-project-lite", - "sync_wrapper 0.1.2", + "sync_wrapper", "tokio", "tower-layer", "tower-service", @@ -2360,9 +3033,9 @@ checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" [[package]] name = "tracing" -version = "0.1.40" +version = "0.1.41" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c3523ab5a71916ccf420eebdf5521fcef02141234bbc0b8a49f2fdc4544364ef" +checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" dependencies = [ "log", "pin-project-lite", @@ -2377,27 +3050,27 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3566e8ce28cc0a3fe42519fc80e6b4c943cc4c8cef275620eb8dac2d3d4e06cf" dependencies = [ "crossbeam-channel", - "thiserror", + "thiserror 1.0.63", "time", "tracing-subscriber", ] [[package]] name = "tracing-attributes" -version = "0.1.27" +version = "0.1.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34704c8d6ebcbc939824180af020566b01a7c01f80641264eba0999f6c2b6be7" +checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.96", ] [[package]] name = "tracing-core" -version = "0.1.32" +version = "0.1.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06d3da6113f116aaee68e4d601191614c9053067f9ab7f6edbcb161237daa54" +checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" dependencies = [ "once_cell", "valuable", @@ -2432,6 +3105,42 @@ dependencies = [ "tracing-log", ] +[[package]] +name = "try-again" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1006cfd66eadaaa9ec7a3d011336e40b98cd6c4d8f7b1e331080f6145ac00671" +dependencies = [ + "tokio", + "tracing", +] + +[[package]] +name = "try-lock" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" + +[[package]] +name = "typed-builder" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7e14ed59dc8b7b26cacb2a92bad2e8b1f098806063898ab42a3bd121d7d45e75" +dependencies = [ + "typed-builder-macro", +] + +[[package]] +name = "typed-builder-macro" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "560b82d656506509d43abe30e0ba64c56b1953ab3d4fe7ba5902747a7a3cedd5" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "typenum" version = "1.17.0" @@ -2477,6 +3186,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + [[package]] name = "url" version = "2.5.4" @@ -2508,9 +3223,9 @@ checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" [[package]] name = "uuid" -version = "1.10.0" +version = "1.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81dfa00651efa65069b0b6b651f4aaa31ba9e3c3ce0137aaad053604ee7e0314" +checksum = "b3758f5e68192bb96cc8f9b7e2c2cfdabb435499a28499a42f8f984092adad4b" dependencies = [ "getrandom", "serde", @@ -2542,7 +3257,16 @@ checksum = "1d74d8fdab2e45cc05fa4964e5ff7ee72499f9edfffb38455bb94e62d1e3bcaa" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.96", +] + +[[package]] +name = "want" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" +dependencies = [ + "try-lock", ] [[package]] @@ -2557,6 +3281,87 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" +[[package]] +name = "wasm-bindgen" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1edc8929d7499fc4e8f0be2262a241556cfc54a0bea223790e71446f2aab1ef5" +dependencies = [ + "cfg-if", + "once_cell", + "rustversion", + "wasm-bindgen-macro", +] + +[[package]] +name = "wasm-bindgen-backend" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2f0a0651a5c2bc21487bde11ee802ccaf4c51935d0d3d42a6101f98161700bc6" +dependencies = [ + "bumpalo", + "log", + "proc-macro2", + "quote", + "syn 2.0.96", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-futures" +version = "0.4.50" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "555d470ec0bc3bb57890405e5d4322cc9ea83cebb085523ced7be4144dac1e61" +dependencies = [ + "cfg-if", + "js-sys", + "once_cell", + "wasm-bindgen", + "web-sys", +] + +[[package]] +name = "wasm-bindgen-macro" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7fe63fc6d09ed3792bd0897b314f53de8e16568c2b3f7982f468c0bf9bd0b407" +dependencies = [ + "quote", + "wasm-bindgen-macro-support", +] + +[[package]] +name = "wasm-bindgen-macro-support" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ae87ea40c9f689fc23f209965b6fb8a99ad69aeeb0231408be24920604395de" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", + "wasm-bindgen-backend", + "wasm-bindgen-shared", +] + +[[package]] +name = "wasm-bindgen-shared" +version = "0.2.100" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a05d73b933a847d6cccdda8f838a22ff101ad9bf93e33684f39c1f5f0eece3d" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "web-sys" +version = "0.3.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33b6dd2ef9186f1f2072e409e99cd22a975331a6b3591b12c764e0e55c60d5d2" +dependencies = [ + "js-sys", + "wasm-bindgen", +] + [[package]] name = "whoami" version = "1.5.1" @@ -2589,6 +3394,45 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows-core" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "33ab640c8d7e35bf8ba19b884ba838ceb4fba93a4e8c65a9059d08afcfc683d9" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-registry" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" +dependencies = [ + "windows-result", + "windows-strings", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-result" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d1043d8214f791817bab27572aaa8af63732e11bf84aa21a45a78d6c317ae0e" +dependencies = [ + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-strings" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" +dependencies = [ + "windows-result", + "windows-targets 0.52.6", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -2769,7 +3613,7 @@ checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.96", "synstructure", ] @@ -2791,7 +3635,7 @@ checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.96", ] [[package]] @@ -2811,7 +3655,7 @@ checksum = "595eed982f7d355beb85837f651fa22e90b3c044842dc7f2c2842c086f295808" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.96", "synstructure", ] @@ -2840,5 +3684,5 @@ checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" dependencies = [ "proc-macro2", "quote", - "syn 2.0.72", + "syn 2.0.96", ] diff --git a/server/Cargo.toml b/server/Cargo.toml index 1b5b770..2ac693c 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -20,6 +20,7 @@ tracing-subscriber = { workspace = true } axum = { version = "0.7.4", features = ["json", "tracing"] } axum-extra = { version = "0.9.2", features = ["typed-header", "query"] } +axum-keycloak-auth = "0.6.0" tower-http = { version = "0.5.1", features = ["tokio", "cors"] } tokio = { workspace = true, features = ["rt", "rt-multi-thread", "macros"] } @@ -33,3 +34,4 @@ serde = { workspace = true } application = { path = "../application" } driver = { path = "../driver" } kernel = { path = "../kernel" } +log = "0.4.22" diff --git a/server/src/keycloak.rs b/server/src/keycloak.rs new file mode 100644 index 0000000..f99607e --- /dev/null +++ b/server/src/keycloak.rs @@ -0,0 +1,38 @@ +use axum_keycloak_auth::instance::{KeycloakAuthInstance, KeycloakConfig}; + +const KEYCLOAK_SERVER_KEY: &str = "KEYCLOAK_SERVER"; +const KEYCLOAK_REALM_KEY: &str = "KEYCLOAK_REALM"; + +pub fn create_keycloak_instance() -> KeycloakAuthInstance { + let server = + dotenvy::var(KEYCLOAK_SERVER_KEY).unwrap_or_else(|_| "http://localhost:18080/".to_string()); + let realm = dotenvy::var(KEYCLOAK_REALM_KEY).unwrap_or_else(|_| "MyRealm".to_string()); + log::info!("Keycloak info: server={server}, realm={realm}"); + KeycloakAuthInstance::new( + KeycloakConfig::builder() + .server(server.parse().unwrap()) + .realm(realm) + .build(), + ) +} + +#[macro_export] +macro_rules! expect_role { + ($token: expr, $req: expr) => { + let expected_roles = + $crate::route::to_permission_strings(&$req.uri().to_string(), $req.method().as_str()); + let role_result = expected_roles + .iter() + .map(|role| axum_keycloak_auth::role::ExpectRoles::expect_roles($token, &[role])) + .collect::>>(); + if !role_result.iter().any(|r| r.is_ok()) { + return Err($crate::error::ErrorStatus::from(( + StatusCode::FORBIDDEN, + format!( + "Permission denied: required roles(any) = {:?}", + expected_roles + ), + ))); + } + }; +} diff --git a/server/src/main.rs b/server/src/main.rs index a95bad2..9ded590 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,12 +1,13 @@ mod error; mod handler; -mod route; +mod keycloak; mod permission; +mod route; use crate::error::StackTrace; use crate::handler::AppModule; +use crate::keycloak::create_keycloak_instance; use crate::route::account::AccountRouter; -use axum::ServiceExt; use error_stack::ResultExt; use kernel::KernelError; use std::net::SocketAddr; @@ -38,10 +39,11 @@ async fn main() -> Result<(), StackTrace> { ) .init(); + let keycloak_auth_instance = create_keycloak_instance(); let app = AppModule::new().await?; let router = axum::Router::new() - .route_account() + .route_account(keycloak_auth_instance) .layer(CorsLayer::new()) .with_state(app); diff --git a/server/src/route.rs b/server/src/route.rs index b875493..8a1cb9e 100644 --- a/server/src/route.rs +++ b/server/src/route.rs @@ -1,21 +1,49 @@ use crate::error::ErrorStatus; -use application::service::Direction; +use application::transfer::pagination::Direction; use axum::http::StatusCode; pub mod account; trait DirectionConverter { - fn convert_to_direction(self) -> Result, ErrorStatus>; + fn convert_to_direction(self) -> Result; } impl DirectionConverter for Option { - fn convert_to_direction(self) -> Result, ErrorStatus> { + fn convert_to_direction(self) -> Result { match self { Some(d) => match Direction::try_from(d) { - Ok(d) => Ok(Some(d)), + Ok(d) => Ok(d), Err(message) => Err((StatusCode::BAD_REQUEST, message).into()), }, - None => Ok(None), + None => Ok(Direction::default()), } } } + +/// ("/a/b/c", GET) -> `["/a:GET", "/a/b:GET", "/a/b/c:GET"]` +pub(super) fn to_permission_strings(path: &str, method: &str) -> Vec { + path.split('/') + .filter(|s| !s.is_empty()) + .scan(String::new(), |state, part| { + if !state.is_empty() { + state.push('/'); + } + state.push_str(part); + Some(format!("/{state}:{method}")) + }) + .collect::>() +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_to_permission_strings() { + let path = "/a/b/c"; + let method = "GET"; + let result = to_permission_strings(path, method); + let expected = vec!["/a:GET", "/a/b:GET", "/a/b/c:GET"]; + assert_eq!(result, expected); + } +} diff --git a/server/src/route/account.rs b/server/src/route/account.rs index 08ae4cc..f4e5b86 100644 --- a/server/src/route/account.rs +++ b/server/src/route/account.rs @@ -1,17 +1,23 @@ use crate::error::ErrorStatus; +use crate::expect_role; use crate::handler::AppModule; use crate::route::DirectionConverter; use application::service::account::GetAccountService; -use axum::extract::{Query, State}; +use application::transfer::pagination::Pagination; +use axum::extract::{Query, Request, State}; use axum::http::StatusCode; use axum::routing::get; -use axum::{Json, Router}; +use axum::{Extension, Json, Router}; +use axum_keycloak_auth::decode::KeycloakToken; +use axum_keycloak_auth::instance::KeycloakAuthInstance; +use axum_keycloak_auth::layer::KeycloakAuthLayer; +use axum_keycloak_auth::PassthroughMode; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; #[derive(Debug, Deserialize)] struct GetAllAccountQuery { - limit: Option, + limit: Option, cursor: Option, direction: Option, } @@ -33,25 +39,29 @@ struct AccountsResponse { } pub trait AccountRouter { - fn route_account(self) -> Self; + fn route_account(self, instance: KeycloakAuthInstance) -> Self; } impl AccountRouter for Router { - fn route_account(self) -> Self { + fn route_account(self, instance: KeycloakAuthInstance) -> Self { self.route( "/accounts", get( - |State(module): State, + |Extension(token): Extension>, + State(module): State, Query(GetAllAccountQuery { direction, limit, cursor, - }): Query| async move { + }): Query, + req: Request| async move { + expect_role!(&token, req); let direction = direction.convert_to_direction()?; + let pagination = Pagination::new(limit, cursor, direction); let result = module .handler() .pgpool() - .get_all_accounts(limit, cursor, direction) + .get_all_accounts(token.subject, pagination) .await .map_err(ErrorStatus::from)?; if result.is_empty() { @@ -78,5 +88,12 @@ impl AccountRouter for Router { }, ), ) + .layer( + KeycloakAuthLayer::::builder() + .instance(instance) + .passthrough_mode(PassthroughMode::Block) + .expected_audiences(vec![String::from("account")]) + .build(), + ) } } From 93699638143fff10df936fcbf2a2a8bee06b3768 Mon Sep 17 00:00:00 2001 From: turtton Date: Mon, 27 Jan 2025 20:04:24 +0900 Subject: [PATCH 15/59] :bug: Add missed case --- server/src/route.rs | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/server/src/route.rs b/server/src/route.rs index 8a1cb9e..ad3c54d 100644 --- a/server/src/route.rs +++ b/server/src/route.rs @@ -20,10 +20,9 @@ impl DirectionConverter for Option { } } -/// ("/a/b/c", GET) -> `["/a:GET", "/a/b:GET", "/a/b/c:GET"]` +/// ("/a/b/c", "GET") -> `["/:GET", "/a:GET", "/a/b:GET", "/a/b/c:GET"]` pub(super) fn to_permission_strings(path: &str, method: &str) -> Vec { path.split('/') - .filter(|s| !s.is_empty()) .scan(String::new(), |state, part| { if !state.is_empty() { state.push('/'); @@ -43,7 +42,7 @@ mod test { let path = "/a/b/c"; let method = "GET"; let result = to_permission_strings(path, method); - let expected = vec!["/a:GET", "/a/b:GET", "/a/b/c:GET"]; + let expected = vec!["/:GET", "/a:GET", "/a/b:GET", "/a/b/c:GET"]; assert_eq!(result, expected); } } From b8f8dde8622c68d4b08347a594f8f6d4d2cb7d26 Mon Sep 17 00:00:00 2001 From: turtton Date: Mon, 27 Jan 2025 20:05:13 +0900 Subject: [PATCH 16/59] :art: Format codes --- server/src/permission.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/server/src/permission.rs b/server/src/permission.rs index 880be65..af3c21e 100644 --- a/server/src/permission.rs +++ b/server/src/permission.rs @@ -64,10 +64,9 @@ mod test { impl ToRequest for RoleManager { fn to_request(&self, target: &User) -> Request { - Request(self.0.get(&target.id).map_or_else( - HashSet::new, - |role| vec![role.clone()].into_iter().collect::>(), - )) + Request(self.0.get(&target.id).map_or_else(HashSet::new, |role| { + vec![role.clone()].into_iter().collect::>() + })) } } From 2e1d3bd7d936c90ccab46910feec2564c673f3b3 Mon Sep 17 00:00:00 2001 From: turtton Date: Fri, 7 Feb 2025 11:36:03 +0900 Subject: [PATCH 17/59] :wrench: Automate keycloak initialization --- .gitignore | 4 +- README.md | 22 +- keycloak-data/import/emumet-realm.json | 2046 ++++++++++++++++++++++ keycloak-data/import/emumet-users-0.json | 25 + 4 files changed, 2094 insertions(+), 3 deletions(-) create mode 100644 keycloak-data/import/emumet-realm.json create mode 100644 keycloak-data/import/emumet-users-0.json diff --git a/.gitignore b/.gitignore index a7510ff..ce82118 100644 --- a/.gitignore +++ b/.gitignore @@ -2,4 +2,6 @@ .idea .direnv /migrations/dbml-error.log -.env \ No newline at end of file +.env +keycloak-data/* +!keycloak-data/import \ No newline at end of file diff --git a/README.md b/README.md index b300923..8e44011 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ # Emumet + @@ -6,11 +7,27 @@ # Keycloak ```shell -podman run --rm -p 18080:8080 -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin --name emumet-keycloak docker.io/keycloak/keycloak:26.0.7 start-dev +mkdir -p keycloak-data/h2 +podman run --rm -it -v ./keycloak-data/h2:/opt/keycloak/data/h2:Z,U -v ./keycloak-data/import:/opt/keycloak/data/import:Z,U -p 18080:8080 -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin --name emumet-keycloak quay.io/keycloak/keycloak:26.1 start-dev --import-realm ``` -> Url: http://localhost:18080 + +> - Url: http://localhost:18080 +> - ユーザー名: admin +> - パスワード: admin +> - realm: emumet + +### Update realm data + +```shell +mkdir -p keycloak-data/export +podman run --rm -v ./keycloak-data/h2:/opt/keycloak/data/h2:Z,U -v ./keycloak-data/export:/opt/keycloak/data/export:Z,U quay.io/keycloak/keycloak:latest export --dir /opt/keycloak/data/export --users same_file --realm emumet +sudo cp keycloak-data/export/* keycloak-data/import/ +``` + +> keycloak本体を停止してから実行してください # DB + Podman(docker)にて環境構築が可能です ```shell @@ -21,4 +38,5 @@ podman run --rm --name emumet-postgres -e POSTGRES_PASSWORD=develop -p 5432:5432 > パスワード: develop # 語源 + EMU(Extravehicular Mobility Unit=宇宙服)+Helmet diff --git a/keycloak-data/import/emumet-realm.json b/keycloak-data/import/emumet-realm.json new file mode 100644 index 0000000..ac5e401 --- /dev/null +++ b/keycloak-data/import/emumet-realm.json @@ -0,0 +1,2046 @@ +{ + "id" : "20fdc959-26f7-4a13-92ea-5fcd3380d354", + "realm" : "emumet", + "notBefore" : 0, + "defaultSignatureAlgorithm" : "RS256", + "revokeRefreshToken" : false, + "refreshTokenMaxReuse" : 0, + "accessTokenLifespan" : 300, + "accessTokenLifespanForImplicitFlow" : 900, + "ssoSessionIdleTimeout" : 1800, + "ssoSessionMaxLifespan" : 36000, + "ssoSessionIdleTimeoutRememberMe" : 0, + "ssoSessionMaxLifespanRememberMe" : 0, + "offlineSessionIdleTimeout" : 2592000, + "offlineSessionMaxLifespanEnabled" : false, + "offlineSessionMaxLifespan" : 5184000, + "clientSessionIdleTimeout" : 0, + "clientSessionMaxLifespan" : 0, + "clientOfflineSessionIdleTimeout" : 0, + "clientOfflineSessionMaxLifespan" : 0, + "accessCodeLifespan" : 60, + "accessCodeLifespanUserAction" : 300, + "accessCodeLifespanLogin" : 1800, + "actionTokenGeneratedByAdminLifespan" : 43200, + "actionTokenGeneratedByUserLifespan" : 300, + "oauth2DeviceCodeLifespan" : 600, + "oauth2DevicePollingInterval" : 5, + "enabled" : true, + "sslRequired" : "external", + "registrationAllowed" : false, + "registrationEmailAsUsername" : false, + "rememberMe" : false, + "verifyEmail" : false, + "loginWithEmailAllowed" : true, + "duplicateEmailsAllowed" : false, + "resetPasswordAllowed" : false, + "editUsernameAllowed" : false, + "bruteForceProtected" : false, + "permanentLockout" : false, + "maxTemporaryLockouts" : 0, + "bruteForceStrategy" : "MULTIPLE", + "maxFailureWaitSeconds" : 900, + "minimumQuickLoginWaitSeconds" : 60, + "waitIncrementSeconds" : 60, + "quickLoginCheckMilliSeconds" : 1000, + "maxDeltaTimeSeconds" : 43200, + "failureFactor" : 30, + "roles" : { + "realm" : [ { + "id" : "8d81d43e-775d-4db2-b93b-87d4a4b002a8", + "name" : "uma_authorization", + "description" : "${role_uma_authorization}", + "composite" : false, + "clientRole" : false, + "containerId" : "20fdc959-26f7-4a13-92ea-5fcd3380d354", + "attributes" : { } + }, { + "id" : "855612d9-abeb-4121-946b-dfb505977829", + "name" : "offline_access", + "description" : "${role_offline-access}", + "composite" : false, + "clientRole" : false, + "containerId" : "20fdc959-26f7-4a13-92ea-5fcd3380d354", + "attributes" : { } + }, { + "id" : "71555fc5-bcff-47ec-a2ba-9bdb33a813d5", + "name" : "default-roles-emumet", + "description" : "${role_default-roles}", + "composite" : true, + "composites" : { + "realm" : [ "offline_access", "uma_authorization" ], + "client" : { + "account" : [ "manage-account", "view-profile" ] + } + }, + "clientRole" : false, + "containerId" : "20fdc959-26f7-4a13-92ea-5fcd3380d354", + "attributes" : { } + } ], + "client" : { + "myclient" : [ ], + "realm-management" : [ { + "id" : "8166caae-b0a9-4c11-8bff-6fcf4a0e8b7e", + "name" : "manage-authorization", + "description" : "${role_manage-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", + "attributes" : { } + }, { + "id" : "79ed0831-5266-41ae-86a0-16b70079d649", + "name" : "query-groups", + "description" : "${role_query-groups}", + "composite" : false, + "clientRole" : true, + "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", + "attributes" : { } + }, { + "id" : "5ae5edd9-73e8-4ea4-9837-043b05d59766", + "name" : "view-authorization", + "description" : "${role_view-authorization}", + "composite" : false, + "clientRole" : true, + "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", + "attributes" : { } + }, { + "id" : "a1203f7d-f93b-4ac2-b56a-065f6e3a1844", + "name" : "view-clients", + "description" : "${role_view-clients}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-clients" ] + } + }, + "clientRole" : true, + "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", + "attributes" : { } + }, { + "id" : "012f433b-d622-47c2-895c-3c970209f193", + "name" : "manage-realm", + "description" : "${role_manage-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", + "attributes" : { } + }, { + "id" : "ef2860ce-4b70-46b1-999a-a52252750b48", + "name" : "view-identity-providers", + "description" : "${role_view-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", + "attributes" : { } + }, { + "id" : "e5235e1f-3470-4419-bb81-67f76c91aa23", + "name" : "query-realms", + "description" : "${role_query-realms}", + "composite" : false, + "clientRole" : true, + "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", + "attributes" : { } + }, { + "id" : "a5e183c3-6925-4228-9e9c-00e224a315a8", + "name" : "manage-users", + "description" : "${role_manage-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", + "attributes" : { } + }, { + "id" : "f1aa65cd-c595-44cd-90df-142cf5a8d7a5", + "name" : "view-events", + "description" : "${role_view-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", + "attributes" : { } + }, { + "id" : "d2db0c5e-f7da-4ae0-9a87-84c0f280c16c", + "name" : "realm-admin", + "description" : "${role_realm-admin}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "view-authorization", "query-groups", "manage-authorization", "view-clients", "manage-realm", "view-identity-providers", "query-realms", "manage-users", "view-events", "query-users", "manage-events", "manage-clients", "view-users", "view-realm", "impersonation", "create-client", "query-clients", "manage-identity-providers" ] + } + }, + "clientRole" : true, + "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", + "attributes" : { } + }, { + "id" : "eaebdfa1-e6c6-4b44-aedc-819ee3c6c4ec", + "name" : "query-users", + "description" : "${role_query-users}", + "composite" : false, + "clientRole" : true, + "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", + "attributes" : { } + }, { + "id" : "c0d79eeb-9eb7-4e14-b50e-2b0500fc7181", + "name" : "manage-events", + "description" : "${role_manage-events}", + "composite" : false, + "clientRole" : true, + "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", + "attributes" : { } + }, { + "id" : "0180a75e-9022-4a12-842d-295172cccb22", + "name" : "manage-clients", + "description" : "${role_manage-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", + "attributes" : { } + }, { + "id" : "68095bbe-5a69-4dc6-8972-ca96982a92c2", + "name" : "view-users", + "description" : "${role_view-users}", + "composite" : true, + "composites" : { + "client" : { + "realm-management" : [ "query-groups", "query-users" ] + } + }, + "clientRole" : true, + "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", + "attributes" : { } + }, { + "id" : "d92bbdfa-acd5-4e74-b518-9df525cab01f", + "name" : "view-realm", + "description" : "${role_view-realm}", + "composite" : false, + "clientRole" : true, + "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", + "attributes" : { } + }, { + "id" : "f2365fb1-d03f-4dc0-aa83-7d96e4fab676", + "name" : "impersonation", + "description" : "${role_impersonation}", + "composite" : false, + "clientRole" : true, + "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", + "attributes" : { } + }, { + "id" : "d918fc22-2751-4d21-b924-5e769b867e78", + "name" : "create-client", + "description" : "${role_create-client}", + "composite" : false, + "clientRole" : true, + "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", + "attributes" : { } + }, { + "id" : "df7d3e14-4e9f-4b3c-a01a-20d5d25459bf", + "name" : "query-clients", + "description" : "${role_query-clients}", + "composite" : false, + "clientRole" : true, + "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", + "attributes" : { } + }, { + "id" : "7f072c25-1801-4fd4-aedd-94d1964821ba", + "name" : "manage-identity-providers", + "description" : "${role_manage-identity-providers}", + "composite" : false, + "clientRole" : true, + "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", + "attributes" : { } + } ], + "security-admin-console" : [ ], + "admin-cli" : [ ], + "account-console" : [ ], + "broker" : [ { + "id" : "25c3d9c4-c9d8-4760-a29a-55b91f295530", + "name" : "read-token", + "description" : "${role_read-token}", + "composite" : false, + "clientRole" : true, + "containerId" : "e233a020-34ea-4e9d-9949-f9d839c4eb64", + "attributes" : { } + } ], + "account" : [ { + "id" : "9d39f10a-69d3-419b-b9fa-8328b4717b0b", + "name" : "manage-account", + "description" : "${role_manage-account}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "manage-account-links" ] + } + }, + "clientRole" : true, + "containerId" : "6ea8dcc8-c870-4012-8382-141d26864418", + "attributes" : { } + }, { + "id" : "7492bf66-f2a2-423f-9995-3cab271300cf", + "name" : "view-groups", + "description" : "${role_view-groups}", + "composite" : false, + "clientRole" : true, + "containerId" : "6ea8dcc8-c870-4012-8382-141d26864418", + "attributes" : { } + }, { + "id" : "2e059bf9-1c7c-4a59-958b-ea5082761c2f", + "name" : "manage-consent", + "description" : "${role_manage-consent}", + "composite" : true, + "composites" : { + "client" : { + "account" : [ "view-consent" ] + } + }, + "clientRole" : true, + "containerId" : "6ea8dcc8-c870-4012-8382-141d26864418", + "attributes" : { } + }, { + "id" : "9582787b-2975-437e-a5fe-29b515901555", + "name" : "manage-account-links", + "description" : "${role_manage-account-links}", + "composite" : false, + "clientRole" : true, + "containerId" : "6ea8dcc8-c870-4012-8382-141d26864418", + "attributes" : { } + }, { + "id" : "3b8dca64-75f9-465b-b55e-a34db591874c", + "name" : "view-applications", + "description" : "${role_view-applications}", + "composite" : false, + "clientRole" : true, + "containerId" : "6ea8dcc8-c870-4012-8382-141d26864418", + "attributes" : { } + }, { + "id" : "de3d32e3-4945-45e9-b141-6632cc166480", + "name" : "delete-account", + "description" : "${role_delete-account}", + "composite" : false, + "clientRole" : true, + "containerId" : "6ea8dcc8-c870-4012-8382-141d26864418", + "attributes" : { } + }, { + "id" : "f9b78a08-22ae-45d8-9246-64140df4b284", + "name" : "view-consent", + "description" : "${role_view-consent}", + "composite" : false, + "clientRole" : true, + "containerId" : "6ea8dcc8-c870-4012-8382-141d26864418", + "attributes" : { } + }, { + "id" : "2cda31da-3997-45b5-aca7-a5b143e284bb", + "name" : "view-profile", + "description" : "${role_view-profile}", + "composite" : false, + "clientRole" : true, + "containerId" : "6ea8dcc8-c870-4012-8382-141d26864418", + "attributes" : { } + } ] + } + }, + "groups" : [ ], + "defaultRole" : { + "id" : "71555fc5-bcff-47ec-a2ba-9bdb33a813d5", + "name" : "default-roles-emumet", + "description" : "${role_default-roles}", + "composite" : true, + "clientRole" : false, + "containerId" : "20fdc959-26f7-4a13-92ea-5fcd3380d354" + }, + "requiredCredentials" : [ "password" ], + "otpPolicyType" : "totp", + "otpPolicyAlgorithm" : "HmacSHA1", + "otpPolicyInitialCounter" : 0, + "otpPolicyDigits" : 6, + "otpPolicyLookAheadWindow" : 1, + "otpPolicyPeriod" : 30, + "otpPolicyCodeReusable" : false, + "otpSupportedApplications" : [ "totpAppFreeOTPName", "totpAppGoogleName", "totpAppMicrosoftAuthenticatorName" ], + "localizationTexts" : { }, + "webAuthnPolicyRpEntityName" : "keycloak", + "webAuthnPolicySignatureAlgorithms" : [ "ES256", "RS256" ], + "webAuthnPolicyRpId" : "", + "webAuthnPolicyAttestationConveyancePreference" : "not specified", + "webAuthnPolicyAuthenticatorAttachment" : "not specified", + "webAuthnPolicyRequireResidentKey" : "not specified", + "webAuthnPolicyUserVerificationRequirement" : "not specified", + "webAuthnPolicyCreateTimeout" : 0, + "webAuthnPolicyAvoidSameAuthenticatorRegister" : false, + "webAuthnPolicyAcceptableAaguids" : [ ], + "webAuthnPolicyExtraOrigins" : [ ], + "webAuthnPolicyPasswordlessRpEntityName" : "keycloak", + "webAuthnPolicyPasswordlessSignatureAlgorithms" : [ "ES256", "RS256" ], + "webAuthnPolicyPasswordlessRpId" : "", + "webAuthnPolicyPasswordlessAttestationConveyancePreference" : "not specified", + "webAuthnPolicyPasswordlessAuthenticatorAttachment" : "not specified", + "webAuthnPolicyPasswordlessRequireResidentKey" : "not specified", + "webAuthnPolicyPasswordlessUserVerificationRequirement" : "not specified", + "webAuthnPolicyPasswordlessCreateTimeout" : 0, + "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister" : false, + "webAuthnPolicyPasswordlessAcceptableAaguids" : [ ], + "webAuthnPolicyPasswordlessExtraOrigins" : [ ], + "scopeMappings" : [ { + "clientScope" : "offline_access", + "roles" : [ "offline_access" ] + } ], + "clientScopeMappings" : { + "account" : [ { + "client" : "account-console", + "roles" : [ "manage-account", "view-groups" ] + } ] + }, + "clients" : [ { + "id" : "6ea8dcc8-c870-4012-8382-141d26864418", + "clientId" : "account", + "name" : "${client_account}", + "rootUrl" : "${authBaseUrl}", + "baseUrl" : "/realms/emumet/account/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/realms/emumet/account/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "realm_client" : "false", + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "basic", "email" ], + "optionalClientScopes" : [ "address", "phone", "organization", "offline_access", "microprofile-jwt" ] + }, { + "id" : "cdc2ed5e-d827-41cd-aa86-7a92cda4ddc9", + "clientId" : "account-console", + "name" : "${client_account-console}", + "rootUrl" : "${authBaseUrl}", + "baseUrl" : "/realms/emumet/account/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/realms/emumet/account/*" ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "realm_client" : "false", + "post.logout.redirect.uris" : "+", + "pkce.code.challenge.method" : "S256" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "13a4a708-b03c-47d9-99c1-5dbb0c678142", + "name" : "audience resolve", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-resolve-mapper", + "consentRequired" : false, + "config" : { } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "basic", "email" ], + "optionalClientScopes" : [ "address", "phone", "organization", "offline_access", "microprofile-jwt" ] + }, { + "id" : "7f2b1299-d123-40eb-8882-5f44eb7b020a", + "clientId" : "admin-cli", + "name" : "${client_admin-cli}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : false, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "realm_client" : "false", + "client.use.lightweight.access.token.enabled" : "true", + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "basic", "email" ], + "optionalClientScopes" : [ "address", "phone", "organization", "offline_access", "microprofile-jwt" ] + }, { + "id" : "e233a020-34ea-4e9d-9949-f9d839c4eb64", + "clientId" : "broker", + "name" : "${client_broker}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : true, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "realm_client" : "true", + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "basic", "email" ], + "optionalClientScopes" : [ "address", "phone", "organization", "offline_access", "microprofile-jwt" ] + }, { + "id" : "b4365bfb-8dc3-4956-90c0-ddf520852804", + "clientId" : "myclient", + "name" : "", + "description" : "", + "rootUrl" : "", + "adminUrl" : "", + "baseUrl" : "", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "https://www.keycloak.org/app/*" ], + "webOrigins" : [ "https://www.keycloak.org" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : true, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : true, + "protocol" : "openid-connect", + "attributes" : { + "realm_client" : "false", + "oidc.ciba.grant.enabled" : "false", + "backchannel.logout.session.required" : "true", + "post.logout.redirect.uris" : "+", + "oauth2.device.authorization.grant.enabled" : "false", + "backchannel.logout.revoke.offline.tokens" : "false" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : -1, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "basic", "email" ], + "optionalClientScopes" : [ "address", "phone", "organization", "offline_access", "microprofile-jwt" ] + }, { + "id" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", + "clientId" : "realm-management", + "name" : "${client_realm-management}", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ ], + "webOrigins" : [ ], + "notBefore" : 0, + "bearerOnly" : true, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : false, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "realm_client" : "true", + "post.logout.redirect.uris" : "+" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : false, + "nodeReRegistrationTimeout" : 0, + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "basic", "email" ], + "optionalClientScopes" : [ "address", "phone", "organization", "offline_access", "microprofile-jwt" ] + }, { + "id" : "d759db8d-3c2d-47c1-8ebc-2d3f1116db8d", + "clientId" : "security-admin-console", + "name" : "${client_security-admin-console}", + "rootUrl" : "${authAdminUrl}", + "baseUrl" : "/admin/emumet/console/", + "surrogateAuthRequired" : false, + "enabled" : true, + "alwaysDisplayInConsole" : false, + "clientAuthenticatorType" : "client-secret", + "redirectUris" : [ "/admin/emumet/console/*" ], + "webOrigins" : [ "+" ], + "notBefore" : 0, + "bearerOnly" : false, + "consentRequired" : false, + "standardFlowEnabled" : true, + "implicitFlowEnabled" : false, + "directAccessGrantsEnabled" : false, + "serviceAccountsEnabled" : false, + "publicClient" : true, + "frontchannelLogout" : false, + "protocol" : "openid-connect", + "attributes" : { + "realm_client" : "false", + "client.use.lightweight.access.token.enabled" : "true", + "post.logout.redirect.uris" : "+", + "pkce.code.challenge.method" : "S256" + }, + "authenticationFlowBindingOverrides" : { }, + "fullScopeAllowed" : true, + "nodeReRegistrationTimeout" : 0, + "protocolMappers" : [ { + "id" : "45ee8535-98e7-48c3-96f9-501090eb4dab", + "name" : "locale", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "locale", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "locale", + "jsonType.label" : "String" + } + } ], + "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "basic", "email" ], + "optionalClientScopes" : [ "address", "phone", "organization", "offline_access", "microprofile-jwt" ] + } ], + "clientScopes" : [ { + "id" : "8ae530d6-82c4-4fd6-a737-1b3e2be2cb18", + "name" : "roles", + "description" : "OpenID Connect scope for add user roles to the access token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "consent.screen.text" : "${rolesScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "e40bd2e4-9d87-49d1-9e77-87017a7fe276", + "name" : "audience resolve", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-audience-resolve-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "access.token.claim" : "true" + } + }, { + "id" : "5160d261-4863-4e66-ae7f-2a1a0c827c1e", + "name" : "realm roles", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-realm-role-mapper", + "consentRequired" : false, + "config" : { + "user.attribute" : "foo", + "introspection.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "realm_access.roles", + "jsonType.label" : "String", + "multivalued" : "true" + } + }, { + "id" : "33ffe511-24c4-4c92-9753-de100289e281", + "name" : "client roles", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-client-role-mapper", + "consentRequired" : false, + "config" : { + "user.attribute" : "foo", + "introspection.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "resource_access.${client_id}.roles", + "jsonType.label" : "String", + "multivalued" : "true" + } + } ] + }, { + "id" : "a6a9f309-bff4-42fc-849a-dfefc92c14a3", + "name" : "role_list", + "description" : "SAML role list", + "protocol" : "saml", + "attributes" : { + "consent.screen.text" : "${samlRoleListScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "aff40860-4fcf-4e28-b1a7-e76ae04a8e65", + "name" : "role list", + "protocol" : "saml", + "protocolMapper" : "saml-role-list-mapper", + "consentRequired" : false, + "config" : { + "single" : "false", + "attribute.nameformat" : "Basic", + "attribute.name" : "Role" + } + } ] + }, { + "id" : "006963a9-566d-4d4b-bfd1-bcc8e5b3be97", + "name" : "email", + "description" : "OpenID Connect built-in scope: email", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "consent.screen.text" : "${emailScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "a3e00a31-6f3c-4f8d-ac15-10d9f7d4a34b", + "name" : "email verified", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-property-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "emailVerified", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email_verified", + "jsonType.label" : "boolean" + } + }, { + "id" : "6567d0f4-8069-47db-bffa-327113cdb35a", + "name" : "email", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "email", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "email", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "91894ed6-98fe-45b5-8245-b8a6a2c8fc15", + "name" : "web-origins", + "description" : "OpenID Connect scope for add allowed web origins to the access token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "consent.screen.text" : "", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "2fe3bfc2-0c05-4202-959f-28d5442f2576", + "name" : "allowed web origins", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-allowed-origins-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "access.token.claim" : "true" + } + } ] + }, { + "id" : "12cb62b5-7a31-4aa9-af58-05d58387447c", + "name" : "organization", + "description" : "Additional claims about the organization a subject belongs to", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "consent.screen.text" : "${organizationScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "27d37db0-6df0-46a3-ad1a-9bf7068f1f29", + "name" : "organization", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-organization-membership-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "multivalued" : "true", + "userinfo.token.claim" : "true", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "organization", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "7ab385a7-8fa1-4016-a41b-773db5c7ded8", + "name" : "address", + "description" : "OpenID Connect built-in scope: address", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "consent.screen.text" : "${addressScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "b225fb58-ceec-4bb9-89c4-e074c5a849c2", + "name" : "address", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-address-mapper", + "consentRequired" : false, + "config" : { + "user.attribute.formatted" : "formatted", + "user.attribute.country" : "country", + "introspection.token.claim" : "true", + "user.attribute.postal_code" : "postal_code", + "userinfo.token.claim" : "true", + "user.attribute.street" : "street", + "id.token.claim" : "true", + "user.attribute.region" : "region", + "access.token.claim" : "true", + "user.attribute.locality" : "locality" + } + } ] + }, { + "id" : "4d0d4f4a-1124-465d-88c9-8370febbc667", + "name" : "profile", + "description" : "OpenID Connect built-in scope: profile", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "consent.screen.text" : "${profileScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "5ddba364-ad48-4477-8886-7476f6fdaa31", + "name" : "birthdate", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "birthdate", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "birthdate", + "jsonType.label" : "String" + } + }, { + "id" : "a6cf2b69-4028-4866-8205-376f3cdfe6b3", + "name" : "zoneinfo", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "zoneinfo", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "zoneinfo", + "jsonType.label" : "String" + } + }, { + "id" : "5bb144fd-d7cc-4f33-be28-7fa9785dfce2", + "name" : "given name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "firstName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "given_name", + "jsonType.label" : "String" + } + }, { + "id" : "4d821a65-224b-43a7-b37e-4aa2eff1f3e4", + "name" : "profile", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "profile", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "profile", + "jsonType.label" : "String" + } + }, { + "id" : "c68aff47-70b3-48aa-bd4d-9f8b4526849e", + "name" : "picture", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "picture", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "picture", + "jsonType.label" : "String" + } + }, { + "id" : "49a5a153-ff85-4764-92c3-2ba9e64c9985", + "name" : "full name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-full-name-mapper", + "consentRequired" : false, + "config" : { + "id.token.claim" : "true", + "introspection.token.claim" : "true", + "access.token.claim" : "true", + "userinfo.token.claim" : "true" + } + }, { + "id" : "0de79ae3-e2d9-4ec0-99fc-3b9ca32d8acf", + "name" : "gender", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "gender", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "gender", + "jsonType.label" : "String" + } + }, { + "id" : "ba9c17a3-4785-4ae1-b508-9a57cbdeffe5", + "name" : "family name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "lastName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "family_name", + "jsonType.label" : "String" + } + }, { + "id" : "8aa3b2f6-962a-4407-84ee-223a27de80cb", + "name" : "middle name", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "middleName", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "middle_name", + "jsonType.label" : "String" + } + }, { + "id" : "b5414967-f28d-4f27-984e-06f93e6c6107", + "name" : "locale", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "locale", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "locale", + "jsonType.label" : "String" + } + }, { + "id" : "750c86fa-2a62-4b69-9cb2-577b834e386c", + "name" : "website", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "website", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "website", + "jsonType.label" : "String" + } + }, { + "id" : "6131f323-699a-4bb7-97a6-d1f0a0a3c854", + "name" : "username", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "preferred_username", + "jsonType.label" : "String" + } + }, { + "id" : "9626048b-ead5-45d6-b9e5-aef75d9d0c2b", + "name" : "nickname", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "nickname", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "nickname", + "jsonType.label" : "String" + } + }, { + "id" : "298e2913-a90d-4d10-869c-0c5aff7034e6", + "name" : "updated at", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "updatedAt", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "updated_at", + "jsonType.label" : "long" + } + } ] + }, { + "id" : "8eec08ca-9335-4e7e-8a86-55c60d0df7de", + "name" : "basic", + "description" : "OpenID Connect scope for add all basic claims to the token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "9d074af9-43c9-4abf-a8f4-236e388c19cd", + "name" : "auth_time", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usersessionmodel-note-mapper", + "consentRequired" : false, + "config" : { + "user.session.note" : "AUTH_TIME", + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "auth_time", + "jsonType.label" : "long" + } + }, { + "id" : "558fe31c-4ef1-490a-9461-6b8250549c3a", + "name" : "sub", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-sub-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "access.token.claim" : "true" + } + } ] + }, { + "id" : "dce02b5b-e49a-4143-afd8-4df702f427a5", + "name" : "phone", + "description" : "OpenID Connect built-in scope: phone", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "consent.screen.text" : "${phoneScopeConsentText}", + "display.on.consent.screen" : "true" + }, + "protocolMappers" : [ { + "id" : "640cf110-1a95-4b8a-8c77-f18c55ffc897", + "name" : "phone number", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "phoneNumber", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "phone_number", + "jsonType.label" : "String" + } + }, { + "id" : "aa261d25-9b27-4db2-93cf-a9658b78f2fb", + "name" : "phone number verified", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "phoneNumberVerified", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "phone_number_verified", + "jsonType.label" : "boolean" + } + } ] + }, { + "id" : "2d33305a-1ce2-42c6-ac0b-080b3dc1ad2f", + "name" : "microprofile-jwt", + "description" : "Microprofile - JWT built-in scope", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "true", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "5199c6a2-b77b-42d9-a347-1334a861737a", + "name" : "groups", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-realm-role-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "multivalued" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "foo", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "groups", + "jsonType.label" : "String" + } + }, { + "id" : "06e64f62-2ecc-484c-9eb7-e33758a390ca", + "name" : "upn", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usermodel-attribute-mapper", + "consentRequired" : false, + "config" : { + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "user.attribute" : "username", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "upn", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "c1412c8d-2c8f-4b18-9176-fde822cf836b", + "name" : "acr", + "description" : "OpenID Connect scope for add acr (authentication context class reference) to the token", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "584060c1-e826-4a77-a24a-f52fab27ada0", + "name" : "acr loa level", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-acr-mapper", + "consentRequired" : false, + "config" : { + "id.token.claim" : "true", + "introspection.token.claim" : "true", + "access.token.claim" : "true", + "userinfo.token.claim" : "true" + } + } ] + }, { + "id" : "aeee216c-ab51-43a7-9e7d-3431061c9821", + "name" : "service_account", + "description" : "Specific scope for a client enabled for service accounts", + "protocol" : "openid-connect", + "attributes" : { + "include.in.token.scope" : "false", + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "fe492866-50e2-4872-b30a-2f605892d4f1", + "name" : "Client IP Address", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usersessionmodel-note-mapper", + "consentRequired" : false, + "config" : { + "user.session.note" : "clientAddress", + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "clientAddress", + "jsonType.label" : "String" + } + }, { + "id" : "535bee5a-aa97-40fe-b053-e91581100767", + "name" : "Client Host", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usersessionmodel-note-mapper", + "consentRequired" : false, + "config" : { + "user.session.note" : "clientHost", + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "clientHost", + "jsonType.label" : "String" + } + }, { + "id" : "8177487b-fd50-42dd-a8b6-9a55d8886b2a", + "name" : "Client ID", + "protocol" : "openid-connect", + "protocolMapper" : "oidc-usersessionmodel-note-mapper", + "consentRequired" : false, + "config" : { + "user.session.note" : "client_id", + "introspection.token.claim" : "true", + "userinfo.token.claim" : "true", + "id.token.claim" : "true", + "access.token.claim" : "true", + "claim.name" : "client_id", + "jsonType.label" : "String" + } + } ] + }, { + "id" : "425e9994-1df2-49ff-9987-e2cd616ed41a", + "name" : "offline_access", + "description" : "OpenID Connect built-in scope: offline_access", + "protocol" : "openid-connect", + "attributes" : { + "consent.screen.text" : "${offlineAccessScopeConsentText}", + "display.on.consent.screen" : "true" + } + }, { + "id" : "690650b8-a502-4cc8-a107-8ed4c6096a92", + "name" : "saml_organization", + "description" : "Organization Membership", + "protocol" : "saml", + "attributes" : { + "display.on.consent.screen" : "false" + }, + "protocolMappers" : [ { + "id" : "d6292315-8182-4a9f-98f8-a75af0e1dfe3", + "name" : "organization", + "protocol" : "saml", + "protocolMapper" : "saml-organization-membership-mapper", + "consentRequired" : false, + "config" : { } + } ] + } ], + "defaultDefaultClientScopes" : [ "role_list", "saml_organization", "profile", "email", "roles", "web-origins", "acr", "basic" ], + "defaultOptionalClientScopes" : [ "offline_access", "address", "phone", "microprofile-jwt", "organization" ], + "browserSecurityHeaders" : { + "contentSecurityPolicyReportOnly" : "", + "xContentTypeOptions" : "nosniff", + "referrerPolicy" : "no-referrer", + "xRobotsTag" : "none", + "xFrameOptions" : "SAMEORIGIN", + "contentSecurityPolicy" : "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", + "xXSSProtection" : "1; mode=block", + "strictTransportSecurity" : "max-age=31536000; includeSubDomains" + }, + "smtpServer" : { }, + "eventsEnabled" : false, + "eventsListeners" : [ "jboss-logging" ], + "enabledEventTypes" : [ ], + "adminEventsEnabled" : false, + "adminEventsDetailsEnabled" : false, + "identityProviders" : [ ], + "identityProviderMappers" : [ ], + "components" : { + "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy" : [ { + "id" : "300fbe82-c8ae-4e9b-bb69-29595ab1af15", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "saml-user-attribute-mapper", "oidc-usermodel-property-mapper", "saml-user-property-mapper", "oidc-usermodel-attribute-mapper", "oidc-full-name-mapper", "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-role-list-mapper" ] + } + }, { + "id" : "62112051-56c0-493e-85bb-39e51c772d60", + "name" : "Trusted Hosts", + "providerId" : "trusted-hosts", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "host-sending-registration-request-must-match" : [ "true" ], + "client-uris-must-match" : [ "true" ] + } + }, { + "id" : "40069870-8ee9-4ba8-ad77-a4901da5cd91", + "name" : "Allowed Protocol Mapper Types", + "providerId" : "allowed-protocol-mappers", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allowed-protocol-mapper-types" : [ "oidc-usermodel-attribute-mapper", "saml-role-list-mapper", "saml-user-property-mapper", "oidc-full-name-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-property-mapper", "saml-user-attribute-mapper", "oidc-address-mapper" ] + } + }, { + "id" : "965d7cea-339e-43e7-9815-458964c34557", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "authenticated", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + }, { + "id" : "065d1d40-0ca7-432b-8555-00dd4481b887", + "name" : "Allowed Client Scopes", + "providerId" : "allowed-client-templates", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "allow-default-scopes" : [ "true" ] + } + }, { + "id" : "2ba17e54-23f1-4eb6-adab-970864799fa0", + "name" : "Max Clients Limit", + "providerId" : "max-clients", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { + "max-clients" : [ "200" ] + } + }, { + "id" : "d6b88488-c61f-417b-bed9-5c01a31966af", + "name" : "Consent Required", + "providerId" : "consent-required", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + }, { + "id" : "6d1b1fda-3811-417d-a23e-5bb86ed7b938", + "name" : "Full Scope Disabled", + "providerId" : "scope", + "subType" : "anonymous", + "subComponents" : { }, + "config" : { } + } ], + "org.keycloak.keys.KeyProvider" : [ { + "id" : "8b6d52a5-9538-4e62-a5b9-e770493e3c44", + "name" : "aes-generated", + "providerId" : "aes-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "d8c544dc-de0d-4ac0-a736-64dbddd49f03" ], + "secret" : [ "uewDe_LI6Fv8kdHNpiGBYQ" ], + "priority" : [ "100" ] + } + }, { + "id" : "a23d66ed-50e0-4b96-9e50-3dc3f7aa9063", + "name" : "hmac-generated-hs512", + "providerId" : "hmac-generated", + "subComponents" : { }, + "config" : { + "kid" : [ "6335f7e5-2b49-421e-83ed-56250fc5426a" ], + "secret" : [ "UeQ65_Pp9_p_nYNwlEc682v3TqL0IH4Y6zs_tiVM8PyOAQTlDpavdjn8KU_Du6rfizPRKXPkoKIcJI5LZyUDAnwv9zBpxIRTHqjPArUAbqPNBjlCmXtuHB-5KdLfxghY-Mu2w1gWQvYpTtPVZSRolUhy6TSXC72tf9pHpRwJs74" ], + "priority" : [ "100" ], + "algorithm" : [ "HS512" ] + } + }, { + "id" : "7c333ba3-85a4-4dfe-88e0-dc3ed71b29ac", + "name" : "rsa-enc-generated", + "providerId" : "rsa-enc-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEogIBAAKCAQEApfy3E12+UJx3l55drUkINQOmvhp61x2sYTZd2J2LiJLawsrWA+tdWQ6cODISjHfPIeMlEHeWU1jtnGxhOl5TQ5to+Srnt9c4Y7Nf1mhmHyuCvEMXaio8Hw1uEFnWGZp6QR56M7Ur9jxWwr0R2WAeOx1S3a4hlb87ussPN/8eS7ecjBP9BW/sz/x2c91ZPxnsAB2zl4RHGomi1a0ElEHIZVkzGV4bPsI0HXvDqB/NyutncXrNSbhev32Kq7L1mO/3zJ/9XBnP5h5o+XagoiiT0qX2gc6638MDFiAx1OVNpg9U6gdHTl3UsNcPBQG0uZxkVDtozd9z1f8kLBBpOJEP4wIDAQABAoIBAAkWs92TGfsm/iNmsAFviMwCVax+HbDOtqQiCnR0d/Hz/JeC7MINLrDULHilQT/Axa441kw3CBurOGOCybYc+RkwFsjh8QsvdS58YWiHkePuCXwOfmc5Rc57eUXa7W68dyo+pXlUV8JlXmjOWn5ZFX6uJd3ujXc6H+aj/MLXrMx/fDFEWajk8Qvgbi4/Fb7qh96sMa2OBKyaXPBgpDWT+vnr7EsC4j+eIfgwAtuaEOCv63ez3pqp6CP5gy3uG6w4UIhlix1BMHcgxjlHwND6Gn+J8nSQAl1kAgqavNKKOqeKjR1xBDxlwEKqpVRYkoCIoznCbHReizCaAmx10OREvCUCgYEA18vh+3+z8UvfsF4B+9ny5fJuVLHp06220ha9z6hjurZE9wPAzGMNbKSbagO+K1yw0N+IfcKr9V9hPuvJz8gR3BPeZHjvFDnhLJr6Ihgqu5+3qtGUw0bLoYjPV11Daqx8H3GkUQC/gb7Ykb2phwKzczZw7Pvpkj4gEXVIumOpX70CgYEAxOk/dWUpK8o0YgUSwpbkQ/8y2++4LtLxT1HZ8QbR7ATV8MFQmZ8TlR6iSDERPKW+u4z249/mgkf8roXI3AcoUYgrP97mjZCvbKlk1ql1Mko6GACp5zt8U31+jR20tMyd5gjo9bP0hqLixkJSclXFnE04gkFkYZCm5E9kiERT2B8CgYB3WUWUqR5GN+ZxTqzeM75JOvmWUge2kP7p1rYH4WO24hPmYecBo07LZYam7YcByHPqMZb1pvMf9C5+dD3bcxWdmEeJXfEsSI6m8tegf6kyt7UG/n6+OatpnZa/BM/Ccb78TQfJ3RYNlhWFFVZrWy0QbW2rQ+/8d+uYfDtLCs+kKQKBgE5O8FSwgVoP1RsyJ07JkUfVYpWC1P2SGDNSOtkWvD8fgTF4v6QIVlJUV3dcRB2ZUKvnmHvxHAutszh4rfOKySb7fy+sZoXgB1OwXhDcXWY9jLLk+KyjxIKzgrN+H9JTGWxVGMg148XzWzo7P+yGXcsWDqYGeXQvgZ+ET1e9zJZDAoGABCE+rU9gaT/UL2ftqPbdX2azeAP9++/BtmZ14WwCo0Y0VJaWU0va+oKtWZlDibRJcBJJRDH1OCYovZirqy8bTm4PFLn6hMosfWnVlksvoYZZ3mNV7n8vtyjjtLnERxCv34I3tOc/8+rdTZ2zgztnsGtou/CL+WeYiRXLtyn5FnM=" ], + "certificate" : [ "MIICmzCCAYMCBgGU3jgG7DANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZlbXVtZXQwHhcNMjUwMjA3MDIyMTQxWhcNMzUwMjA3MDIyMzIxWjARMQ8wDQYDVQQDDAZlbXVtZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCl/LcTXb5QnHeXnl2tSQg1A6a+GnrXHaxhNl3YnYuIktrCytYD611ZDpw4MhKMd88h4yUQd5ZTWO2cbGE6XlNDm2j5Kue31zhjs1/WaGYfK4K8QxdqKjwfDW4QWdYZmnpBHnoztSv2PFbCvRHZYB47HVLdriGVvzu6yw83/x5Lt5yME/0Fb+zP/HZz3Vk/GewAHbOXhEcaiaLVrQSUQchlWTMZXhs+wjQde8OoH83K62dxes1JuF6/fYqrsvWY7/fMn/1cGc/mHmj5dqCiKJPSpfaBzrrfwwMWIDHU5U2mD1TqB0dOXdSw1w8FAbS5nGRUO2jN33PV/yQsEGk4kQ/jAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAIR1JWnpHseNHZmNzZHfHndy4lI/MtibamT4wle9NkOxQ/DxoJpSjs9u+X/In16hbp7OQoXC4JPenAy4zpfhHoYoAytfv6N2UwP0IYdJpbqwn7xwfVzBHoCFXg8v7eLE2GZ0qjNtRgsdSHOmqvokWhqW7/xKL+9MAu2nGKtBPOjYJOKWqWa3N1A/S0s4pWYy8vG3OmS8bCDGY/B3jwc4zf3ABfBfVRSJSRhwXymc9216kTjyMe6Lf7p0Mmse4dFuYst2OFn2twHcFui7eeNu9NMu9lxhN3DbUcmNTAl87nEqzDMX+WqUIMf45EjRTHqI69npfI++QalYH+ed/HTZweA=" ], + "priority" : [ "100" ], + "algorithm" : [ "RSA-OAEP" ] + } + }, { + "id" : "2c913445-586f-4591-a661-3bf7cbc55f27", + "name" : "rsa-generated", + "providerId" : "rsa-generated", + "subComponents" : { }, + "config" : { + "privateKey" : [ "MIIEowIBAAKCAQEAos3UsvHode88kEZEXlBPpRqGXYIpvHjDyDrOfW+9wV1PDONQzbb2twEcQ9KsKUqVED48P/+hwq24LA5gSh2j24xX7OuNTP69Fdf1OOnfvya/seAYDreqU5DnmsK9qJFYuijuqoCT4VpdcJy+N0pJ7IKWC6iAjfWGoWT3eyI/VgUn5nEpSbjsZ+Cg3kSrCg6Ss0l0tt5LPIvPoQk7uzP+OmXkLUmliXZXb4OP3JTctciwL6uloSA1CRkct5VsYjgzsCKmtla47WHAIdlC83NHfjkBJMMCbzkVYmzap52URf2hfj2nYsGJ/iKFrbuC/hWtRS/e45Uh5RUAKHdiipwAFQIDAQABAoIBADwCfN1897/I8F0J2ZeeKM1l6pM7MGEtbpU2v/hSoPJOj53jiFxbjbNFMIL7e8Q4npt/JTw94QVefV2X6vxG0qhRofNNnCb+WvpbQSO6aWQPR2esf5GlN55X8lcEY15oPPlZryef/2J4qaqhzCebNYZ9WAtyD/jDwN1q1yJHLGtrIYnS4cEt6iLLsC4SiZ8ofwSzS5k+yklDp0PhDBmv4oiNrWbZfDlh2HWHYPe2TnSsk5YDGu7XWTQ6zOTthuIv4lcijfFuQpO/SJTYgxF16qNRaIxiG6mljWOssE4o03XMDURIbYMR0BHuM8z+VYiy8v7nz50ehaCwLsANr4lpJF0CgYEA5ek+2YIS7Dzffyx0mNkJV9PVqhe4hHOEpCcBTMKatV3ERljmVZZH9T436Eolja7hTAULpRDy+cqC8opghBQCpW33PGb8w/9/TEr9L1N5xQWm3Ifi0lRkCTv34kTBh8vQEk8nCubbivOXFK5a+C/tjQobcP4BcVrfD66VMddyFgcCgYEAtUcsEE0X+3n3krUJkJxDqDQcdKPDjgEOijuDkqkTk1tEbz+tN6bQ5T71DD7swh1DkG+bO+X1jXEix5I6ByhLXAQqaF7tZcJSBJDaRql4U7aIdV3jhozIfT4etUZZ8EgfDQYZ3egdc1710gOgDp1ciHJTZ16bk6OCMXTdy/Ms0gMCgYABr/mPHR5Ib5XwWAIvEQC5jUt3KR9okXR6w/KFfrQl+p8zKPnfzO+QRDmi0dB+vrbWmP7h4kL2RF87qnpU3dS7JBh5cAQQ6DIl/DLpgwJUyNrVqYWnp4jobHFATuLgvUU0rTILKXCZD3qfYzw1sBxdOaLD7IlULKeQdOaRbBRhRwKBgHIOD6lJ+DbfLGd/xD7aMq9X6jdw+g8UlyNeApB6FLj4CXy9YazMJk62Z9OGm8weQW5U6iSrsO2HK0zJsfzi21dPv6bfYxpNQvFgehVPd0ekZwMBSbBUT6iNNyDy3I+TsQWuuwOlkTIPoza51TCczaWD2PoGynf/vmCDmTFDFQYlAoGBANHhlyDwQt4B3bJERpdP1G8kLt1Xk0BNMQ02iiGG8n0/J4hsJ1OUvCl+okDvlfO4RgXxiL6JicfxSd6wDde9iVYGFtcT0Q6hIk7GMrLeRcQ/tlQnsnaA92wg2vamiDhjOZUjRQ45oApBR5WZYRYvGXv14y7aK4ZfJKom9ns+T7f5" ], + "certificate" : [ "MIICmzCCAYMCBgGU3jgHijANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZlbXVtZXQwHhcNMjUwMjA3MDIyMTQxWhcNMzUwMjA3MDIyMzIxWjARMQ8wDQYDVQQDDAZlbXVtZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCizdSy8eh17zyQRkReUE+lGoZdgim8eMPIOs59b73BXU8M41DNtva3ARxD0qwpSpUQPjw//6HCrbgsDmBKHaPbjFfs641M/r0V1/U46d+/Jr+x4BgOt6pTkOeawr2okVi6KO6qgJPhWl1wnL43SknsgpYLqICN9YahZPd7Ij9WBSfmcSlJuOxn4KDeRKsKDpKzSXS23ks8i8+hCTu7M/46ZeQtSaWJdldvg4/clNy1yLAvq6WhIDUJGRy3lWxiODOwIqa2VrjtYcAh2ULzc0d+OQEkwwJvORVibNqnnZRF/aF+PadiwYn+IoWtu4L+Fa1FL97jlSHlFQAod2KKnAAVAgMBAAEwDQYJKoZIhvcNAQELBQADggEBADCADOwdBJA3bkb9X6M3mRQA6kH+OeMDNNq/ZVdw3vv+n7DbAkLY+r3s30cShP9GH+CHajyAdFhhUvyhbEcfwY3e5aJnfrwU/uqPmg7pjF60JbYfVFC+9E930KKfBkz5Hx1X9wFvy1tYanojf9LGYfXzC5DcLvbYU5oax/8lsuZEQ9Rgw6HNdiodRm+Fb8OvfvXJlev6xBelCfZ61R1s9HqDJXCkRiOi0/FeMT17t1H9UEnPR0LbhqQe86Ra2u43YNfxx/qof3dfsYQrnW+xMnkdpWoF5oBLJjE03OyI3n2VQW+OdPhNt4/NooKFSM9TJejnwSZ7II3LxCyjA4adfaw=" ], + "priority" : [ "100" ] + } + } ] + }, + "internationalizationEnabled" : false, + "supportedLocales" : [ ], + "authenticationFlows" : [ { + "id" : "ac47d516-4b49-4281-9730-19b44b9a0d7e", + "alias" : "Account verification options", + "description" : "Method with which to verity the existing account", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-email-verification", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Verify Existing Account by Re-authentication", + "userSetupAllowed" : false + } ] + }, { + "id" : "afab1457-54b4-4767-a435-2c134a621e5c", + "alias" : "Browser - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-otp-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "f2674436-a31c-4b9b-aa36-da5f8713f1c2", + "alias" : "Browser - Conditional Organization", + "description" : "Flow to determine if the organization identity-first login is to be used", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "organization", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "ec701c44-c425-4c31-9fdb-b0500dc49184", + "alias" : "Direct Grant - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "direct-grant-validate-otp", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "68e873db-9952-441d-8c18-ebe26575d3a0", + "alias" : "First Broker Login - Conditional Organization", + "description" : "Flow to determine if the authenticator that adds organization members is to be used", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "idp-add-organization-member", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "bb675959-4199-4a6b-81ef-7cac6931a3a2", + "alias" : "First broker login - Conditional OTP", + "description" : "Flow to determine if the OTP is required for the authentication", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-otp-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "35f32124-f039-4b95-856b-f48889dd0bf8", + "alias" : "Handle Existing Account", + "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-confirm-link", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Account verification options", + "userSetupAllowed" : false + } ] + }, { + "id" : "585121a6-1f43-433b-877f-86ad6792d590", + "alias" : "Organization", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 10, + "autheticatorFlow" : true, + "flowAlias" : "Browser - Conditional Organization", + "userSetupAllowed" : false + } ] + }, { + "id" : "3a7b6fba-6b57-42f1-8d64-bf34da9d1768", + "alias" : "Reset - Conditional OTP", + "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "conditional-user-configured", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-otp", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "c4a91189-6108-4547-b919-4e4d7ca70cec", + "alias" : "User creation or linking", + "description" : "Flow for the existing/non-existing user alternatives", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorConfig" : "create unique user config", + "authenticator" : "idp-create-user-if-unique", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Handle Existing Account", + "userSetupAllowed" : false + } ] + }, { + "id" : "a0a95326-920c-47bf-b401-ccfd5c9503fe", + "alias" : "Verify Existing Account by Re-authentication", + "description" : "Reauthentication of existing account", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "idp-username-password-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "First broker login - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "b35f76dc-eb0b-4ca0-aa48-6615ad1dc494", + "alias" : "browser", + "description" : "Browser based authentication", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-cookie", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "auth-spnego", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "identity-provider-redirector", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 25, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 26, + "autheticatorFlow" : true, + "flowAlias" : "Organization", + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "ALTERNATIVE", + "priority" : 30, + "autheticatorFlow" : true, + "flowAlias" : "forms", + "userSetupAllowed" : false + } ] + }, { + "id" : "1489b0e6-310a-465b-a184-3f77ed84fb99", + "alias" : "clients", + "description" : "Base authentication for clients", + "providerId" : "client-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "client-secret", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-jwt", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-secret-jwt", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 30, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "client-x509", + "authenticatorFlow" : false, + "requirement" : "ALTERNATIVE", + "priority" : 40, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "b9e4d292-4c18-4584-9698-c9f18b3f701b", + "alias" : "direct grant", + "description" : "OpenID Connect Resource Owner Grant", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "direct-grant-validate-username", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "direct-grant-validate-password", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 30, + "autheticatorFlow" : true, + "flowAlias" : "Direct Grant - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "8411190c-ec3f-41e6-94d3-418c8ebee96e", + "alias" : "docker auth", + "description" : "Used by Docker clients to authenticate against the IDP", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "docker-http-basic-authenticator", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "56a8da93-3260-446e-8649-40898020aa8a", + "alias" : "first broker login", + "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticatorConfig" : "review profile config", + "authenticator" : "idp-review-profile", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "User creation or linking", + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 50, + "autheticatorFlow" : true, + "flowAlias" : "First Broker Login - Conditional Organization", + "userSetupAllowed" : false + } ] + }, { + "id" : "7eedc13c-788d-479e-95ec-bec0ccb6cba8", + "alias" : "forms", + "description" : "Username, password, otp and other auth forms.", + "providerId" : "basic-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "auth-username-password-form", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 20, + "autheticatorFlow" : true, + "flowAlias" : "Browser - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "60abd1c3-362a-4130-a4a1-b1bde7590a60", + "alias" : "registration", + "description" : "Registration flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-page-form", + "authenticatorFlow" : true, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : true, + "flowAlias" : "registration form", + "userSetupAllowed" : false + } ] + }, { + "id" : "336780bc-8dad-4fab-afb1-6fceb4b61ddf", + "alias" : "registration form", + "description" : "Registration form", + "providerId" : "form-flow", + "topLevel" : false, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "registration-user-creation", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-password-action", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 50, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-recaptcha-action", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 60, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "registration-terms-and-conditions", + "authenticatorFlow" : false, + "requirement" : "DISABLED", + "priority" : 70, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + }, { + "id" : "de415055-a2d6-4318-853b-f3b7789bd3db", + "alias" : "reset credentials", + "description" : "Reset credentials for a user if they forgot their password or something", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "reset-credentials-choose-user", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-credential-email", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 20, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticator" : "reset-password", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 30, + "autheticatorFlow" : false, + "userSetupAllowed" : false + }, { + "authenticatorFlow" : true, + "requirement" : "CONDITIONAL", + "priority" : 40, + "autheticatorFlow" : true, + "flowAlias" : "Reset - Conditional OTP", + "userSetupAllowed" : false + } ] + }, { + "id" : "80c9305b-4d24-4e26-bb97-23073d551228", + "alias" : "saml ecp", + "description" : "SAML ECP Profile Authentication Flow", + "providerId" : "basic-flow", + "topLevel" : true, + "builtIn" : true, + "authenticationExecutions" : [ { + "authenticator" : "http-basic-authenticator", + "authenticatorFlow" : false, + "requirement" : "REQUIRED", + "priority" : 10, + "autheticatorFlow" : false, + "userSetupAllowed" : false + } ] + } ], + "authenticatorConfig" : [ { + "id" : "b88fa7f2-e24a-4168-b00b-bc1b8d4b4ef1", + "alias" : "create unique user config", + "config" : { + "require.password.update.after.registration" : "false" + } + }, { + "id" : "8c26db6a-22ae-4a6b-8544-e74525911ce0", + "alias" : "review profile config", + "config" : { + "update.profile.on.first.login" : "missing" + } + } ], + "requiredActions" : [ { + "alias" : "CONFIGURE_TOTP", + "name" : "Configure OTP", + "providerId" : "CONFIGURE_TOTP", + "enabled" : true, + "defaultAction" : false, + "priority" : 10, + "config" : { } + }, { + "alias" : "TERMS_AND_CONDITIONS", + "name" : "Terms and Conditions", + "providerId" : "TERMS_AND_CONDITIONS", + "enabled" : false, + "defaultAction" : false, + "priority" : 20, + "config" : { } + }, { + "alias" : "UPDATE_PASSWORD", + "name" : "Update Password", + "providerId" : "UPDATE_PASSWORD", + "enabled" : true, + "defaultAction" : false, + "priority" : 30, + "config" : { } + }, { + "alias" : "UPDATE_PROFILE", + "name" : "Update Profile", + "providerId" : "UPDATE_PROFILE", + "enabled" : true, + "defaultAction" : false, + "priority" : 40, + "config" : { } + }, { + "alias" : "VERIFY_EMAIL", + "name" : "Verify Email", + "providerId" : "VERIFY_EMAIL", + "enabled" : true, + "defaultAction" : false, + "priority" : 50, + "config" : { } + }, { + "alias" : "delete_account", + "name" : "Delete Account", + "providerId" : "delete_account", + "enabled" : false, + "defaultAction" : false, + "priority" : 60, + "config" : { } + }, { + "alias" : "webauthn-register", + "name" : "Webauthn Register", + "providerId" : "webauthn-register", + "enabled" : true, + "defaultAction" : false, + "priority" : 70, + "config" : { } + }, { + "alias" : "webauthn-register-passwordless", + "name" : "Webauthn Register Passwordless", + "providerId" : "webauthn-register-passwordless", + "enabled" : true, + "defaultAction" : false, + "priority" : 80, + "config" : { } + }, { + "alias" : "VERIFY_PROFILE", + "name" : "Verify Profile", + "providerId" : "VERIFY_PROFILE", + "enabled" : true, + "defaultAction" : false, + "priority" : 90, + "config" : { } + }, { + "alias" : "delete_credential", + "name" : "Delete Credential", + "providerId" : "delete_credential", + "enabled" : true, + "defaultAction" : false, + "priority" : 100, + "config" : { } + }, { + "alias" : "update_user_locale", + "name" : "Update User Locale", + "providerId" : "update_user_locale", + "enabled" : true, + "defaultAction" : false, + "priority" : 1000, + "config" : { } + } ], + "browserFlow" : "browser", + "registrationFlow" : "registration", + "directGrantFlow" : "direct grant", + "resetCredentialsFlow" : "reset credentials", + "clientAuthenticationFlow" : "clients", + "dockerAuthenticationFlow" : "docker auth", + "firstBrokerLoginFlow" : "first broker login", + "attributes" : { + "cibaBackchannelTokenDeliveryMode" : "poll", + "cibaExpiresIn" : "120", + "cibaAuthRequestedUserHint" : "login_hint", + "oauth2DeviceCodeLifespan" : "600", + "clientOfflineSessionMaxLifespan" : "0", + "oauth2DevicePollingInterval" : "5", + "clientSessionIdleTimeout" : "0", + "parRequestUriLifespan" : "60", + "clientSessionMaxLifespan" : "0", + "clientOfflineSessionIdleTimeout" : "0", + "cibaInterval" : "5", + "realmReusableOtpCode" : "false" + }, + "keycloakVersion" : "26.1.1", + "userManagedAccessAllowed" : false, + "organizationsEnabled" : false, + "verifiableCredentialsEnabled" : false, + "adminPermissionsEnabled" : false, + "clientProfiles" : { + "profiles" : [ ] + }, + "clientPolicies" : { + "policies" : [ ] + } +} \ No newline at end of file diff --git a/keycloak-data/import/emumet-users-0.json b/keycloak-data/import/emumet-users-0.json new file mode 100644 index 0000000..26a1dd5 --- /dev/null +++ b/keycloak-data/import/emumet-users-0.json @@ -0,0 +1,25 @@ +{ + "realm" : "emumet", + "users" : [ { + "id" : "0e2cb7da-8a19-4343-87e3-082b9032a652", + "username" : "testuser", + "email" : "testuser@example.com", + "emailVerified" : false, + "createdTimestamp" : 1738895015441, + "enabled" : true, + "totp" : false, + "credentials" : [ { + "id" : "3ae9520c-e3c9-43a6-b5ad-36760a384498", + "type" : "password", + "userLabel" : "My password", + "createdDate" : 1738895132371, + "secretData" : "{\"value\":\"HQ/zyli30BXNZ9a9M/6FZKhjn60z7DlEwgxkN7mroUU=\",\"salt\":\"Tk1Fi0u2kPuLiME14cA33Q==\",\"additionalParameters\":{}}", + "credentialData" : "{\"hashIterations\":5,\"algorithm\":\"argon2\",\"additionalParameters\":{\"hashLength\":[\"32\"],\"memory\":[\"7168\"],\"type\":[\"id\"],\"version\":[\"1.3\"],\"parallelism\":[\"1\"]}}" + } ], + "disableableCredentialTypes" : [ ], + "requiredActions" : [ ], + "realmRoles" : [ "default-roles-emumet" ], + "notBefore" : 0, + "groups" : [ ] + } ] +} \ No newline at end of file From bf1c9be8c0a7f3fe940a1d3d6343d29a7e456495 Mon Sep 17 00:00:00 2001 From: turtton Date: Fri, 7 Feb 2025 11:36:56 +0900 Subject: [PATCH 18/59] :see_no_evil: Ignore sqlx log folder --- .gitignore | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ce82118..eb70f34 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ /migrations/dbml-error.log .env keycloak-data/* -!keycloak-data/import \ No newline at end of file +!keycloak-data/import +logs \ No newline at end of file From f18ef0577b5be7ac106509b3215bfa374da2c5ba Mon Sep 17 00:00:00 2001 From: turtton Date: Thu, 13 Feb 2025 21:32:51 +0900 Subject: [PATCH 19/59] :truck: Update keycloak files --- README.md | 6 +++++- keycloak-data/import/emumet-users-0.json | 2 ++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 8e44011..8cb851e 100644 --- a/README.md +++ b/README.md @@ -14,7 +14,11 @@ podman run --rm -it -v ./keycloak-data/h2:/opt/keycloak/data/h2:Z,U -v ./keycloa > - Url: http://localhost:18080 > - ユーザー名: admin > - パスワード: admin -> - realm: emumet +> - Realm: emumet +> - Client: myclient +> - User: testuser +> - UserPass: testuser +> - RealmLoginTest: https://www.keycloak.org/app/ ### Update realm data diff --git a/keycloak-data/import/emumet-users-0.json b/keycloak-data/import/emumet-users-0.json index 26a1dd5..6200e61 100644 --- a/keycloak-data/import/emumet-users-0.json +++ b/keycloak-data/import/emumet-users-0.json @@ -3,6 +3,8 @@ "users" : [ { "id" : "0e2cb7da-8a19-4343-87e3-082b9032a652", "username" : "testuser", + "firstName" : "test", + "lastName" : "user", "email" : "testuser@example.com", "emailVerified" : false, "createdTimestamp" : 1738895015441, From 981f1fc4ee8cc0ae206784b2a97541fe47a0ddbe Mon Sep 17 00:00:00 2001 From: turtton Date: Thu, 13 Feb 2025 22:03:14 +0900 Subject: [PATCH 20/59] :arrow_up: Update flake.lock --- flake.lock | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/flake.lock b/flake.lock index 7004b4c..bc699ab 100644 --- a/flake.lock +++ b/flake.lock @@ -5,11 +5,11 @@ "systems": "systems" }, "locked": { - "lastModified": 1710146030, - "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", "owner": "numtide", "repo": "flake-utils", - "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", "type": "github" }, "original": { @@ -19,11 +19,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1710608262, - "narHash": "sha256-Tf2zqUWgU1iofcECQ+xj7HJVtoCz6yWG/oEIDmXxwXg=", + "lastModified": 1739419412, + "narHash": "sha256-NCWZQg4DbYVFWg+MOFrxWRaVsLA7yvRWAf6o0xPR1hI=", "owner": "nixos", "repo": "nixpkgs", - "rev": "d211b80d2944a41899a6ab24009d9729cca05e49", + "rev": "2d55b4c1531187926c2a423f6940b3b1301399b5", "type": "github" }, "original": { @@ -75,11 +75,11 @@ "systems": "systems_2" }, "locked": { - "lastModified": 1710146030, - "narHash": "sha256-SZ5L6eA7HJ/nmkzGG7/ISclqe6oZdOZTNoesiInkXPQ=", + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", "owner": "numtide", "repo": "flake-utils", - "rev": "b1d9ab70662946ef0850d488da1c9019f3a9752a", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", "type": "github" }, "original": { From 33aacdf320f73ceb59b1b9fa5feebb097f9e1dd4 Mon Sep 17 00:00:00 2001 From: turtton Date: Thu, 13 Feb 2025 22:23:36 +0900 Subject: [PATCH 21/59] :heavy_plus_sign: Add test_with --- Cargo.lock | 485 +++++++++++++++++- Cargo.toml | 2 + driver/Cargo.toml | 1 + driver/src/database/postgres/account.rs | 7 + driver/src/database/postgres/auth_account.rs | 4 + driver/src/database/postgres/auth_host.rs | 4 + driver/src/database/postgres/event.rs | 3 + driver/src/database/postgres/follow.rs | 5 + driver/src/database/postgres/image.rs | 4 + driver/src/database/postgres/metadata.rs | 5 + driver/src/database/postgres/profile.rs | 3 + .../src/database/postgres/remote_account.rs | 6 + 12 files changed, 519 insertions(+), 10 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index d08678c..7dee1b2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,17 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.11" @@ -82,6 +93,12 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +[[package]] +name = "arrayvec" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" + [[package]] name = "async-trait" version = "0.1.81" @@ -280,6 +297,18 @@ dependencies = [ "serde", ] +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -289,12 +318,68 @@ dependencies = [ "generic-array", ] +[[package]] +name = "borsh" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5430e3be710b68d984d1391c854eb431a9d548640711faa54eecb1df93db91cc" +dependencies = [ + "borsh-derive", + "cfg_aliases", +] + +[[package]] +name = "borsh-derive" +version = "1.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8b668d39970baad5356d7c83a86fee3a539e6f93bf6764c97368243e17a0487" +dependencies = [ + "once_cell", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "bumpalo" version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" +[[package]] +name = "byte-unit" +version = "5.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1cd29c3c585209b0cbc7309bfe3ed7efd8c84c21b7af29c8bfae908f8777174" +dependencies = [ + "rust_decimal", + "serde", + "utf8-width", +] + +[[package]] +name = "bytecheck" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "23cdc57ce23ac53c931e88a43d06d070a6fd142f2617be5855eb75efc9beb1c2" +dependencies = [ + "bytecheck_derive", + "ptr_meta", + "simdutf8", +] + +[[package]] +name = "bytecheck_derive" +version = "0.6.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db406d29fbcd95542e92559bed4d8ad92636d1ca8b3b72ede10b4bcc010e659" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "byteorder" version = "1.5.0" @@ -319,6 +404,12 @@ version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + [[package]] name = "chrono" version = "0.4.39" @@ -327,8 +418,10 @@ checksum = "7e36cc9d416881d2e24f9a963be5fb1cd90966419ac844274161d10488b3e825" dependencies = [ "android-tzdata", "iana-time-zone", + "js-sys", "num-traits", "serde", + "wasm-bindgen", "windows-targets 0.52.6", ] @@ -364,9 +457,9 @@ dependencies = [ [[package]] name = "core-foundation-sys" -version = "0.8.6" +version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06ea2b9bc92be3c2baa9334a323ebca2d6f074ff852cd1d7b11064035cd3868f" +checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" [[package]] name = "cpufeatures" @@ -401,6 +494,25 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dd111b7b7f7d55b72c0a6ae361660ee5853c9af73f70c3c2ef6858b950e2e51" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-queue" version = "0.3.11" @@ -586,6 +698,7 @@ dependencies = [ "serde", "serde_json", "sqlx", + "test-with", "time", "tokio", "uuid", @@ -641,6 +754,12 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "env_home" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c7f84e12ccf0a7ddc17a6c41c93326024c42920d7ee630d04950e6926645c0fe" + [[package]] name = "equivalent" version = "1.0.1" @@ -731,6 +850,12 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + [[package]] name = "futures" version = "0.3.31" @@ -884,6 +1009,9 @@ name = "hashbrown" version = "0.12.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash 0.7.8", +] [[package]] name = "hashbrown" @@ -891,7 +1019,7 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "ahash", + "ahash 0.8.11", "allocator-api2", ] @@ -1089,7 +1217,7 @@ dependencies = [ "http-body", "hyper", "pin-project-lite", - "socket2", + "socket2 0.5.7", "tokio", "tower-service", "tracing", @@ -1106,7 +1234,7 @@ dependencies = [ "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows-core", + "windows-core 0.52.0", ] [[package]] @@ -1576,6 +1704,15 @@ version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "303e8749c804ccd6ca3b428de7fe0d86cb86bc7606bc15291f100fd487960bb8" +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -1790,6 +1927,17 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "ping" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "122ee1f5a6843bec84fcbd5c6ba3622115337a6b8965b93a61aad347648f4e8d" +dependencies = [ + "rand", + "socket2 0.4.10", + "thiserror 1.0.63", +] + [[package]] name = "pkcs1" version = "0.7.5" @@ -1838,6 +1986,37 @@ dependencies = [ "zerocopy", ] +[[package]] +name = "proc-macro-crate" +version = "3.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecf48c7ca261d60b74ab1a7b20da18bede46776b2e55535cb958eb595c5fa7b" +dependencies = [ + "toml_edit", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "proc-macro2" version = "1.0.93" @@ -1847,6 +2026,26 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "ptr_meta" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0738ccf7ea06b608c10564b31debd4f5bc5e197fc8bfe088f68ae5ce81e7a4f1" +dependencies = [ + "ptr_meta_derive", +] + +[[package]] +name = "ptr_meta_derive" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16b845dbfca988fa33db069c0e230574d15a3088f147a87b64c7589eb662c9ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "quote" version = "1.0.36" @@ -1856,6 +2055,12 @@ dependencies = [ "proc-macro2", ] +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + [[package]] name = "rand" version = "0.8.5" @@ -1886,6 +2091,26 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rayon" +version = "1.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b418a60154510ca1a002a752ca9714984e21e4241e804d32555251faf8b78ffa" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redis" version = "0.23.3" @@ -1922,7 +2147,7 @@ dependencies = [ "pin-project-lite", "ryu", "sha1_smol", - "socket2", + "socket2 0.5.7", "tokio", "tokio-util", "url", @@ -1990,6 +2215,15 @@ version = "0.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" +[[package]] +name = "rend" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71fe3824f5629716b1589be05dacd749f6aa084c87e00e016714a8cdfccc997c" +dependencies = [ + "bytecheck", +] + [[package]] name = "reqwest" version = "0.12.12" @@ -1999,6 +2233,7 @@ dependencies = [ "base64 0.22.1", "bytes", "encoding_rs", + "futures-channel", "futures-core", "futures-util", "h2", @@ -2071,6 +2306,35 @@ dependencies = [ "windows-sys 0.52.0", ] +[[package]] +name = "rkyv" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9008cd6385b9e161d8229e1f6549dd23c3d022f132a2ea37ac3a10ac4935779b" +dependencies = [ + "bitvec", + "bytecheck", + "bytes", + "hashbrown 0.12.3", + "ptr_meta", + "rend", + "rkyv_derive", + "seahash", + "tinyvec", + "uuid", +] + +[[package]] +name = "rkyv_derive" +version = "0.7.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "503d1d27590a2b0a3a4ca4c94755aa2875657196ecbf401a42eff41d7de532c0" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "rsa" version = "0.9.6" @@ -2091,6 +2355,22 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rust_decimal" +version = "1.36.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b082d80e3e3cc52b2ed634388d436fe1f4de6af5786cc2de9ba9737527bdf555" +dependencies = [ + "arrayvec", + "borsh", + "bytes", + "num-traits", + "rand", + "rkyv", + "serde", + "serde_json", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -2185,6 +2465,12 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" +[[package]] +name = "seahash" +version = "4.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c107b6f4780854c8b126e228ea8869f4d7b71260f962fefb57b996b8959ba6b" + [[package]] name = "security-framework" version = "2.11.1" @@ -2406,6 +2692,12 @@ dependencies = [ "rand_core", ] +[[package]] +name = "simdutf8" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" + [[package]] name = "simple_asn1" version = "0.6.3" @@ -2454,6 +2746,16 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "socket2" +version = "0.4.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f7916fc008ca5542385b89a3d3ce689953c143e9304a9bf8beec1de48994c0d" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "socket2" version = "0.5.7" @@ -2512,7 +2814,7 @@ version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24ba59a9342a3d9bab6c56c118be528b27c9b60e490080e9711a04dccac83ef6" dependencies = [ - "ahash", + "ahash 0.8.11", "atoi", "byteorder", "bytes", @@ -2773,6 +3075,20 @@ dependencies = [ "syn 2.0.96", ] +[[package]] +name = "sysinfo" +version = "0.33.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fc858248ea01b66f19d8e8a6d55f41deaf91e9d495246fd01368d99935c6c01" +dependencies = [ + "core-foundation-sys", + "libc", + "memchr", + "ntapi", + "rayon", + "windows", +] + [[package]] name = "system-configuration" version = "0.6.1" @@ -2794,6 +3110,12 @@ dependencies = [ "libc", ] +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + [[package]] name = "tempfile" version = "3.12.0" @@ -2807,6 +3129,27 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "test-with" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eb39a1199760f44d7e11b6644b620d35abe7e012fa34288abae9e5aa95a243da" +dependencies = [ + "byte-unit", + "chrono", + "num_cpus", + "ping", + "proc-macro-error2", + "proc-macro2", + "quote", + "regex", + "reqwest", + "syn 2.0.96", + "sysinfo", + "uzers", + "which", +] + [[package]] name = "thiserror" version = "1.0.63" @@ -2926,7 +3269,7 @@ dependencies = [ "parking_lot", "pin-project-lite", "signal-hook-registry", - "socket2", + "socket2 0.5.7", "tokio-macros", "windows-sys 0.52.0", ] @@ -2986,6 +3329,23 @@ dependencies = [ "tokio", ] +[[package]] +name = "toml_datetime" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" + +[[package]] +name = "toml_edit" +version = "0.22.24" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" +dependencies = [ + "indexmap 2.3.0", + "toml_datetime", + "winnow", +] + [[package]] name = "tower" version = "0.5.2" @@ -3215,6 +3575,12 @@ version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" +[[package]] +name = "utf8-width" +version = "0.1.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86bd8d4e895da8537e5315b8254664e6b769c4ff3db18321b297a1e7004392e3" + [[package]] name = "utf8_iter" version = "1.0.4" @@ -3231,6 +3597,16 @@ dependencies = [ "serde", ] +[[package]] +name = "uzers" +version = "0.11.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76d283dc7e8c901e79e32d077866eaf599156cbf427fffa8289aecc52c5c3f63" +dependencies = [ + "libc", + "log", +] + [[package]] name = "valuable" version = "0.1.0" @@ -3362,6 +3738,18 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "which" +version = "7.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2774c861e1f072b3aadc02f8ba886c26ad6321567ecc294c935434cad06f1283" +dependencies = [ + "either", + "env_home", + "rustix", + "winsafe", +] + [[package]] name = "whoami" version = "1.5.1" @@ -3394,6 +3782,16 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" +[[package]] +name = "windows" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12342cb4d8e3b046f3d80effd474a7a02447231330ef77d71daa6fbc40681143" +dependencies = [ + "windows-core 0.57.0", + "windows-targets 0.52.6", +] + [[package]] name = "windows-core" version = "0.52.0" @@ -3403,17 +3801,60 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "windows-core" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d2ed2439a290666cd67ecce2b0ffaad89c2a56b976b736e6ece670297897832d" +dependencies = [ + "windows-implement", + "windows-interface", + "windows-result 0.1.2", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-implement" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9107ddc059d5b6fbfbffdfa7a7fe3e22a226def0b2608f72e9d552763d3e1ad7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + +[[package]] +name = "windows-interface" +version = "0.57.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29bee4b38ea3cde66011baa44dba677c432a78593e202392d1e9070cf2a7fca7" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "windows-registry" version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e400001bb720a623c1c69032f8e3e4cf09984deec740f007dd2b03ec864804b0" dependencies = [ - "windows-result", + "windows-result 0.2.0", "windows-strings", "windows-targets 0.52.6", ] +[[package]] +name = "windows-result" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e383302e8ec8515204254685643de10811af0ed97ea37210dc26fb0032647f8" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-result" version = "0.2.0" @@ -3429,7 +3870,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" dependencies = [ - "windows-result", + "windows-result 0.2.0", "windows-targets 0.52.6", ] @@ -3581,6 +4022,21 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" +[[package]] +name = "winnow" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59690dea168f2198d1a3b0cac23b8063efcd11012f10ae4698f284808c8ef603" +dependencies = [ + "memchr", +] + +[[package]] +name = "winsafe" +version = "0.0.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" + [[package]] name = "write16" version = "1.0.0" @@ -3593,6 +4049,15 @@ version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + [[package]] name = "yoke" version = "0.7.5" diff --git a/Cargo.toml b/Cargo.toml index 4a9cd3f..13b5660 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -21,6 +21,8 @@ error-stack = "0.4.1" tracing = "0.1.40" tracing-subscriber = { version = "0.3.18", features = ["registry", "env-filter", "fmt"] } +test-with = "0.14.7" + [workspace.package] version = "0.1.0" edition = "2021" diff --git a/driver/Cargo.toml b/driver/Cargo.toml index 79c90e1..7643e2e 100644 --- a/driver/Cargo.toml +++ b/driver/Cargo.toml @@ -22,3 +22,4 @@ kernel = { path = "../kernel" } [dev-dependencies] tokio = { workspace = true, features = ["macros", "test-util"] } +test-with.workspace = true diff --git a/driver/src/database/postgres/account.rs b/driver/src/database/postgres/account.rs index 6c3ffed..7d256e9 100644 --- a/driver/src/database/postgres/account.rs +++ b/driver/src/database/postgres/account.rs @@ -238,6 +238,7 @@ mod test { }; use sqlx::types::Uuid; + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn find_by_id() { let database = PostgresDatabase::new().await.unwrap(); @@ -268,6 +269,7 @@ mod test { assert_eq!(result.as_ref().map(Account::id), Some(account.id())); } + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn find_by_auth_id() { let database = PostgresDatabase::new().await.unwrap(); @@ -281,6 +283,7 @@ mod test { assert!(accounts.is_empty()); } + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn find_by_name() { let database = PostgresDatabase::new().await.unwrap(); @@ -317,6 +320,7 @@ mod test { .unwrap(); } + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn find_by_nanoid() { let database = PostgresDatabase::new().await.unwrap(); @@ -366,6 +370,7 @@ mod test { use sqlx::types::time::OffsetDateTime; use sqlx::types::Uuid; + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn create() { let database = PostgresDatabase::new().await.unwrap(); @@ -396,6 +401,7 @@ mod test { assert_eq!(result.id(), account.id()); } + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn update() { let database = PostgresDatabase::new().await.unwrap(); @@ -441,6 +447,7 @@ mod test { assert_eq!(result.as_ref().map(Account::id), Some(updated_account.id())); } + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn delete() { let database = PostgresDatabase::new().await.unwrap(); diff --git a/driver/src/database/postgres/auth_account.rs b/driver/src/database/postgres/auth_account.rs index 0bdcb8a..3d0ff07 100644 --- a/driver/src/database/postgres/auth_account.rs +++ b/driver/src/database/postgres/auth_account.rs @@ -176,6 +176,7 @@ mod test { }; use uuid::Uuid; + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn find_by_id() { let database = PostgresDatabase::new().await.unwrap(); @@ -229,6 +230,7 @@ mod test { }; use uuid::Uuid; + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn create() { let database = PostgresDatabase::new().await.unwrap(); @@ -266,6 +268,7 @@ mod test { .unwrap(); } + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn update() { let database = PostgresDatabase::new().await.unwrap(); @@ -314,6 +317,7 @@ mod test { .unwrap(); } + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn delete() { let database = PostgresDatabase::new().await.unwrap(); diff --git a/driver/src/database/postgres/auth_host.rs b/driver/src/database/postgres/auth_host.rs index a13315c..12371c5 100644 --- a/driver/src/database/postgres/auth_host.rs +++ b/driver/src/database/postgres/auth_host.rs @@ -146,6 +146,7 @@ mod test { use kernel::prelude::entity::{AuthHost, AuthHostId}; use uuid::Uuid; + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn find_by_id() { let database = PostgresDatabase::new().await.unwrap(); @@ -167,6 +168,7 @@ mod test { assert_eq!(auth_host, found_auth_host); } + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn find_by_url() { let database = PostgresDatabase::new().await.unwrap(); @@ -198,6 +200,7 @@ mod test { use kernel::prelude::entity::{AuthHost, AuthHostId}; use uuid::Uuid; + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn create() { let database = PostgresDatabase::new().await.unwrap(); @@ -219,6 +222,7 @@ mod test { assert_eq!(auth_host, found_auth_host); } + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn update() { let database = PostgresDatabase::new().await.unwrap(); diff --git a/driver/src/database/postgres/event.rs b/driver/src/database/postgres/event.rs index b8fdb1b..6834264 100644 --- a/driver/src/database/postgres/event.rs +++ b/driver/src/database/postgres/event.rs @@ -191,6 +191,7 @@ mod test { use crate::database::PostgresDatabase; + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn find_by_id() { let db = PostgresDatabase::new().await.unwrap(); @@ -237,6 +238,7 @@ mod test { assert_eq!(&events[2].event, deleted_account.event()); } + #[test_with::env(DATABASE_URL)] #[tokio::test] #[should_panic] async fn find_by_id_with_version() { @@ -291,6 +293,7 @@ mod test { use crate::database::PostgresDatabase; + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn basic_creation() { let db = PostgresDatabase::new().await.unwrap(); diff --git a/driver/src/database/postgres/follow.rs b/driver/src/database/postgres/follow.rs index 43779b8..aa9644b 100644 --- a/driver/src/database/postgres/follow.rs +++ b/driver/src/database/postgres/follow.rs @@ -246,6 +246,7 @@ mod test { }; use uuid::Uuid; + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn find_followers() { let database = PostgresDatabase::new().await.unwrap(); @@ -328,6 +329,7 @@ mod test { .unwrap(); } + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn find_followings() { let database = PostgresDatabase::new().await.unwrap(); @@ -424,6 +426,7 @@ mod test { }; use uuid::Uuid; + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn create() { let database = PostgresDatabase::new().await.unwrap(); @@ -492,6 +495,7 @@ mod test { .unwrap(); } + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn update() { let database = PostgresDatabase::new().await.unwrap(); @@ -588,6 +592,7 @@ mod test { .unwrap(); } + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn delete() { let database = PostgresDatabase::new().await.unwrap(); diff --git a/driver/src/database/postgres/image.rs b/driver/src/database/postgres/image.rs index 0ef3afa..a0b5724 100644 --- a/driver/src/database/postgres/image.rs +++ b/driver/src/database/postgres/image.rs @@ -153,6 +153,7 @@ mod test { use kernel::prelude::entity::{Image, ImageBlurHash, ImageHash, ImageId}; use uuid::Uuid; + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn find_by_id() { let database = PostgresDatabase::new().await.unwrap(); @@ -185,6 +186,7 @@ mod test { .unwrap(); } + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn find_by_url() { let database = PostgresDatabase::new().await.unwrap(); @@ -226,6 +228,7 @@ mod test { use kernel::prelude::entity::{Image, ImageBlurHash, ImageHash, ImageId}; use uuid::Uuid; + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn create() { let database = PostgresDatabase::new().await.unwrap(); @@ -252,6 +255,7 @@ mod test { .unwrap(); } + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn delete() { let database = PostgresDatabase::new().await.unwrap(); diff --git a/driver/src/database/postgres/metadata.rs b/driver/src/database/postgres/metadata.rs index a20ea93..ab4561d 100644 --- a/driver/src/database/postgres/metadata.rs +++ b/driver/src/database/postgres/metadata.rs @@ -185,6 +185,7 @@ mod test { }; use uuid::Uuid; + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn find_by_id() { let database = PostgresDatabase::new().await.unwrap(); @@ -232,6 +233,7 @@ mod test { assert_eq!(found.as_ref().map(Metadata::id), Some(metadata.id())); } + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn find_by_account_id() { let database = PostgresDatabase::new().await.unwrap(); @@ -307,6 +309,7 @@ mod test { }; use uuid::Uuid; + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn create() { let database = PostgresDatabase::new().await.unwrap(); @@ -353,6 +356,7 @@ mod test { assert_eq!(found.as_ref().map(Metadata::id), Some(metadata.id())); } + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn update() { let database = PostgresDatabase::new().await.unwrap(); @@ -417,6 +421,7 @@ mod test { ); } + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn delete() { let database = PostgresDatabase::new().await.unwrap(); diff --git a/driver/src/database/postgres/profile.rs b/driver/src/database/postgres/profile.rs index eef3900..39a0f72 100644 --- a/driver/src/database/postgres/profile.rs +++ b/driver/src/database/postgres/profile.rs @@ -160,6 +160,7 @@ mod test { use crate::database::PostgresDatabase; + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn find_by_id() { let database = PostgresDatabase::new().await.unwrap(); @@ -229,6 +230,7 @@ mod test { use crate::database::PostgresDatabase; + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn create() { let database = PostgresDatabase::new().await.unwrap(); @@ -274,6 +276,7 @@ mod test { .unwrap(); } + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn update() { let database = PostgresDatabase::new().await.unwrap(); diff --git a/driver/src/database/postgres/remote_account.rs b/driver/src/database/postgres/remote_account.rs index 5ec4fc6..407d60c 100644 --- a/driver/src/database/postgres/remote_account.rs +++ b/driver/src/database/postgres/remote_account.rs @@ -214,6 +214,7 @@ mod test { use kernel::prelude::entity::{RemoteAccount, RemoteAccountId}; use uuid::Uuid; + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn find_by_id() { let database = PostgresDatabase::new().await.unwrap(); @@ -240,6 +241,7 @@ mod test { .unwrap(); } + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn find_by_acct() { let database = PostgresDatabase::new().await.unwrap(); @@ -271,6 +273,7 @@ mod test { .unwrap(); } + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn find_by_url() { let database = PostgresDatabase::new().await.unwrap(); @@ -311,6 +314,7 @@ mod test { use kernel::prelude::entity::{RemoteAccount, RemoteAccountId}; use uuid::Uuid; + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn create() { let database = PostgresDatabase::new().await.unwrap(); @@ -331,6 +335,7 @@ mod test { .unwrap(); } + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn update() { let database = PostgresDatabase::new().await.unwrap(); @@ -365,6 +370,7 @@ mod test { .unwrap(); } + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn delete() { let database = PostgresDatabase::new().await.unwrap(); From 8e2e0f5a493b6e71792de34366b80933751342a0 Mon Sep 17 00:00:00 2001 From: turtton Date: Thu, 13 Feb 2025 22:26:44 +0900 Subject: [PATCH 22/59] :wrench: Add server as package --- flake.nix | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/flake.nix b/flake.nix index 327428e..5b85618 100644 --- a/flake.nix +++ b/flake.nix @@ -11,9 +11,19 @@ pkgs = import nixpkgs { inherit system; }; + emumet = pkgs.rustPlatform.buildRustPackage { + pname = "server"; + name = "emumet"; + src = ./.; + cargoLock.lockFile = ./Cargo.lock; + nativeBuildInputs = [ pkgs.pkg-config ]; + buildInputs = [ pkgs.openssl ]; + PKG_CONFIG_PATH = "${pkgs.openssl.dev}/lib/pkgconfig"; + }; in with pkgs; rec { formatter = nixpkgs-fmt; + packages.default = emumet; devShells.default = mkShell { nativeBuildInputs = [ pkg-config ]; buildInputs = [ openssl ]; From be3f432c98a0e9af9a26d7e45fac872463f3188c Mon Sep 17 00:00:00 2001 From: turtton Date: Thu, 13 Feb 2025 22:30:02 +0900 Subject: [PATCH 23/59] :wrench: Add keycloak env defaults --- .env.example | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.env.example b/.env.example index cc84191..ba47af7 100644 --- a/.env.example +++ b/.env.example @@ -4,4 +4,7 @@ DATABASE_HOST=localhost DATABASE_PORT=5432 DATABASE_USER=user DATABASE_PASSWORD=password -DATABASE_NAME=dbname \ No newline at end of file +DATABASE_NAME=dbname + +KEYCLOAK_SERVER=http://localhost:18080/ +KEYCLOAK_REALM=emumet \ No newline at end of file From e4134125ceedbba1e675bf9bad76bea1d4a3bd10 Mon Sep 17 00:00:00 2001 From: turtton Date: Thu, 20 Feb 2025 22:23:10 +0900 Subject: [PATCH 24/59] :recycle: Extract closure to function and improve response --- Cargo.lock | 3 + application/Cargo.toml | 7 +++ application/src/service/account.rs | 6 +- server/src/route/account.rs | 90 ++++++++++++++---------------- 4 files changed, 56 insertions(+), 50 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7dee1b2..3030142 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -81,9 +81,12 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" name = "application" version = "0.1.0" dependencies = [ + "driver", "error-stack", "kernel", "time", + "tokio", + "uuid", "vodca", ] diff --git a/application/Cargo.toml b/application/Cargo.toml index 8f74d2c..f48958a 100644 --- a/application/Cargo.toml +++ b/application/Cargo.toml @@ -13,3 +13,10 @@ vodca = { workspace = true } error-stack = { workspace = true } kernel = { path = "../kernel" } + +[dev-dependencies] +tokio = { workspace = true, features = ["macros", "test-util"] } + +uuid.workspace = true + +driver.path = "../driver" \ No newline at end of file diff --git a/application/src/service/account.rs b/application/src/service/account.rs index 36378ec..fa0f0e6 100644 --- a/application/src/service/account.rs +++ b/application/src/service/account.rs @@ -18,12 +18,12 @@ pub trait GetAccountService: cursor, limit, }: Pagination, - ) -> impl Future, KernelError>> { + ) -> impl Future>, KernelError>> { async move { let auth_account = if let Some(auth_account) = get_auth_account(self, subject).await? { auth_account } else { - return Ok(vec![]); + return Ok(None); }; let mut transaction = self.database_connection().begin_transaction().await?; let accounts = self @@ -39,7 +39,7 @@ pub trait GetAccountService: None }; let accounts = apply_pagination(accounts, limit, cursor, direction); - Ok(accounts.into_iter().map(AccountDto::from).collect()) + Ok(Some(accounts.into_iter().map(AccountDto::from).collect())) } } } diff --git a/server/src/route/account.rs b/server/src/route/account.rs index f4e5b86..f828f44 100644 --- a/server/src/route/account.rs +++ b/server/src/route/account.rs @@ -33,8 +33,8 @@ struct AccountResponse { #[derive(Debug, Serialize)] struct AccountsResponse { - first: String, - last: String, + first: Option, + last: Option, items: Vec, } @@ -42,53 +42,49 @@ pub trait AccountRouter { fn route_account(self, instance: KeycloakAuthInstance) -> Self; } +async fn get_accounts( + Extension(token): Extension>, + State(module): State, + Query(GetAllAccountQuery { + direction, + limit, + cursor, + }): Query, + req: Request, +) -> Result, ErrorStatus> { + expect_role!(&token, req); + let direction = direction.convert_to_direction()?; + let pagination = Pagination::new(limit, cursor, direction); + let result = module + .handler() + .pgpool() + .get_all_accounts(token.subject, pagination) + .await + .map_err(ErrorStatus::from)? + .ok_or(ErrorStatus::from(StatusCode::NOT_FOUND))?; + if result.is_empty() { + return Err(ErrorStatus::from(StatusCode::NOT_FOUND)); + } + let response = AccountsResponse { + first: result.first().map(|account| account.nanoid.clone()), + last: result.last().map(|account| account.nanoid.clone()), + items: result + .into_iter() + .map(|account| AccountResponse { + id: account.nanoid, + name: account.name, + public_key: account.public_key, + is_bot: account.is_bot, + created_at: account.created_at, + }) + .collect(), + }; + Ok(Json(response)) +} + impl AccountRouter for Router { fn route_account(self, instance: KeycloakAuthInstance) -> Self { - self.route( - "/accounts", - get( - |Extension(token): Extension>, - State(module): State, - Query(GetAllAccountQuery { - direction, - limit, - cursor, - }): Query, - req: Request| async move { - expect_role!(&token, req); - let direction = direction.convert_to_direction()?; - let pagination = Pagination::new(limit, cursor, direction); - let result = module - .handler() - .pgpool() - .get_all_accounts(token.subject, pagination) - .await - .map_err(ErrorStatus::from)?; - if result.is_empty() { - return Err(ErrorStatus::from(StatusCode::NOT_FOUND)); - } - let response = AccountsResponse { - first: result - .first() - .map(|account| account.nanoid.clone()) - .unwrap(), - last: result.last().map(|account| account.nanoid.clone()).unwrap(), - items: result - .into_iter() - .map(|account| AccountResponse { - id: account.nanoid, - name: account.name, - public_key: account.public_key, - is_bot: account.is_bot, - created_at: account.created_at, - }) - .collect(), - }; - Ok(Json(response)) - }, - ), - ) - .layer( + self.route("/accounts", get(get_accounts)).layer( KeycloakAuthLayer::::builder() .instance(instance) .passthrough_mode(PassthroughMode::Block) From afd85f2c4fc8b0202a716d883d651e31a300f555 Mon Sep 17 00:00:00 2001 From: turtton Date: Fri, 28 Feb 2025 00:43:35 +0900 Subject: [PATCH 25/59] :bulb: I tried to use mold but gained no advantages...(1sec slower than ld) --- flake.nix | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/flake.nix b/flake.nix index 5b85618..3da7b8a 100644 --- a/flake.nix +++ b/flake.nix @@ -21,16 +21,20 @@ PKG_CONFIG_PATH = "${pkgs.openssl.dev}/lib/pkgconfig"; }; in - with pkgs; rec { + with pkgs; { formatter = nixpkgs-fmt; packages.default = emumet; - devShells.default = mkShell { + devShells.default = mkShell rec { nativeBuildInputs = [ pkg-config ]; buildInputs = [ openssl ]; packages = [ nodePackages.pnpm sqlx-cli ]; + shellHook = '' + #export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER="${pkgs.clang}/bin/clang" + #export CARGO_TARGET_X86_64-UNKNOWN_LINUX_GNU_RUSTFLAGS="-C link-arg=-fuse-ld=${pkgs.mold-wrapped.override(old: { extraPackages = nativeBuildInputs ++ buildInputs; })}/bin/mold" + ''; }; }); } From 2bf4ac871f4f79722038e9e982d89b776e28b5b0 Mon Sep 17 00:00:00 2001 From: turtton Date: Fri, 28 Feb 2025 00:45:14 +0900 Subject: [PATCH 26/59] :hammer: Add defaults --- kernel/src/entity/auth_account/id.rs | 6 ++++++ kernel/src/entity/auth_host/id.rs | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/kernel/src/entity/auth_account/id.rs b/kernel/src/entity/auth_account/id.rs index 9c6d074..985939d 100644 --- a/kernel/src/entity/auth_account/id.rs +++ b/kernel/src/entity/auth_account/id.rs @@ -6,6 +6,12 @@ use vodca::{AsRefln, Fromln, Newln}; #[derive(Debug, Clone, PartialEq, Eq, Hash, Fromln, AsRefln, Newln, Serialize, Deserialize)] pub struct AuthAccountId(Uuid); +impl Default for AuthAccountId { + fn default() -> Self { + AuthAccountId(Uuid::now_v7()) + } +} + impl From for EventId { fn from(auth_account_id: AuthAccountId) -> Self { EventId::new(auth_account_id.0) diff --git a/kernel/src/entity/auth_host/id.rs b/kernel/src/entity/auth_host/id.rs index 34fb6b5..cf00636 100644 --- a/kernel/src/entity/auth_host/id.rs +++ b/kernel/src/entity/auth_host/id.rs @@ -4,3 +4,9 @@ use vodca::{AsRefln, Fromln, Newln}; #[derive(Debug, Clone, PartialEq, Eq, Hash, Fromln, AsRefln, Newln, Serialize, Deserialize)] pub struct AuthHostId(Uuid); + +impl Default for AuthHostId { + fn default() -> Self { + AuthHostId(Uuid::now_v7()) + } +} From eba1b849909815fb0a76910d0b70ce15b23dc7e4 Mon Sep 17 00:00:00 2001 From: turtton Date: Fri, 28 Feb 2025 00:45:52 +0900 Subject: [PATCH 27/59] :construction: Add auth_account treatments --- application/src/service/account.rs | 33 +++++++++++----- application/src/service/auth_account.rs | 49 +++++++++++++++++++++--- application/src/transfer.rs | 1 + application/src/transfer/auth_account.rs | 5 +++ kernel/src/entity/auth_host.rs | 5 ++- server/src/keycloak.rs | 35 ++++++++++++++++- server/src/route/account.rs | 24 +++++++----- 7 files changed, 126 insertions(+), 26 deletions(-) create mode 100644 application/src/transfer/auth_account.rs diff --git a/application/src/service/account.rs b/application/src/service/account.rs index fa0f0e6..80032e4 100644 --- a/application/src/service/account.rs +++ b/application/src/service/account.rs @@ -1,18 +1,29 @@ use crate::service::auth_account::get_auth_account; use crate::transfer::account::AccountDto; +use crate::transfer::auth_account::AuthAccountInfo; use crate::transfer::pagination::{apply_pagination, Pagination}; use kernel::interfaces::database::DatabaseConnection; -use kernel::interfaces::query::{AccountQuery, DependOnAccountQuery, DependOnAuthAccountQuery}; +use kernel::interfaces::modify::{DependOnAuthAccountModifier, DependOnAuthHostModifier}; +use kernel::interfaces::query::{ + AccountQuery, DependOnAccountQuery, DependOnAuthAccountQuery, DependOnAuthHostQuery, +}; use kernel::prelude::entity::{Account, Nanoid}; use kernel::KernelError; use std::future::Future; pub trait GetAccountService: - 'static + Sync + Send + DependOnAccountQuery + DependOnAuthAccountQuery + 'static + + Sync + + Send + + DependOnAccountQuery + + DependOnAuthAccountQuery + + DependOnAuthAccountModifier + + DependOnAuthHostQuery + + DependOnAuthHostModifier { fn get_all_accounts( &self, - subject: String, + account: AuthAccountInfo, Pagination { direction, cursor, @@ -20,11 +31,7 @@ pub trait GetAccountService: }: Pagination, ) -> impl Future>, KernelError>> { async move { - let auth_account = if let Some(auth_account) = get_auth_account(self, subject).await? { - auth_account - } else { - return Ok(None); - }; + let auth_account = get_auth_account(self, account).await?; let mut transaction = self.database_connection().begin_transaction().await?; let accounts = self .account_query() @@ -44,4 +51,12 @@ pub trait GetAccountService: } } -impl GetAccountService for T where T: 'static + DependOnAccountQuery + DependOnAuthAccountQuery {} +impl GetAccountService for T where + T: 'static + + DependOnAccountQuery + + DependOnAuthAccountQuery + + DependOnAuthAccountModifier + + DependOnAuthHostQuery + + DependOnAuthHostModifier +{ +} diff --git a/application/src/service/auth_account.rs b/application/src/service/auth_account.rs index 299bfad..df1f7ee 100644 --- a/application/src/service/auth_account.rs +++ b/application/src/service/auth_account.rs @@ -1,19 +1,58 @@ +use crate::transfer::auth_account::AuthAccountInfo; use kernel::interfaces::database::{DatabaseConnection, DependOnDatabaseConnection}; -use kernel::interfaces::query::{AuthAccountQuery, DependOnAuthAccountQuery}; -use kernel::prelude::entity::{AuthAccount, AuthAccountClientId}; +use kernel::interfaces::modify::{ + AuthHostModifier, DependOnAuthAccountModifier, DependOnAuthHostModifier, +}; +use kernel::interfaces::query::{ + AuthAccountQuery, AuthHostQuery, DependOnAuthAccountQuery, DependOnAuthHostQuery, +}; +use kernel::prelude::entity::{ + AuthAccount, AuthAccountClientId, AuthAccountId, AuthHost, AuthHostId, AuthHostUrl, +}; use kernel::KernelError; pub(crate) async fn get_auth_account< - S: DependOnDatabaseConnection + DependOnAuthAccountQuery + ?Sized, + S: 'static + + DependOnDatabaseConnection + + DependOnAuthAccountQuery + + DependOnAuthAccountModifier + + DependOnAuthHostQuery + + DependOnAuthHostModifier + + ?Sized, >( service: &S, - client_id: String, -) -> error_stack::Result, KernelError> { + AuthAccountInfo { + host_url: host, + client_id, + }: AuthAccountInfo, +) -> error_stack::Result { let client_id = AuthAccountClientId::new(client_id); let mut transaction = service.database_connection().begin_transaction().await?; let auth_account = service .auth_account_query() .find_by_client_id(&mut transaction, &client_id) .await?; + let auth_account = if let Some(auth_account) = auth_account { + auth_account + } else { + let url = AuthHostUrl::new(host); + let auth_host = service + .auth_host_query() + .find_by_url(&mut transaction, &url) + .await?; + let auth_host = if let Some(auth_host) = auth_host { + auth_host + } else { + let auth_host = AuthHost::new(AuthHostId::default(), url); + service + .auth_host_modifier() + .create(&mut transaction, &auth_host) + .await?; + auth_host + }; + let host_id = auth_host.into_destruct().id; + let auth_account = AuthAccount::create(AuthAccountId::default(), host_id, client_id); + todo!() + }; Ok(auth_account) } diff --git a/application/src/transfer.rs b/application/src/transfer.rs index 971413f..bac1dc3 100644 --- a/application/src/transfer.rs +++ b/application/src/transfer.rs @@ -1,2 +1,3 @@ pub mod account; +pub mod auth_account; pub mod pagination; diff --git a/application/src/transfer/auth_account.rs b/application/src/transfer/auth_account.rs new file mode 100644 index 0000000..7fa9d9d --- /dev/null +++ b/application/src/transfer/auth_account.rs @@ -0,0 +1,5 @@ +#[derive(Debug)] +pub struct AuthAccountInfo { + pub host_url: String, + pub client_id: String, +} diff --git a/kernel/src/entity/auth_host.rs b/kernel/src/entity/auth_host.rs index f453864..551c1f3 100644 --- a/kernel/src/entity/auth_host.rs +++ b/kernel/src/entity/auth_host.rs @@ -2,10 +2,13 @@ mod id; mod url; pub use self::{id::*, url::*}; +use destructure::Destructure; use serde::{Deserialize, Serialize}; use vodca::{Newln, References}; -#[derive(Debug, Clone, PartialEq, Eq, Hash, References, Newln, Serialize, Deserialize)] +#[derive( + Debug, Clone, PartialEq, Eq, Hash, References, Newln, Serialize, Deserialize, Destructure, +)] pub struct AuthHost { id: AuthHostId, url: AuthHostUrl, diff --git a/server/src/keycloak.rs b/server/src/keycloak.rs index f99607e..a969f6f 100644 --- a/server/src/keycloak.rs +++ b/server/src/keycloak.rs @@ -1,11 +1,18 @@ +use application::transfer::auth_account::AuthAccountInfo; +use axum_keycloak_auth::decode::KeycloakToken; use axum_keycloak_auth::instance::{KeycloakAuthInstance, KeycloakConfig}; +use std::sync::LazyLock; +use axum_keycloak_auth::role::Role; const KEYCLOAK_SERVER_KEY: &str = "KEYCLOAK_SERVER"; +static SERVER_URL: LazyLock = LazyLock::new(|| { + dotenvy::var(KEYCLOAK_SERVER_KEY).unwrap_or_else(|_| "http://localhost:18080/".to_string()) +}); + const KEYCLOAK_REALM_KEY: &str = "KEYCLOAK_REALM"; pub fn create_keycloak_instance() -> KeycloakAuthInstance { - let server = - dotenvy::var(KEYCLOAK_SERVER_KEY).unwrap_or_else(|_| "http://localhost:18080/".to_string()); + let server = &*SERVER_URL; let realm = dotenvy::var(KEYCLOAK_REALM_KEY).unwrap_or_else(|_| "MyRealm".to_string()); log::info!("Keycloak info: server={server}, realm={realm}"); KeycloakAuthInstance::new( @@ -16,6 +23,30 @@ pub fn create_keycloak_instance() -> KeycloakAuthInstance { ) } +#[derive(Debug)] +pub struct KeycloakAuthAccount { + host_url: String, + client_id: String, +} + +impl From> for KeycloakAuthAccount { + fn from(token: KeycloakToken) -> Self { + Self { + host_url: SERVER_URL.clone(), + client_id: token.subject, + } + } +} + +impl Into for KeycloakAuthAccount { + fn into(self) -> AuthAccountInfo { + AuthAccountInfo { + host_url: self.host_url, + client_id: self.client_id, + } + } +} + #[macro_export] macro_rules! expect_role { ($token: expr, $req: expr) => { diff --git a/server/src/route/account.rs b/server/src/route/account.rs index f828f44..2a006c1 100644 --- a/server/src/route/account.rs +++ b/server/src/route/account.rs @@ -6,7 +6,7 @@ use application::service::account::GetAccountService; use application::transfer::pagination::Pagination; use axum::extract::{Query, Request, State}; use axum::http::StatusCode; -use axum::routing::get; +use axum::routing::{delete, get, post}; use axum::{Extension, Json, Router}; use axum_keycloak_auth::decode::KeycloakToken; use axum_keycloak_auth::instance::KeycloakAuthInstance; @@ -14,6 +14,7 @@ use axum_keycloak_auth::layer::KeycloakAuthLayer; use axum_keycloak_auth::PassthroughMode; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; +use crate::keycloak::KeycloakAuthAccount; #[derive(Debug, Deserialize)] struct GetAllAccountQuery { @@ -53,12 +54,13 @@ async fn get_accounts( req: Request, ) -> Result, ErrorStatus> { expect_role!(&token, req); + let auth_info = KeycloakAuthAccount::from(token); let direction = direction.convert_to_direction()?; let pagination = Pagination::new(limit, cursor, direction); let result = module .handler() .pgpool() - .get_all_accounts(token.subject, pagination) + .get_all_accounts(auth_info.into(), pagination) .await .map_err(ErrorStatus::from)? .ok_or(ErrorStatus::from(StatusCode::NOT_FOUND))?; @@ -84,12 +86,16 @@ async fn get_accounts( impl AccountRouter for Router { fn route_account(self, instance: KeycloakAuthInstance) -> Self { - self.route("/accounts", get(get_accounts)).layer( - KeycloakAuthLayer::::builder() - .instance(instance) - .passthrough_mode(PassthroughMode::Block) - .expected_audiences(vec![String::from("account")]) - .build(), - ) + self.route("/accounts", get(get_accounts)) + // .route("/accounts", post(todo!())) + // .route("/accounts/:id", get(todo!())) + // .route("/accounts/:id", delete(todo!())) + .layer( + KeycloakAuthLayer::::builder() + .instance(instance) + .passthrough_mode(PassthroughMode::Block) + .expected_audiences(vec![String::from("account")]) + .build(), + ) } } From 3fc173d7d3c211af025d127c63350aa34fb14a62 Mon Sep 17 00:00:00 2001 From: turtton Date: Thu, 13 Mar 2025 23:58:58 +0900 Subject: [PATCH 28/59] :art: Format codes --- server/src/keycloak.rs | 10 +++++----- server/src/route/account.rs | 4 ++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/server/src/keycloak.rs b/server/src/keycloak.rs index a969f6f..23efca9 100644 --- a/server/src/keycloak.rs +++ b/server/src/keycloak.rs @@ -1,8 +1,8 @@ use application::transfer::auth_account::AuthAccountInfo; use axum_keycloak_auth::decode::KeycloakToken; use axum_keycloak_auth::instance::{KeycloakAuthInstance, KeycloakConfig}; -use std::sync::LazyLock; use axum_keycloak_auth::role::Role; +use std::sync::LazyLock; const KEYCLOAK_SERVER_KEY: &str = "KEYCLOAK_SERVER"; static SERVER_URL: LazyLock = LazyLock::new(|| { @@ -38,11 +38,11 @@ impl From> for KeycloakAuthAccount { } } -impl Into for KeycloakAuthAccount { - fn into(self) -> AuthAccountInfo { +impl From for AuthAccountInfo { + fn from(val: KeycloakAuthAccount) -> Self { AuthAccountInfo { - host_url: self.host_url, - client_id: self.client_id, + host_url: val.host_url, + client_id: val.client_id, } } } diff --git a/server/src/route/account.rs b/server/src/route/account.rs index 2a006c1..8af16f9 100644 --- a/server/src/route/account.rs +++ b/server/src/route/account.rs @@ -1,12 +1,13 @@ use crate::error::ErrorStatus; use crate::expect_role; use crate::handler::AppModule; +use crate::keycloak::KeycloakAuthAccount; use crate::route::DirectionConverter; use application::service::account::GetAccountService; use application::transfer::pagination::Pagination; use axum::extract::{Query, Request, State}; use axum::http::StatusCode; -use axum::routing::{delete, get, post}; +use axum::routing::get; use axum::{Extension, Json, Router}; use axum_keycloak_auth::decode::KeycloakToken; use axum_keycloak_auth::instance::KeycloakAuthInstance; @@ -14,7 +15,6 @@ use axum_keycloak_auth::layer::KeycloakAuthLayer; use axum_keycloak_auth::PassthroughMode; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; -use crate::keycloak::KeycloakAuthAccount; #[derive(Debug, Deserialize)] struct GetAllAccountQuery { From ca4cd4dbfa09ce4dd1c40a1e09402ef4379032a1 Mon Sep 17 00:00:00 2001 From: turtton Date: Fri, 14 Mar 2025 00:10:14 +0900 Subject: [PATCH 29/59] :hammer: Implement get_auth_account --- application/src/service/account.rs | 9 +++- application/src/service/auth_account.rs | 67 +++++++++++++++++++++++-- 2 files changed, 70 insertions(+), 6 deletions(-) diff --git a/application/src/service/account.rs b/application/src/service/account.rs index 80032e4..6de069d 100644 --- a/application/src/service/account.rs +++ b/application/src/service/account.rs @@ -3,9 +3,12 @@ use crate::transfer::account::AccountDto; use crate::transfer::auth_account::AuthAccountInfo; use crate::transfer::pagination::{apply_pagination, Pagination}; use kernel::interfaces::database::DatabaseConnection; -use kernel::interfaces::modify::{DependOnAuthAccountModifier, DependOnAuthHostModifier}; +use kernel::interfaces::modify::{ + DependOnAuthAccountModifier, DependOnAuthHostModifier, DependOnEventModifier, +}; use kernel::interfaces::query::{ AccountQuery, DependOnAccountQuery, DependOnAuthAccountQuery, DependOnAuthHostQuery, + DependOnEventQuery, }; use kernel::prelude::entity::{Account, Nanoid}; use kernel::KernelError; @@ -20,6 +23,8 @@ pub trait GetAccountService: + DependOnAuthAccountModifier + DependOnAuthHostQuery + DependOnAuthHostModifier + + DependOnEventModifier + + DependOnEventQuery { fn get_all_accounts( &self, @@ -58,5 +63,7 @@ impl GetAccountService for T where + DependOnAuthAccountModifier + DependOnAuthHostQuery + DependOnAuthHostModifier + + DependOnEventModifier + + DependOnEventQuery { } diff --git a/application/src/service/auth_account.rs b/application/src/service/auth_account.rs index df1f7ee..33f5a05 100644 --- a/application/src/service/auth_account.rs +++ b/application/src/service/auth_account.rs @@ -1,16 +1,22 @@ use crate::transfer::auth_account::AuthAccountInfo; +use error_stack::Report; use kernel::interfaces::database::{DatabaseConnection, DependOnDatabaseConnection}; +use kernel::interfaces::event::EventApplier; use kernel::interfaces::modify::{ - AuthHostModifier, DependOnAuthAccountModifier, DependOnAuthHostModifier, + AuthAccountModifier, AuthHostModifier, DependOnAuthAccountModifier, DependOnAuthHostModifier, + DependOnEventModifier, EventModifier, }; use kernel::interfaces::query::{ AuthAccountQuery, AuthHostQuery, DependOnAuthAccountQuery, DependOnAuthHostQuery, + DependOnEventQuery, EventQuery, }; use kernel::prelude::entity::{ - AuthAccount, AuthAccountClientId, AuthAccountId, AuthHost, AuthHostId, AuthHostUrl, + AuthAccount, AuthAccountClientId, AuthAccountId, AuthHost, AuthHostId, AuthHostUrl, EventId, }; use kernel::KernelError; +/// Get an auth account by the host URL and client ID. +/// If the auth account does not exist, it will be created. pub(crate) async fn get_auth_account< S: 'static + DependOnDatabaseConnection @@ -18,6 +24,8 @@ pub(crate) async fn get_auth_account< + DependOnAuthAccountModifier + DependOnAuthHostQuery + DependOnAuthHostModifier + + DependOnEventModifier + + DependOnEventQuery + ?Sized, >( service: &S, @@ -33,7 +41,34 @@ pub(crate) async fn get_auth_account< .find_by_client_id(&mut transaction, &client_id) .await?; let auth_account = if let Some(auth_account) = auth_account { - auth_account + // この辺の処理を共通化してUpdateAndGetを実装できるかもしれない + let event_id = EventId::from(auth_account.id().clone()); + let events = service + .event_query() + .find_by_id(&mut transaction, &event_id, Some(auth_account.version())) + .await?; + if events + .last() + .map(|event| &event.version != auth_account.version()) + .unwrap_or_else(|| false) + { + let mut auth_account = Some(auth_account); + for event in events { + AuthAccount::apply(&mut auth_account, event)?; + } + if let Some(auth_account) = auth_account { + service + .auth_account_modifier() + .update(&mut transaction, &auth_account) + .await?; + auth_account + } else { + return Err(Report::new(KernelError::Internal) + .attach_printable("Failed to get auth account")); + } + } else { + auth_account + } } else { let url = AuthHostUrl::new(host); let auth_host = service @@ -51,8 +86,30 @@ pub(crate) async fn get_auth_account< auth_host }; let host_id = auth_host.into_destruct().id; - let auth_account = AuthAccount::create(AuthAccountId::default(), host_id, client_id); - todo!() + let create_command = AuthAccount::create(AuthAccountId::default(), host_id, client_id); + service + .event_modifier() + .handle(&mut transaction, &create_command) + .await?; + let event_id = create_command.id(); + let events = service + .event_query() + .find_by_id(&mut transaction, event_id, None) + .await?; + let mut auth_account = None; + for event in events { + AuthAccount::apply(&mut auth_account, event)?; + } + if let Some(auth_account) = auth_account { + service + .auth_account_modifier() + .create(&mut transaction, &auth_account) + .await?; + auth_account + } else { + return Err(Report::new(KernelError::Internal) + .attach_printable("Failed to create auth account")); + } }; Ok(auth_account) } From 30397c381c9c63386f63c74390b42a33c4026afe Mon Sep 17 00:00:00 2001 From: turtton Date: Sun, 23 Mar 2025 11:01:17 +0900 Subject: [PATCH 30/59] :hammer: Add redis database --- driver/src/database.rs | 5 ++++- driver/src/database/redis.rs | 34 ++++++++++++++++++++++++++++++++++ server/src/handler.rs | 23 +++++------------------ 3 files changed, 43 insertions(+), 19 deletions(-) create mode 100644 driver/src/database/redis.rs diff --git a/driver/src/database.rs b/driver/src/database.rs index ab71a54..e27c66b 100644 --- a/driver/src/database.rs +++ b/driver/src/database.rs @@ -1,10 +1,13 @@ mod postgres; +mod redis; use error_stack::Report; use kernel::KernelError; -pub use postgres::*; use std::env; +pub use postgres::*; +pub use redis::*; + pub(crate) fn env(key: &str) -> error_stack::Result, KernelError> { let result = dotenvy::var(key); match result { diff --git a/driver/src/database/redis.rs b/driver/src/database/redis.rs new file mode 100644 index 0000000..2754034 --- /dev/null +++ b/driver/src/database/redis.rs @@ -0,0 +1,34 @@ +use crate::database::env; +use deadpool_redis::{Config, Pool, Runtime}; +use error_stack::{Report, ResultExt}; +use kernel::KernelError; + +// redis://127.0.0.1 +const REDIS_URL: &str = "REDIS_URL"; + +// 127.0.0.1 +const HOST: &str = "REDIS_HOST"; + +#[derive(Clone)] +pub struct RedisDatabase { + pool: Pool, +} + +impl RedisDatabase { + pub fn new() -> error_stack::Result { + let url = if let Some(env) = env(REDIS_URL)? { + env + } else { + let host = env(HOST)?.ok_or_else(|| { + Report::new(KernelError::Internal) + .attach_printable(format!("Failed to get env: {HOST}")) + })?; + format!("redis://{}", host) + }; + let config = Config::from_url(&url); + let pool = config + .create_pool(Some(Runtime::Tokio1)) + .change_context_lazy(|| KernelError::Internal)?; + Ok(Self { pool }) + } +} diff --git a/server/src/handler.rs b/server/src/handler.rs index 8e1fe3e..a449b7e 100644 --- a/server/src/handler.rs +++ b/server/src/handler.rs @@ -1,4 +1,4 @@ -use driver::database::PostgresDatabase; +use driver::database::{PostgresDatabase, RedisDatabase}; use kernel::KernelError; use std::sync::Arc; use vodca::References; @@ -6,39 +6,26 @@ use vodca::References; #[derive(Clone, References)] pub struct AppModule { handler: Arc, - worker: Arc, } impl AppModule { pub async fn new() -> error_stack::Result { let handler = Arc::new(Handler::init().await?); - let worker = Arc::new(Worker::new(&handler)); - Ok(Self { handler, worker }) + Ok(Self { handler }) } } #[derive(References)] pub struct Handler { pgpool: PostgresDatabase, + redis: RedisDatabase, } impl Handler { pub async fn init() -> error_stack::Result { let pgpool = PostgresDatabase::new().await?; + let redis = RedisDatabase::new()?; - Ok(Self { pgpool }) - } -} - -#[derive(References)] -pub struct Worker { - // command: RedisMessageQueue, CommandOperation>, -} - -impl Worker { - pub fn new(handler: &Arc) -> Self { - // let command = init_command_worker(handler); - // Self { command } - Self {} + Ok(Self { pgpool, redis }) } } From 72418daccaa2cc7221a76f475e88995e3a02e784 Mon Sep 17 00:00:00 2001 From: turtton Date: Thu, 27 Mar 2025 22:05:24 +0900 Subject: [PATCH 31/59] :hammer: Implement DatabaseConnection for RedisConnection --- driver/Cargo.toml | 1 + driver/src/database/redis.rs | 30 +++++++++++++++++++++++++++++- 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/driver/Cargo.toml b/driver/Cargo.toml index 7643e2e..bbaa238 100644 --- a/driver/Cargo.toml +++ b/driver/Cargo.toml @@ -16,6 +16,7 @@ uuid = { workspace = true } time = { workspace = true } async-trait = "0.1" +vodca.workspace = true error-stack = { workspace = true } kernel = { path = "../kernel" } diff --git a/driver/src/database/redis.rs b/driver/src/database/redis.rs index 2754034..36ffe37 100644 --- a/driver/src/database/redis.rs +++ b/driver/src/database/redis.rs @@ -1,7 +1,10 @@ use crate::database::env; use deadpool_redis::{Config, Pool, Runtime}; use error_stack::{Report, ResultExt}; +use kernel::interfaces::database::{DatabaseConnection, Transaction}; use kernel::KernelError; +use std::ops::Deref; +use vodca::References; // redis://127.0.0.1 const REDIS_URL: &str = "REDIS_URL"; @@ -9,7 +12,7 @@ const REDIS_URL: &str = "REDIS_URL"; // 127.0.0.1 const HOST: &str = "REDIS_HOST"; -#[derive(Clone)] +#[derive(Clone, References)] pub struct RedisDatabase { pool: Pool, } @@ -32,3 +35,28 @@ impl RedisDatabase { Ok(Self { pool }) } } + +pub struct RedisConnection(deadpool_redis::Connection); + +impl Transaction for RedisConnection {} + +impl Deref for RedisConnection { + type Target = deadpool_redis::Connection; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DatabaseConnection for RedisDatabase { + type Transaction = RedisConnection; + + async fn begin_transaction(&self) -> error_stack::Result { + let pool = self + .pool + .get() + .await + .change_context_lazy(|| KernelError::Internal)?; + Ok(RedisConnection(pool)) + } +} From 14f3290d466cd353593fcec0ad199ea8f7bb9813 Mon Sep 17 00:00:00 2001 From: turtton Date: Fri, 28 Mar 2025 00:26:31 +0900 Subject: [PATCH 32/59] :hammer: Add Signal system --- Cargo.lock | 81 +++++---------------------- Cargo.toml | 1 + application/src/service/account.rs | 65 ++++++++++++++++++++- driver/Cargo.toml | 2 +- kernel/src/lib.rs | 5 ++ kernel/src/signal.rs | 6 ++ server/Cargo.toml | 3 +- server/src/applier.rs | 1 + server/src/applier/account_applier.rs | 45 +++++++++++++++ server/src/main.rs | 1 + 10 files changed, 137 insertions(+), 73 deletions(-) create mode 100644 kernel/src/signal.rs create mode 100644 server/src/applier.rs create mode 100644 server/src/applier/account_applier.rs diff --git a/Cargo.lock b/Cargo.lock index 3030142..02b7cb6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -576,19 +576,6 @@ dependencies = [ "syn 2.0.96", ] -[[package]] -name = "deadpool" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "421fe0f90f2ab22016f32a9881be5134fdd71c65298917084b0c7477cbc3856e" -dependencies = [ - "async-trait", - "deadpool-runtime", - "num_cpus", - "retain_mut", - "tokio", -] - [[package]] name = "deadpool" version = "0.12.1" @@ -602,22 +589,12 @@ dependencies = [ [[package]] name = "deadpool-redis" -version = "0.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5f1760f60ffc6653b4afd924c5792098d8c00d9a3deb6b3d989eac17949dc422" -dependencies = [ - "deadpool 0.9.5", - "redis 0.23.3", -] - -[[package]] -name = "deadpool-redis" -version = "0.18.0" +version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfae6799b68a735270e4344ee3e834365f707c72da09c9a8bb89b45cc3351395" +checksum = "c136f185b3ca9d1f4e4e19c11570e1002f4bfdd592d589053e225716d613851f" dependencies = [ - "deadpool 0.12.1", - "redis 0.27.4", + "deadpool", + "redis", ] [[package]] @@ -694,7 +671,7 @@ name = "driver" version = "0.1.0" dependencies = [ "async-trait", - "deadpool-redis 0.12.0", + "deadpool-redis", "dotenvy", "error-stack", "kernel", @@ -705,6 +682,7 @@ dependencies = [ "time", "tokio", "uuid", + "vodca", ] [[package]] @@ -2116,31 +2094,11 @@ dependencies = [ [[package]] name = "redis" -version = "0.23.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f49cdc0bb3f412bf8e7d1bd90fe1d9eb10bc5c399ba90973c14662a27b3f8ba" -dependencies = [ - "async-trait", - "bytes", - "combine", - "futures-util", - "itoa", - "percent-encoding", - "pin-project-lite", - "ryu", - "tokio", - "tokio-util", - "url", -] - -[[package]] -name = "redis" -version = "0.27.4" +version = "0.29.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc6baebe319ef5e4b470f248335620098d1c2e9261e995be05f56f719ca4bdb2" +checksum = "b110459d6e323b7cda23980c46c77157601199c9da6241552b284cd565a7a133" dependencies = [ "arc-swap", - "async-trait", "bytes", "combine", "futures-util", @@ -2149,7 +2107,6 @@ dependencies = [ "percent-encoding", "pin-project-lite", "ryu", - "sha1_smol", "socket2 0.5.7", "tokio", "tokio-util", @@ -2272,24 +2229,17 @@ dependencies = [ "windows-registry", ] -[[package]] -name = "retain_mut" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4389f1d5789befaf6029ebd9f7dac4af7f7e3d61b69d4f30e2ac02b57e7712b0" - [[package]] name = "rikka-mq" -version = "0.1.1" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7839049a440c6c96eb8acfffcc65c580276d6ea6f8c029af36f29bd57b8401f9" +checksum = "52886d21f46f54dfa1ba31bc734f164940d8acca1c666d45114c01c6077725ad" dependencies = [ - "deadpool-redis 0.18.0", + "deadpool-redis", "destructure", - "redis 0.27.4", "serde", "serde_json", - "thiserror 1.0.63", + "thiserror 2.0.11", "tokio", "tracing", ] @@ -2636,6 +2586,7 @@ dependencies = [ "tracing-appender", "tracing-subscriber", "url", + "uuid", "vodca", ] @@ -2650,12 +2601,6 @@ dependencies = [ "digest", ] -[[package]] -name = "sha1_smol" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" - [[package]] name = "sha2" version = "0.10.8" diff --git a/Cargo.toml b/Cargo.toml index 13b5660..e8f9c16 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ dotenvy = "0.15.7" vodca = "0.1.8" destructure = "0.5.6" +qualified_do = "0.1.0" error-stack = "0.4.1" tracing = "0.1.40" diff --git a/application/src/service/account.rs b/application/src/service/account.rs index 6de069d..4796725 100644 --- a/application/src/service/account.rs +++ b/application/src/service/account.rs @@ -3,14 +3,16 @@ use crate::transfer::account::AccountDto; use crate::transfer::auth_account::AuthAccountInfo; use crate::transfer::pagination::{apply_pagination, Pagination}; use kernel::interfaces::database::DatabaseConnection; +use kernel::interfaces::event::EventApplier; use kernel::interfaces::modify::{ - DependOnAuthAccountModifier, DependOnAuthHostModifier, DependOnEventModifier, + AccountModifier, DependOnAccountModifier, DependOnAuthAccountModifier, + DependOnAuthHostModifier, DependOnEventModifier, }; use kernel::interfaces::query::{ AccountQuery, DependOnAccountQuery, DependOnAuthAccountQuery, DependOnAuthHostQuery, - DependOnEventQuery, + DependOnEventQuery, EventQuery, }; -use kernel::prelude::entity::{Account, Nanoid}; +use kernel::prelude::entity::{Account, AccountId, EventId, Nanoid}; use kernel::KernelError; use std::future::Future; @@ -67,3 +69,60 @@ impl GetAccountService for T where + DependOnEventQuery { } + +pub trait UpdateAccountService: + 'static + + DependOnAccountQuery + + DependOnAccountModifier + + DependOnEventQuery + + DependOnEventModifier +{ + fn update_account( + &self, + account_id: AccountId, + ) -> impl Future> { + async move { + let mut transaction = self.database_connection().begin_transaction().await?; + let account = self + .account_query() + .find_by_id(&mut transaction, &account_id) + .await?; + if let Some(account) = account { + let event_id = EventId::from(account_id.clone()); + let events = self + .event_query() + .find_by_id(&mut transaction, &event_id, Some(account.version())) + .await?; + if events.is_empty() { + return Ok(()); + } + let mut account = Some(account); + for event in events { + Account::apply(&mut account, event)?; + } + if let Some(account) = account { + self.account_modifier() + .update(&mut transaction, &account) + .await?; + } else { + self.account_modifier() + .delete(&mut transaction, &account_id) + .await?; + } + Ok(()) + } else { + Err(error_stack::Report::new(KernelError::Internal) + .attach_printable(format!("Failed to get target account: {account_id:?}"))) + } + } + } +} + +impl UpdateAccountService for T where + T: 'static + + DependOnAccountQuery + + DependOnAccountModifier + + DependOnEventQuery + + DependOnEventModifier +{ +} diff --git a/driver/Cargo.toml b/driver/Cargo.toml index bbaa238..2a9ae65 100644 --- a/driver/Cargo.toml +++ b/driver/Cargo.toml @@ -8,7 +8,7 @@ authors.workspace = true [dependencies] dotenvy = { workspace = true } -deadpool-redis = "0.12.0" +deadpool-redis = "0.20.0" sqlx = { version = "0.7", features = ["uuid", "time", "postgres", "runtime-tokio-native-tls", "json"] } serde_json = "1" serde = { workspace = true } diff --git a/kernel/src/lib.rs b/kernel/src/lib.rs index cd6b287..4f0a600 100644 --- a/kernel/src/lib.rs +++ b/kernel/src/lib.rs @@ -4,6 +4,7 @@ mod error; mod event; mod modify; mod query; +mod signal; pub use self::error::*; @@ -28,4 +29,8 @@ pub mod interfaces { pub mod event { pub use crate::event::*; } + + pub mod signal { + pub use crate::signal::*; + } } diff --git a/kernel/src/signal.rs b/kernel/src/signal.rs new file mode 100644 index 0000000..0c9294f --- /dev/null +++ b/kernel/src/signal.rs @@ -0,0 +1,6 @@ +use crate::KernelError; +use std::future::Future; + +pub trait Signal { + fn emit(&self, signal_id: ID) -> impl Future>; +} diff --git a/server/Cargo.toml b/server/Cargo.toml index 2ac693c..2d8cad1 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -8,6 +8,7 @@ authors.workspace = true [dependencies] time = { workspace = true } +uuid = { workspace = true, features = ["v4"] } rand = "0.8" rand_chacha = "0.3" sha2 = "0.10" @@ -24,7 +25,7 @@ axum-keycloak-auth = "0.6.0" tower-http = { version = "0.5.1", features = ["tokio", "cors"] } tokio = { workspace = true, features = ["rt", "rt-multi-thread", "macros"] } -rikka-mq = { version = "0.1.1", features = ["redis", "tracing"] } +rikka-mq = { version = "0.1.3", features = ["redis", "tracing"] } error-stack = { workspace = true } vodca = { workspace = true } diff --git a/server/src/applier.rs b/server/src/applier.rs new file mode 100644 index 0000000..d8f058c --- /dev/null +++ b/server/src/applier.rs @@ -0,0 +1 @@ +mod account_applier; diff --git a/server/src/applier/account_applier.rs b/server/src/applier/account_applier.rs new file mode 100644 index 0000000..c5befb4 --- /dev/null +++ b/server/src/applier/account_applier.rs @@ -0,0 +1,45 @@ +use crate::handler::AppModule; +use application::service::account::UpdateAccountService; +use error_stack::ResultExt; +use kernel::interfaces::signal::Signal; +use kernel::prelude::entity::AccountId; +use kernel::KernelError; +use rikka_mq::config::MQConfig; +use rikka_mq::define::redis::mq::RedisMessageQueue; +use rikka_mq::error::ErrorOperation; +use rikka_mq::info::QueueInfo; +use rikka_mq::mq::MessageQueue; +use uuid::Uuid; + +struct AccountApplier(RedisMessageQueue); + +impl AccountApplier { + fn new(module: AppModule) -> Self { + let queue = RedisMessageQueue::new( + module.handler().redis().pool().clone(), + module.clone(), + "account_applier".to_string(), + MQConfig::default(), + Uuid::new_v4, + |module: AppModule, id: AccountId| async move { + module + .handler() + .pgpool() + .update_account(id) + .await + .map_err(|e| ErrorOperation::Delay(format!("{:?}", e))) + }, + ); + AccountApplier(queue) + } +} + +impl Signal for AccountApplier { + async fn emit(&self, signal_id: AccountId) -> error_stack::Result<(), KernelError> { + self.0 + .queue(QueueInfo::new(Uuid::new_v4(), signal_id)) + .await + .map_err(|e| error_stack::Report::new(e)) + .change_context_lazy(|| KernelError::Internal) + } +} diff --git a/server/src/main.rs b/server/src/main.rs index 9ded590..3bb8d0b 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,3 +1,4 @@ +mod applier; mod error; mod handler; mod keycloak; From 5af3c72a4013ee68c3a59d83fbf4ff2864f274d7 Mon Sep 17 00:00:00 2001 From: turtton Date: Sat, 12 Apr 2025 20:23:58 +0900 Subject: [PATCH 33/59] :hammer: Add persist_and_emit in command handler --- application/src/service/auth_account.rs | 13 +-- driver/src/database/postgres/event.rs | 107 ++++++++++++++++++++---- kernel/src/modify/event.rs | 12 ++- 3 files changed, 101 insertions(+), 31 deletions(-) diff --git a/application/src/service/auth_account.rs b/application/src/service/auth_account.rs index 33f5a05..dfd88c1 100644 --- a/application/src/service/auth_account.rs +++ b/application/src/service/auth_account.rs @@ -87,19 +87,12 @@ pub(crate) async fn get_auth_account< }; let host_id = auth_host.into_destruct().id; let create_command = AuthAccount::create(AuthAccountId::default(), host_id, client_id); - service + let create_event = service .event_modifier() - .handle(&mut transaction, &create_command) - .await?; - let event_id = create_command.id(); - let events = service - .event_query() - .find_by_id(&mut transaction, event_id, None) + .persist_and_transform(&mut transaction, create_command) .await?; let mut auth_account = None; - for event in events { - AuthAccount::apply(&mut auth_account, event)?; - } + AuthAccount::apply(&mut auth_account, create_event)?; if let Some(auth_account) = auth_account { service .auth_account_modifier() diff --git a/driver/src/database/postgres/event.rs b/driver/src/database/postgres/event.rs index 6834264..17b8abc 100644 --- a/driver/src/database/postgres/event.rs +++ b/driver/src/database/postgres/event.rs @@ -90,16 +90,48 @@ impl DependOnEventQuery for PostgresDatabase { impl EventModifier for PostgresEventRepository { type Transaction = PostgresConnection; - async fn handle( + async fn persist( &self, transaction: &mut Self::Transaction, - event: &CommandEnvelope, + command: &CommandEnvelope, + ) -> error_stack::Result<(), KernelError> { + self.persist_internal(transaction, command, Uuid::now_v7()) + .await?; + Ok(()) + } + + async fn persist_and_transform( + &self, + transaction: &mut Self::Transaction, + command: CommandEnvelope, + ) -> error_stack::Result, KernelError> { + let version = Uuid::now_v7(); + + self.persist_internal(transaction, &command, version) + .await?; + + let command = command.into_destruct(); + Ok(EventEnvelope::new( + command.id, + command.event, + EventVersion::new(version), + )) + } +} + +impl PostgresEventRepository { + async fn persist_internal( + &self, + transaction: &mut PostgresConnection, + command: &CommandEnvelope, + version: Uuid, ) -> error_stack::Result<(), KernelError> { let con: &mut PgConnection = transaction; - let event_name = event.event_name(); - let version = event.prev_version().as_ref(); - if let Some(prev_version) = version { + // Validation logic + let event_name = command.event_name(); + let prev_version = command.prev_version().as_ref(); + if let Some(prev_version) = prev_version { match prev_version { KnownEventVersion::Nothing => { let amount = sqlx::query_as::<_, CountRow>( @@ -110,13 +142,13 @@ impl EventModifier for PostgresEventRepository { WHERE id = $1 "#, ) - .bind(event.id().as_ref()) + .bind(command.id().as_ref()) .fetch_one(&mut *con) .await .convert_error()?; if amount.count != 0 { return Err(Report::new(KernelError::Concurrency).attach_printable( - format!("Event {} already exists", event.id().as_ref()), + format!("Event {} already exists", command.id().as_ref()), )); } } @@ -131,7 +163,7 @@ impl EventModifier for PostgresEventRepository { LIMIT 1 "#, ) - .bind(event.id().as_ref()) + .bind(command.id().as_ref()) .fetch_optional(&mut *con) .await .convert_error()?; @@ -142,7 +174,7 @@ impl EventModifier for PostgresEventRepository { return Err(Report::new(KernelError::Concurrency).attach_printable( format!( "Event {} version {} already exists", - event.id().as_ref(), + command.id().as_ref(), prev_version.as_ref() ), )); @@ -150,6 +182,8 @@ impl EventModifier for PostgresEventRepository { } }; } + + // Insertion logic sqlx::query( //language=postgresql r#" @@ -157,13 +191,14 @@ impl EventModifier for PostgresEventRepository { VALUES ($1, $2, $3, $4) "#, ) - .bind(Uuid::now_v7()) - .bind(event.id().as_ref()) + .bind(version) + .bind(command.id().as_ref()) .bind(event_name) - .bind(serde_json::to_value(event.event()).convert_error()?) + .bind(serde_json::to_value(command.event()).convert_error()?) .execute(con) .await .convert_error()?; + Ok(()) } } @@ -216,15 +251,15 @@ mod test { let deleted_account = Account::delete(account_id.clone()); db.event_modifier() - .handle(&mut transaction, &created_account) + .persist(&mut transaction, &created_account) .await .unwrap(); db.event_modifier() - .handle(&mut transaction, &updated_account) + .persist(&mut transaction, &updated_account) .await .unwrap(); db.event_modifier() - .handle(&mut transaction, &deleted_account) + .persist(&mut transaction, &deleted_account) .await .unwrap(); let events = db @@ -256,11 +291,11 @@ mod test { ); let updated_account = Account::update(account_id.clone(), AccountIsBot::new(true)); db.event_modifier() - .handle(&mut transaction, &created_account) + .persist(&mut transaction, &created_account) .await .unwrap(); db.event_modifier() - .handle(&mut transaction, &updated_account) + .persist(&mut transaction, &updated_account) .await .unwrap(); @@ -308,7 +343,7 @@ mod test { Nanoid::default(), ); db.event_modifier() - .handle(&mut transaction, &created_account) + .persist(&mut transaction, &created_account) .await .unwrap(); let events = db @@ -318,5 +353,41 @@ mod test { .unwrap(); assert_eq!(events.len(), 1); } + + #[test_with::env(DATABASE_URL)] + #[tokio::test] + async fn persist_and_transform_test() { + let db = PostgresDatabase::new().await.unwrap(); + let mut transaction = db.begin_transaction().await.unwrap(); + let account_id = AccountId::new(Uuid::now_v7()); + let created_account = Account::create( + account_id.clone(), + AccountName::new("test"), + AccountPrivateKey::new("test"), + AccountPublicKey::new("test"), + AccountIsBot::new(false), + Nanoid::default(), + ); + + // Test persist_and_transform + let event_envelope = db + .event_modifier() + .persist_and_transform(&mut transaction, created_account.clone()) + .await + .unwrap(); + + // Verify the returned envelope + assert_eq!(event_envelope.id, EventId::from(account_id.clone())); + assert_eq!(&event_envelope.event, created_account.event()); + + // Verify it was persisted + let events = db + .event_query() + .find_by_id(&mut transaction, &EventId::from(account_id), None) + .await + .unwrap(); + assert_eq!(events.len(), 1); + assert_eq!(&events[0].event, created_account.event()); + } } } diff --git a/kernel/src/modify/event.rs b/kernel/src/modify/event.rs index db43658..7c5a118 100644 --- a/kernel/src/modify/event.rs +++ b/kernel/src/modify/event.rs @@ -1,5 +1,5 @@ use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Transaction}; -use crate::entity::CommandEnvelope; +use crate::entity::{CommandEnvelope, EventEnvelope}; use crate::KernelError; use serde::Serialize; use std::future::Future; @@ -7,11 +7,17 @@ use std::future::Future; pub trait EventModifier: 'static + Sync + Send { type Transaction: Transaction; - fn handle( + fn persist( &self, transaction: &mut Self::Transaction, - event: &CommandEnvelope, + command: &CommandEnvelope, ) -> impl Future> + Send; + + fn persist_and_transform( + &self, + transaction: &mut Self::Transaction, + command: CommandEnvelope, + ) -> impl Future, KernelError>> + Send; } pub trait DependOnEventModifier: Sync + Send + DependOnDatabaseConnection { From edeb399a4a2cbeea419af1c6f410cf4c5f154c1c Mon Sep 17 00:00:00 2001 From: turtton Date: Sat, 12 Apr 2025 23:21:54 +0900 Subject: [PATCH 34/59] :hammer: Implement event applier system --- application/src/service/account.rs | 6 +- application/src/service/auth_account.rs | 101 +++++++++++++++------ server/src/applier.rs | 35 +++++++ server/src/applier/account_applier.rs | 16 ++-- server/src/applier/auth_account_applier.rs | 41 +++++++++ server/src/handler.rs | 8 +- server/src/route/account.rs | 7 +- 7 files changed, 173 insertions(+), 41 deletions(-) create mode 100644 server/src/applier/auth_account_applier.rs diff --git a/application/src/service/account.rs b/application/src/service/account.rs index 4796725..23aa69a 100644 --- a/application/src/service/account.rs +++ b/application/src/service/account.rs @@ -12,7 +12,8 @@ use kernel::interfaces::query::{ AccountQuery, DependOnAccountQuery, DependOnAuthAccountQuery, DependOnAuthHostQuery, DependOnEventQuery, EventQuery, }; -use kernel::prelude::entity::{Account, AccountId, EventId, Nanoid}; +use kernel::interfaces::signal::Signal; +use kernel::prelude::entity::{Account, AccountId, AuthAccountId, EventId, Nanoid}; use kernel::KernelError; use std::future::Future; @@ -30,6 +31,7 @@ pub trait GetAccountService: { fn get_all_accounts( &self, + signal: &impl Signal, account: AuthAccountInfo, Pagination { direction, @@ -38,7 +40,7 @@ pub trait GetAccountService: }: Pagination, ) -> impl Future>, KernelError>> { async move { - let auth_account = get_auth_account(self, account).await?; + let auth_account = get_auth_account(self, signal, account).await?; let mut transaction = self.database_connection().begin_transaction().await?; let accounts = self .account_query() diff --git a/application/src/service/auth_account.rs b/application/src/service/auth_account.rs index dfd88c1..03f7747 100644 --- a/application/src/service/auth_account.rs +++ b/application/src/service/auth_account.rs @@ -10,10 +10,77 @@ use kernel::interfaces::query::{ AuthAccountQuery, AuthHostQuery, DependOnAuthAccountQuery, DependOnAuthHostQuery, DependOnEventQuery, EventQuery, }; +use kernel::interfaces::signal::Signal; use kernel::prelude::entity::{ AuthAccount, AuthAccountClientId, AuthAccountId, AuthHost, AuthHostId, AuthHostUrl, EventId, }; use kernel::KernelError; +use std::future::Future; + +pub trait UpdateAuthAccount: + 'static + + DependOnDatabaseConnection + + DependOnAuthAccountQuery + + DependOnAuthAccountModifier + + DependOnAuthHostQuery + + DependOnAuthHostModifier + + DependOnEventQuery +{ + /// Update the auth account from events + fn update_auth_account( + &self, + auth_account_id: AuthAccountId, + ) -> impl Future> { + async move { + let mut transaction = self.database_connection().begin_transaction().await?; + let auth_account = self + .auth_account_query() + .find_by_id(&mut transaction, &auth_account_id) + .await?; + if let Some(auth_account) = auth_account { + let event_id = EventId::from(auth_account.id().clone()); + let events = self + .event_query() + .find_by_id(&mut transaction, &event_id, Some(auth_account.version())) + .await?; + if events + .last() + .map(|event| &event.version != auth_account.version()) + .unwrap_or_else(|| false) + { + let mut auth_account = Some(auth_account); + for event in events { + AuthAccount::apply(&mut auth_account, event)?; + } + if let Some(auth_account) = auth_account { + self.auth_account_modifier() + .update(&mut transaction, &auth_account) + .await?; + } else { + return Err(Report::new(KernelError::Internal) + .attach_printable("Failed to get auth account")); + } + } + Ok(()) + } else { + Err(Report::new(KernelError::Internal).attach_printable(format!( + "Failed to get target auth account: {auth_account_id:?}" + ))) + } + } + } +} + +impl UpdateAuthAccount for T where + T: 'static + + DependOnDatabaseConnection + + DependOnAuthAccountQuery + + DependOnAuthAccountModifier + + DependOnAuthHostQuery + + DependOnAuthHostModifier + + DependOnEventQuery +{ +} /// Get an auth account by the host URL and client ID. /// If the auth account does not exist, it will be created. @@ -29,6 +96,7 @@ pub(crate) async fn get_auth_account< + ?Sized, >( service: &S, + signal: &impl Signal, AuthAccountInfo { host_url: host, client_id, @@ -41,34 +109,7 @@ pub(crate) async fn get_auth_account< .find_by_client_id(&mut transaction, &client_id) .await?; let auth_account = if let Some(auth_account) = auth_account { - // この辺の処理を共通化してUpdateAndGetを実装できるかもしれない - let event_id = EventId::from(auth_account.id().clone()); - let events = service - .event_query() - .find_by_id(&mut transaction, &event_id, Some(auth_account.version())) - .await?; - if events - .last() - .map(|event| &event.version != auth_account.version()) - .unwrap_or_else(|| false) - { - let mut auth_account = Some(auth_account); - for event in events { - AuthAccount::apply(&mut auth_account, event)?; - } - if let Some(auth_account) = auth_account { - service - .auth_account_modifier() - .update(&mut transaction, &auth_account) - .await?; - auth_account - } else { - return Err(Report::new(KernelError::Internal) - .attach_printable("Failed to get auth account")); - } - } else { - auth_account - } + auth_account } else { let url = AuthHostUrl::new(host); let auth_host = service @@ -86,11 +127,13 @@ pub(crate) async fn get_auth_account< auth_host }; let host_id = auth_host.into_destruct().id; - let create_command = AuthAccount::create(AuthAccountId::default(), host_id, client_id); + let auth_account_id = AuthAccountId::default(); + let create_command = AuthAccount::create(auth_account_id.clone(), host_id, client_id); let create_event = service .event_modifier() .persist_and_transform(&mut transaction, create_command) .await?; + signal.emit(auth_account_id.clone()).await?; let mut auth_account = None; AuthAccount::apply(&mut auth_account, create_event)?; if let Some(auth_account) = auth_account { diff --git a/server/src/applier.rs b/server/src/applier.rs index d8f058c..79292f1 100644 --- a/server/src/applier.rs +++ b/server/src/applier.rs @@ -1 +1,36 @@ +use crate::handler::Handler; +use account_applier::AccountApplier; +use auth_account_applier::AuthAccountApplier; +use kernel::interfaces::signal::Signal; +use kernel::prelude::entity::{AccountId, AuthAccountId}; +use std::sync::Arc; + mod account_applier; +mod auth_account_applier; + +pub(crate) struct ApplierContainer { + account_applier: AccountApplier, + auth_account_applier: AuthAccountApplier, +} + +impl ApplierContainer { + pub fn new(module: Arc) -> Self { + Self { + account_applier: AccountApplier::new(module.clone()), + auth_account_applier: AuthAccountApplier::new(module.clone()), + } + } +} + +macro_rules! impl_signal { + ($type:ty, $field:ident) => { + impl Signal<$type> for ApplierContainer { + async fn emit(&self, signal_id: $type) -> error_stack::Result<(), kernel::KernelError> { + self.$field.emit(signal_id).await + } + } + }; +} + +impl_signal!(AccountId, account_applier); +impl_signal!(AuthAccountId, auth_account_applier); diff --git a/server/src/applier/account_applier.rs b/server/src/applier/account_applier.rs index c5befb4..87cff5c 100644 --- a/server/src/applier/account_applier.rs +++ b/server/src/applier/account_applier.rs @@ -1,4 +1,4 @@ -use crate::handler::AppModule; +use crate::handler::Handler; use application::service::account::UpdateAccountService; use error_stack::ResultExt; use kernel::interfaces::signal::Signal; @@ -9,21 +9,21 @@ use rikka_mq::define::redis::mq::RedisMessageQueue; use rikka_mq::error::ErrorOperation; use rikka_mq::info::QueueInfo; use rikka_mq::mq::MessageQueue; +use std::sync::Arc; use uuid::Uuid; -struct AccountApplier(RedisMessageQueue); +pub(crate) struct AccountApplier(RedisMessageQueue, Uuid, AccountId>); impl AccountApplier { - fn new(module: AppModule) -> Self { + pub fn new(handler: Arc) -> Self { let queue = RedisMessageQueue::new( - module.handler().redis().pool().clone(), - module.clone(), + handler.redis().pool().clone(), + handler, "account_applier".to_string(), MQConfig::default(), Uuid::new_v4, - |module: AppModule, id: AccountId| async move { - module - .handler() + |handler: Arc, id: AccountId| async move { + handler .pgpool() .update_account(id) .await diff --git a/server/src/applier/auth_account_applier.rs b/server/src/applier/auth_account_applier.rs new file mode 100644 index 0000000..9717393 --- /dev/null +++ b/server/src/applier/auth_account_applier.rs @@ -0,0 +1,41 @@ +use crate::handler::Handler; +use application::service::auth_account::UpdateAuthAccount; +use error_stack::ResultExt; +use kernel::interfaces::signal::Signal; +use kernel::prelude::entity::AuthAccountId; +use rikka_mq::define::redis::mq::RedisMessageQueue; +use rikka_mq::mq::MessageQueue; +use std::sync::Arc; +use uuid::Uuid; + +pub(crate) struct AuthAccountApplier(RedisMessageQueue, Uuid, AuthAccountId>); + +impl AuthAccountApplier { + pub fn new(handler: Arc) -> Self { + let queue = RedisMessageQueue::new( + handler.redis().pool().clone(), + handler, + "auth_account_applier".to_string(), + rikka_mq::config::MQConfig::default(), + Uuid::new_v4, + |handler: Arc, id: AuthAccountId| async move { + handler + .pgpool() + .update_auth_account(id) + .await + .map_err(|e| rikka_mq::error::ErrorOperation::Delay(format!("{:?}", e))) + }, + ); + AuthAccountApplier(queue) + } +} + +impl Signal for AuthAccountApplier { + async fn emit(&self, signal_id: AuthAccountId) -> error_stack::Result<(), kernel::KernelError> { + self.0 + .queue(rikka_mq::info::QueueInfo::new(Uuid::new_v4(), signal_id)) + .await + .map_err(|e| error_stack::Report::new(e)) + .change_context_lazy(|| kernel::KernelError::Internal) + } +} diff --git a/server/src/handler.rs b/server/src/handler.rs index a449b7e..f67350a 100644 --- a/server/src/handler.rs +++ b/server/src/handler.rs @@ -1,3 +1,4 @@ +use crate::applier::ApplierContainer; use driver::database::{PostgresDatabase, RedisDatabase}; use kernel::KernelError; use std::sync::Arc; @@ -6,12 +7,17 @@ use vodca::References; #[derive(Clone, References)] pub struct AppModule { handler: Arc, + applier_container: Arc, } impl AppModule { pub async fn new() -> error_stack::Result { let handler = Arc::new(Handler::init().await?); - Ok(Self { handler }) + let applier_container = Arc::new(ApplierContainer::new(handler.clone())); + Ok(Self { + handler, + applier_container, + }) } } diff --git a/server/src/route/account.rs b/server/src/route/account.rs index 8af16f9..b018162 100644 --- a/server/src/route/account.rs +++ b/server/src/route/account.rs @@ -14,6 +14,7 @@ use axum_keycloak_auth::instance::KeycloakAuthInstance; use axum_keycloak_auth::layer::KeycloakAuthLayer; use axum_keycloak_auth::PassthroughMode; use serde::{Deserialize, Serialize}; +use std::ops::Deref; use time::OffsetDateTime; #[derive(Debug, Deserialize)] @@ -60,7 +61,11 @@ async fn get_accounts( let result = module .handler() .pgpool() - .get_all_accounts(auth_info.into(), pagination) + .get_all_accounts( + module.applier_container().deref(), + auth_info.into(), + pagination, + ) .await .map_err(ErrorStatus::from)? .ok_or(ErrorStatus::from(StatusCode::NOT_FOUND))?; From 840b160545386ce4b751dd6da8bb73a4a1830145 Mon Sep 17 00:00:00 2001 From: turtton Date: Wed, 31 Dec 2025 19:59:48 +0900 Subject: [PATCH 35/59] :lock: Implement RSA-2048 key pair generation with encryption MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add crypto module with RSA-2048 key generation using rsa crate - Encrypt private keys with Argon2id key derivation + AES-256-GCM - Read master password from /run/secrets or ./master-key-password - Add security measures: - Zeroize sensitive data in memory - Validate password file permissions (Unix 0o600/0o400) - Use generic error messages to prevent timing attacks - Add KernelError::NotFound for proper HTTP 404 responses 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- Cargo.lock | 144 +++++++++++++++++ application/Cargo.toml | 11 ++ application/src/crypto/algorithm.rs | 18 +++ application/src/crypto/encryption.rs | 183 +++++++++++++++++++++ application/src/crypto/key_pair.rs | 32 ++++ application/src/crypto/mod.rs | 11 ++ application/src/crypto/password.rs | 177 +++++++++++++++++++++ application/src/crypto/rsa.rs | 122 ++++++++++++++ application/src/lib.rs | 1 + application/src/service/account.rs | 227 ++++++++++++++++++++++++++- kernel/src/error.rs | 4 + server/src/error.rs | 2 + 12 files changed, 929 insertions(+), 3 deletions(-) create mode 100644 application/src/crypto/algorithm.rs create mode 100644 application/src/crypto/encryption.rs create mode 100644 application/src/crypto/key_pair.rs create mode 100644 application/src/crypto/mod.rs create mode 100644 application/src/crypto/password.rs create mode 100644 application/src/crypto/rsa.rs diff --git a/Cargo.lock b/Cargo.lock index 02b7cb6..50f398f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,41 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array", +] + +[[package]] +name = "aes" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b169f7a6d4742236a0a00c541b845991d0ac43e546831af1249753ab4c3aa3a0" +dependencies = [ + "cfg-if", + "cipher", + "cpufeatures", +] + +[[package]] +name = "aes-gcm" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "831010a0f742e1209b3bcea8fab6a8e149051ba6099432c8cb2cc117dec3ead1" +dependencies = [ + "aead", + "aes", + "cipher", + "ctr", + "ghash", + "subtle", +] + [[package]] name = "ahash" version = "0.7.8" @@ -81,13 +116,22 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" name = "application" version = "0.1.0" dependencies = [ + "aes-gcm", + "argon2", + "base64 0.22.1", "driver", "error-stack", "kernel", + "rand", + "rsa", + "serde", + "serde_json", + "tempfile", "time", "tokio", "uuid", "vodca", + "zeroize", ] [[package]] @@ -96,6 +140,18 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +[[package]] +name = "argon2" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3c3610892ee6e0cbce8ae2700349fcf8f98adb0dbfbee85aec3c9179d29cc072" +dependencies = [ + "base64ct", + "blake2", + "cpufeatures", + "password-hash", +] + [[package]] name = "arrayvec" version = "0.7.6" @@ -312,6 +368,15 @@ dependencies = [ "wyz", ] +[[package]] +name = "blake2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46502ad458c9a52b69d4d4d32775c788b7a1b85e8bc9d482d92250fc0e3f8efe" +dependencies = [ + "digest", +] + [[package]] name = "block-buffer" version = "0.10.4" @@ -428,6 +493,16 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "cipher" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "773f3b9af64447d2ce9850330c473515014aa235e6a783b02db81ff39e4a3dad" +dependencies = [ + "crypto-common", + "inout", +] + [[package]] name = "combine" version = "4.6.7" @@ -538,9 +613,19 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array", + "rand_core", "typenum", ] +[[package]] +name = "ctr" +version = "0.9.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0369ee1ad671834580515889b80f2ea915f23b8be8d0daa4bbaf2ac5c7590835" +dependencies = [ + "cipher", +] + [[package]] name = "darling" version = "0.20.10" @@ -960,6 +1045,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "ghash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0d8a4362ccb29cb0b265253fb0a2728f592895ee6854fd9bc13f2ffda266ff1" +dependencies = [ + "opaque-debug", + "polyval", +] + [[package]] name = "gimli" version = "0.29.0" @@ -1394,6 +1489,15 @@ dependencies = [ "serde", ] +[[package]] +name = "inout" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "879f10e63c20629ecabbb64a8010319738c66a5cd0c29b02d63d272b03751d01" +dependencies = [ + "generic-array", +] + [[package]] name = "ipnet" version = "2.11.0" @@ -1792,6 +1896,12 @@ version = "1.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92" +[[package]] +name = "opaque-debug" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" + [[package]] name = "openssl" version = "0.10.66" @@ -1865,6 +1975,17 @@ dependencies = [ "windows-targets 0.52.6", ] +[[package]] +name = "password-hash" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "346f04948ba92c43e8469c1ee6736c7563d71012b17d40745260fe106aac2166" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "paste" version = "1.0.15" @@ -1946,6 +2067,18 @@ version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d231b230927b5e4ad203db57bbcbee2802f6bce620b1e4a9024a07d94e2907ec" +[[package]] +name = "polyval" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1fe60d06143b2430aa532c94cfe9e29783047f06c0d7fd359a9a51b729fa25" +dependencies = [ + "cfg-if", + "cpufeatures", + "opaque-debug", + "universal-hash", +] + [[package]] name = "portable-atomic" version = "1.10.0" @@ -2302,6 +2435,7 @@ dependencies = [ "pkcs1", "pkcs8", "rand_core", + "sha2", "signature", "spki", "subtle", @@ -3494,6 +3628,16 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39ec24b3121d976906ece63c9daad25b85969647682eee313cb5779fdd69e14e" +[[package]] +name = "universal-hash" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fc1de2c688dc15305988b563c3854064043356019f97a4b46276fe734c4f07ea" +dependencies = [ + "crypto-common", + "subtle", +] + [[package]] name = "untrusted" version = "0.9.0" diff --git a/application/Cargo.toml b/application/Cargo.toml index f48958a..72741ce 100644 --- a/application/Cargo.toml +++ b/application/Cargo.toml @@ -9,13 +9,24 @@ authors.workspace = true [dependencies] time = { workspace = true } vodca = { workspace = true } +serde = { workspace = true } error-stack = { workspace = true } +# Cryptography +rsa = { version = "0.9", features = ["sha2", "pem"] } +argon2 = "0.5" +aes-gcm = "0.10" +base64 = "0.22" +rand = "0.8" +serde_json = "1" +zeroize = "1.7" + kernel = { path = "../kernel" } [dev-dependencies] tokio = { workspace = true, features = ["macros", "test-util"] } +tempfile = "3" uuid.workspace = true diff --git a/application/src/crypto/algorithm.rs b/application/src/crypto/algorithm.rs new file mode 100644 index 0000000..6b15a7f --- /dev/null +++ b/application/src/crypto/algorithm.rs @@ -0,0 +1,18 @@ +use serde::{Deserialize, Serialize}; + +/// Supported key algorithms for account key pairs +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] +#[serde(rename_all = "lowercase")] +pub enum KeyAlgorithm { + #[default] + Rsa2048, + // Future: Ed25519, Rsa4096, etc. +} + +impl std::fmt::Display for KeyAlgorithm { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Rsa2048 => write!(f, "rsa2048"), + } + } +} diff --git a/application/src/crypto/encryption.rs b/application/src/crypto/encryption.rs new file mode 100644 index 0000000..bf17428 --- /dev/null +++ b/application/src/crypto/encryption.rs @@ -0,0 +1,183 @@ +use aes_gcm::{ + aead::{Aead, KeyInit}, + Aes256Gcm, Nonce, +}; +use argon2::Argon2; +use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; +use error_stack::{Report, Result}; +use kernel::KernelError; +use rand::{rngs::OsRng, RngCore}; +use zeroize::Zeroizing; + +use super::{algorithm::KeyAlgorithm, key_pair::EncryptedPrivateKey}; + +/// Argon2id parameters (OWASP recommended) +pub struct Argon2Params { + /// Memory cost in KiB (default: 64 MiB = 65536 KiB) + pub memory_cost: u32, + /// Number of iterations (default: 3) + pub time_cost: u32, + /// Degree of parallelism (default: 4) + pub parallelism: u32, +} + +impl Default for Argon2Params { + fn default() -> Self { + Self { + memory_cost: 65536, // 64 MiB + time_cost: 3, + parallelism: 4, + } + } +} + +/// Encrypt private key PEM with Argon2id key derivation and AES-256-GCM +pub fn encrypt_private_key( + private_key_pem: &[u8], + password: &[u8], + algorithm: KeyAlgorithm, + params: &Argon2Params, +) -> Result { + // Generate random salt (16 bytes) + let mut salt = [0u8; 16]; + OsRng.fill_bytes(&mut salt); + + // Derive key using Argon2id (32 bytes for AES-256) + let argon2_params = argon2::Params::new( + params.memory_cost, + params.time_cost, + params.parallelism, + Some(32), + ) + .map_err(|e| { + Report::new(KernelError::Internal) + .attach_printable(format!("Invalid Argon2 parameters: {e}")) + })?; + + let argon2 = Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, argon2_params); + + // Derived key is zeroized on drop + let mut derived_key = Zeroizing::new([0u8; 32]); + argon2 + .hash_password_into(password, &salt, &mut *derived_key) + .map_err(|e| { + Report::new(KernelError::Internal) + .attach_printable(format!("Argon2id key derivation failed: {e}")) + })?; + + // Encrypt with AES-256-GCM + let cipher = Aes256Gcm::new_from_slice(&*derived_key).map_err(|e| { + Report::new(KernelError::Internal) + .attach_printable(format!("Failed to create AES-GCM cipher: {e}")) + })?; + + let mut nonce_bytes = [0u8; 12]; + OsRng.fill_bytes(&mut nonce_bytes); + let nonce = Nonce::from_slice(&nonce_bytes); + + let ciphertext = cipher.encrypt(nonce, private_key_pem).map_err(|e| { + Report::new(KernelError::Internal) + .attach_printable(format!("AES-GCM encryption failed: {e}")) + })?; + + Ok(EncryptedPrivateKey { + ciphertext: BASE64.encode(&ciphertext), + nonce: BASE64.encode(nonce_bytes), + salt: BASE64.encode(salt), + algorithm, + }) +} + +/// Decrypt private key using the stored metadata and password +pub fn decrypt_private_key( + encrypted: &EncryptedPrivateKey, + password: &[u8], + params: &Argon2Params, +) -> Result, KernelError> { + // Decode Base64 fields (use generic error message to prevent information leakage) + let salt = BASE64.decode(&encrypted.salt).map_err(|_| { + Report::new(KernelError::Internal).attach_printable("Invalid encrypted data format") + })?; + + let nonce_bytes = BASE64.decode(&encrypted.nonce).map_err(|_| { + Report::new(KernelError::Internal).attach_printable("Invalid encrypted data format") + })?; + + let ciphertext = BASE64.decode(&encrypted.ciphertext).map_err(|_| { + Report::new(KernelError::Internal).attach_printable("Invalid encrypted data format") + })?; + + // Derive key using Argon2id + let argon2_params = argon2::Params::new( + params.memory_cost, + params.time_cost, + params.parallelism, + Some(32), + ) + .map_err(|e| { + Report::new(KernelError::Internal) + .attach_printable(format!("Invalid Argon2 parameters: {e}")) + })?; + + let argon2 = Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, argon2_params); + + // Derived key is zeroized on drop + let mut derived_key = Zeroizing::new([0u8; 32]); + argon2 + .hash_password_into(password, &salt, &mut *derived_key) + .map_err(|e| { + Report::new(KernelError::Internal) + .attach_printable(format!("Argon2id key derivation failed: {e}")) + })?; + + // Decrypt with AES-256-GCM + let cipher = Aes256Gcm::new_from_slice(&*derived_key).map_err(|e| { + Report::new(KernelError::Internal) + .attach_printable(format!("Failed to create AES-GCM cipher: {e}")) + })?; + + let nonce = Nonce::from_slice(&nonce_bytes); + + // Use generic error message to prevent timing attacks + cipher.decrypt(nonce, ciphertext.as_ref()).map_err(|_| { + Report::new(KernelError::Internal) + .attach_printable("Decryption failed: invalid password or corrupted data") + }) +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_encrypt_decrypt_roundtrip() { + let original = b"-----BEGIN PRIVATE KEY-----\ntest data\n-----END PRIVATE KEY-----"; + let password = b"test-password-123"; + let params = Argon2Params::default(); + + let encrypted = + encrypt_private_key(original, password, KeyAlgorithm::Rsa2048, ¶ms).unwrap(); + + assert!(!encrypted.ciphertext.is_empty()); + assert!(!encrypted.nonce.is_empty()); + assert!(!encrypted.salt.is_empty()); + assert_eq!(encrypted.algorithm, KeyAlgorithm::Rsa2048); + + let decrypted = decrypt_private_key(&encrypted, password, ¶ms).unwrap(); + assert_eq!(decrypted, original); + } + + #[test] + fn test_wrong_password_fails() { + let original = b"secret data"; + let password = b"correct-password"; + let wrong_password = b"wrong-password"; + let params = Argon2Params::default(); + + let encrypted = + encrypt_private_key(original, password, KeyAlgorithm::Rsa2048, ¶ms).unwrap(); + + let result = decrypt_private_key(&encrypted, wrong_password, ¶ms); + assert!(result.is_err()); + } +} diff --git a/application/src/crypto/key_pair.rs b/application/src/crypto/key_pair.rs new file mode 100644 index 0000000..17633bd --- /dev/null +++ b/application/src/crypto/key_pair.rs @@ -0,0 +1,32 @@ +use error_stack::Result; +use kernel::KernelError; +use serde::{Deserialize, Serialize}; + +use super::algorithm::KeyAlgorithm; + +/// Encrypted private key with metadata for decryption +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EncryptedPrivateKey { + /// Base64-encoded ciphertext (encrypted PEM) + pub ciphertext: String, + /// Base64-encoded nonce (12 bytes for AES-GCM) + pub nonce: String, + /// Base64-encoded salt (16 bytes for Argon2id) + pub salt: String, + /// Algorithm used to generate the key pair + pub algorithm: KeyAlgorithm, +} + +/// Generated key pair with public key in PEM format and encrypted private key +pub struct GeneratedKeyPair { + /// Public key in PEM format + pub public_key_pem: String, + /// Encrypted private key with metadata + pub encrypted_private_key: EncryptedPrivateKey, +} + +/// Trait for key pair generation +pub trait KeyPairGenerator: Send + Sync { + /// Generate a new key pair, encrypting the private key with the given password + fn generate(&self, password: &[u8]) -> Result; +} diff --git a/application/src/crypto/mod.rs b/application/src/crypto/mod.rs new file mode 100644 index 0000000..f83ff10 --- /dev/null +++ b/application/src/crypto/mod.rs @@ -0,0 +1,11 @@ +mod algorithm; +mod encryption; +mod key_pair; +mod password; +mod rsa; + +pub use algorithm::KeyAlgorithm; +pub use encryption::{decrypt_private_key, encrypt_private_key, Argon2Params}; +pub use key_pair::{EncryptedPrivateKey, GeneratedKeyPair, KeyPairGenerator}; +pub use password::{FilePasswordProvider, PasswordProvider}; +pub use rsa::Rsa2048Generator; diff --git a/application/src/crypto/password.rs b/application/src/crypto/password.rs new file mode 100644 index 0000000..2bfc1f6 --- /dev/null +++ b/application/src/crypto/password.rs @@ -0,0 +1,177 @@ +use error_stack::{Report, Result}; +use kernel::KernelError; +use std::path::Path; + +const SECRETS_PATH: &str = "/run/secrets/master-key-password"; +const FALLBACK_PATH: &str = "./master-key-password"; + +/// Validate file permissions (Unix only) +/// Rejects files with group or other permissions (must be 0o600 or 0o400) +#[cfg(unix)] +fn validate_file_permissions(path: &str) -> Result<(), KernelError> { + use std::os::unix::fs::PermissionsExt; + + let metadata = std::fs::metadata(path).map_err(|e| { + Report::new(KernelError::Internal) + .attach_printable(format!("Failed to read file metadata for '{}': {}", path, e)) + })?; + + let mode = metadata.permissions().mode(); + // Check if group or other have any permissions + if mode & 0o077 != 0 { + return Err(Report::new(KernelError::Internal).attach_printable(format!( + "Master password file '{}' has insecure permissions: {:o} (expected 0o600 or 0o400)", + path, mode + ))); + } + + Ok(()) +} + +#[cfg(not(unix))] +fn validate_file_permissions(_path: &str) -> Result<(), KernelError> { + // Skip permission check on non-Unix systems + Ok(()) +} + +/// Trait for providing master password (allows mocking in tests) +pub trait PasswordProvider: Send + Sync { + fn get_password(&self) -> Result, KernelError>; +} + +/// File-based password provider with fallback support +/// +/// Tries to read from `/run/secrets/master-key-password` first, +/// then falls back to `./master-key-password` if not found. +/// +/// On Unix systems, validates that the file has secure permissions (0o600 or 0o400). +pub struct FilePasswordProvider { + secrets_path: String, + fallback_path: String, +} + +impl FilePasswordProvider { + /// Create a new provider with default paths + pub fn new() -> Self { + Self { + secrets_path: SECRETS_PATH.to_string(), + fallback_path: FALLBACK_PATH.to_string(), + } + } + + /// Create a provider with custom paths (useful for testing) + pub fn with_paths, P2: AsRef>(secrets_path: P1, fallback_path: P2) -> Self { + Self { + secrets_path: secrets_path.as_ref().to_string_lossy().into_owned(), + fallback_path: fallback_path.as_ref().to_string_lossy().into_owned(), + } + } +} + +impl Default for FilePasswordProvider { + fn default() -> Self { + Self::new() + } +} + +impl PasswordProvider for FilePasswordProvider { + fn get_password(&self) -> Result, KernelError> { + // Try secrets path first + if Path::new(&self.secrets_path).exists() { + validate_file_permissions(&self.secrets_path)?; + let password = std::fs::read(&self.secrets_path).map_err(|e| { + Report::new(KernelError::Internal).attach_printable(format!( + "Failed to read master password from '{}': {}", + self.secrets_path, e + )) + })?; + if password.is_empty() { + return Err(Report::new(KernelError::Internal) + .attach_printable("Master password file is empty")); + } + return Ok(password); + } + + // Fall back to local path + if Path::new(&self.fallback_path).exists() { + validate_file_permissions(&self.fallback_path)?; + let password = std::fs::read(&self.fallback_path).map_err(|e| { + Report::new(KernelError::Internal).attach_printable(format!( + "Failed to read master password from '{}': {}", + self.fallback_path, e + )) + })?; + if password.is_empty() { + return Err(Report::new(KernelError::Internal) + .attach_printable("Master password file is empty")); + } + return Ok(password); + } + + Err(Report::new(KernelError::Internal).attach_printable(format!( + "Master password file not found. Tried: '{}', '{}'", + self.secrets_path, self.fallback_path + ))) + } +} + +/// In-memory password provider for testing +#[cfg(test)] +pub struct InMemoryPasswordProvider { + password: Vec, +} + +#[cfg(test)] +impl InMemoryPasswordProvider { + pub fn new(password: impl Into>) -> Self { + Self { + password: password.into(), + } + } +} + +#[cfg(test)] +impl PasswordProvider for InMemoryPasswordProvider { + fn get_password(&self) -> Result, KernelError> { + Ok(self.password.clone()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use std::io::Write; + use tempfile::NamedTempFile; + + #[test] + fn test_in_memory_provider() { + let provider = InMemoryPasswordProvider::new(b"test-password".to_vec()); + let password = provider.get_password().unwrap(); + assert_eq!(password, b"test-password"); + } + + #[test] + fn test_file_provider_with_custom_path() { + let mut temp_file = NamedTempFile::new().unwrap(); + temp_file.write_all(b"file-password").unwrap(); + + let provider = FilePasswordProvider::with_paths( + "/nonexistent/path", + temp_file.path(), + ); + + let password = provider.get_password().unwrap(); + assert_eq!(password, b"file-password"); + } + + #[test] + fn test_file_provider_no_file() { + let provider = FilePasswordProvider::with_paths( + "/nonexistent/secrets", + "/nonexistent/fallback", + ); + + let result = provider.get_password(); + assert!(result.is_err()); + } +} diff --git a/application/src/crypto/rsa.rs b/application/src/crypto/rsa.rs new file mode 100644 index 0000000..27589c9 --- /dev/null +++ b/application/src/crypto/rsa.rs @@ -0,0 +1,122 @@ +use error_stack::{Report, Result}; +use kernel::KernelError; +use rand::rngs::OsRng; +use rsa::{ + pkcs8::{EncodePrivateKey, EncodePublicKey, LineEnding}, + RsaPrivateKey, RsaPublicKey, +}; +use zeroize::Zeroizing; + +use super::{ + algorithm::KeyAlgorithm, + encryption::{encrypt_private_key, Argon2Params}, + key_pair::{GeneratedKeyPair, KeyPairGenerator}, +}; + +/// RSA-2048 key pair generator +pub struct Rsa2048Generator { + argon2_params: Argon2Params, +} + +impl Rsa2048Generator { + /// Create a new generator with default Argon2 parameters + pub fn new() -> Self { + Self { + argon2_params: Argon2Params::default(), + } + } + + /// Create a generator with custom Argon2 parameters + pub fn with_argon2_params(params: Argon2Params) -> Self { + Self { + argon2_params: params, + } + } +} + +impl Default for Rsa2048Generator { + fn default() -> Self { + Self::new() + } +} + +impl KeyPairGenerator for Rsa2048Generator { + fn generate(&self, password: &[u8]) -> Result { + // Generate RSA-2048 key pair + let private_key = RsaPrivateKey::new(&mut OsRng, 2048).map_err(|e| { + Report::new(KernelError::Internal) + .attach_printable(format!("Failed to generate RSA-2048 key: {e}")) + })?; + + let public_key = RsaPublicKey::from(&private_key); + + // Convert to PEM format + // Note: SecretDocument already implements Zeroize on drop + let private_key_pem = private_key.to_pkcs8_pem(LineEnding::LF).map_err(|e| { + Report::new(KernelError::Internal) + .attach_printable(format!("Failed to encode private key as PEM: {e}")) + })?; + + let public_key_pem = public_key.to_public_key_pem(LineEnding::LF).map_err(|e| { + Report::new(KernelError::Internal) + .attach_printable(format!("Failed to encode public key as PEM: {e}")) + })?; + + // Copy private key bytes with zeroize protection + let private_key_bytes = Zeroizing::new(private_key_pem.as_bytes().to_vec()); + + // Encrypt private key + let encrypted_private_key = encrypt_private_key( + &private_key_bytes, + password, + KeyAlgorithm::Rsa2048, + &self.argon2_params, + )?; + + Ok(GeneratedKeyPair { + public_key_pem, + encrypted_private_key, + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::crypto::encryption::decrypt_private_key; + + #[test] + fn test_rsa2048_generation() { + let generator = Rsa2048Generator::new(); + let password = b"test-password-123"; + + let result = generator.generate(password); + assert!(result.is_ok()); + + let key_pair = result.unwrap(); + assert!(key_pair.public_key_pem.contains("BEGIN PUBLIC KEY")); + assert!(!key_pair.encrypted_private_key.ciphertext.is_empty()); + assert_eq!( + key_pair.encrypted_private_key.algorithm, + KeyAlgorithm::Rsa2048 + ); + } + + #[test] + fn test_decrypt_generated_key() { + let generator = Rsa2048Generator::new(); + let password = b"test-password-456"; + + let key_pair = generator.generate(password).unwrap(); + + let decrypted = decrypt_private_key( + &key_pair.encrypted_private_key, + password, + &Argon2Params::default(), + ) + .unwrap(); + + let decrypted_str = String::from_utf8_lossy(&decrypted); + assert!(decrypted_str.contains("BEGIN PRIVATE KEY")); + } +} diff --git a/application/src/lib.rs b/application/src/lib.rs index bef0d9e..a38c2fb 100644 --- a/application/src/lib.rs +++ b/application/src/lib.rs @@ -1,2 +1,3 @@ +pub mod crypto; pub mod service; pub mod transfer; diff --git a/application/src/service/account.rs b/application/src/service/account.rs index 23aa69a..05116b6 100644 --- a/application/src/service/account.rs +++ b/application/src/service/account.rs @@ -1,20 +1,26 @@ +use crate::crypto::{FilePasswordProvider, KeyPairGenerator, PasswordProvider, Rsa2048Generator}; use crate::service::auth_account::get_auth_account; use crate::transfer::account::AccountDto; use crate::transfer::auth_account::AuthAccountInfo; use crate::transfer::pagination::{apply_pagination, Pagination}; +use error_stack::Report; use kernel::interfaces::database::DatabaseConnection; use kernel::interfaces::event::EventApplier; use kernel::interfaces::modify::{ AccountModifier, DependOnAccountModifier, DependOnAuthAccountModifier, - DependOnAuthHostModifier, DependOnEventModifier, + DependOnAuthHostModifier, DependOnEventModifier, EventModifier, }; use kernel::interfaces::query::{ AccountQuery, DependOnAccountQuery, DependOnAuthAccountQuery, DependOnAuthHostQuery, DependOnEventQuery, EventQuery, }; use kernel::interfaces::signal::Signal; -use kernel::prelude::entity::{Account, AccountId, AuthAccountId, EventId, Nanoid}; +use kernel::prelude::entity::{ + Account, AccountId, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, + AuthAccountId, EventId, Nanoid, +}; use kernel::KernelError; +use serde_json; use std::future::Future; pub trait GetAccountService: @@ -58,6 +64,46 @@ pub trait GetAccountService: Ok(Some(accounts.into_iter().map(AccountDto::from).collect())) } } + + fn get_account_by_id( + &self, + signal: &impl Signal, + auth_info: AuthAccountInfo, + account_id: String, + ) -> impl Future> { + async move { + let auth_account = get_auth_account(self, signal, auth_info).await?; + let mut transaction = self.database_connection().begin_transaction().await?; + + // Nanoidからアカウントを検索 + let nanoid = Nanoid::::new(account_id); + let account = self + .account_query() + .find_by_nanoid(&mut transaction, &nanoid) + .await? + .ok_or_else(|| { + Report::new(KernelError::NotFound).attach_printable(format!( + "Account not found with nanoid: {}", + nanoid.as_ref() + )) + })?; + + // 権限チェック (認証アカウントに紐づくアカウントのみアクセス可能) + let auth_account_id = auth_account.id(); + let accounts = self + .account_query() + .find_by_auth_id(&mut transaction, auth_account_id) + .await?; + + let found = accounts.iter().any(|a| a.id() == account.id()); + if !found { + return Err(Report::new(KernelError::PermissionDenied) + .attach_printable("This account does not belong to the authenticated user")); + } + + Ok(AccountDto::from(account)) + } + } } impl GetAccountService for T where @@ -72,6 +118,102 @@ impl GetAccountService for T where { } +pub trait CreateAccountService: + 'static + + Sync + + Send + + DependOnAccountQuery + + DependOnAuthAccountQuery + + DependOnAuthAccountModifier + + DependOnAuthHostQuery + + DependOnAuthHostModifier + + DependOnEventModifier + + DependOnEventQuery +{ + fn create_account( + &self, + signal: &S, + auth_info: AuthAccountInfo, + name: String, + is_bot: bool, + ) -> impl Future> + where + S: Signal + Signal + Send + Sync + 'static, + { + async move { + // 認証アカウントを確認 + let _auth_account = get_auth_account(self, signal, auth_info).await?; + let mut transaction = self.database_connection().begin_transaction().await?; + + // アカウントID生成 + let account_id = AccountId::default(); + + // 鍵ペア生成 (RSA-2048, Argon2id + AES-GCM暗号化) + let password_provider = FilePasswordProvider::new(); + let master_password = password_provider.get_password()?; + + let generator = Rsa2048Generator::new(); + let key_pair = generator.generate(&master_password)?; + + // 暗号化された秘密鍵をJSON文字列として保存 + let encrypted_private_key_json = + serde_json::to_string(&key_pair.encrypted_private_key).map_err(|e| { + Report::new(KernelError::Internal) + .attach_printable(format!("Failed to serialize encrypted private key: {e}")) + })?; + + let private_key = AccountPrivateKey::new(encrypted_private_key_json); + let public_key = AccountPublicKey::new(key_pair.public_key_pem); + + // アカウント名とbot状態設定 + let account_name = AccountName::new(name); + let account_is_bot = AccountIsBot::new(is_bot); + + // NanoIDの生成 + let nanoid = Nanoid::::default(); + + // アカウント作成イベントの生成と保存 + let create_command = Account::create( + account_id.clone(), + account_name, + private_key, + public_key, + account_is_bot, + nanoid, + ); + + let create_event = self + .event_modifier() + .persist_and_transform(&mut transaction, create_command) + .await?; + signal.emit(account_id).await?; + + // イベントの適用 + let mut account = None; + Account::apply(&mut account, create_event)?; + + let account = account.ok_or_else(|| { + Report::new(KernelError::Internal) + .attach_printable("Failed to apply account creation event") + })?; + + Ok(AccountDto::from(account)) + } + } +} + +impl CreateAccountService for T where + T: 'static + + DependOnAccountQuery + + DependOnAuthAccountQuery + + DependOnAuthAccountModifier + + DependOnAuthHostQuery + + DependOnAuthHostModifier + + DependOnEventModifier + + DependOnEventQuery +{ +} + pub trait UpdateAccountService: 'static + DependOnAccountQuery @@ -113,7 +255,7 @@ pub trait UpdateAccountService: } Ok(()) } else { - Err(error_stack::Report::new(KernelError::Internal) + Err(Report::new(KernelError::Internal) .attach_printable(format!("Failed to get target account: {account_id:?}"))) } } @@ -128,3 +270,82 @@ impl UpdateAccountService for T where + DependOnEventModifier { } + +pub trait DeleteAccountService: + 'static + + Sync + + Send + + DependOnAccountQuery + + DependOnAuthAccountQuery + + DependOnAuthAccountModifier + + DependOnAuthHostQuery + + DependOnAuthHostModifier + + DependOnEventModifier + + DependOnEventQuery +{ + fn delete_account( + &self, + signal: &S, + auth_info: AuthAccountInfo, + account_id: String, + ) -> impl Future> + where + S: Signal + Signal + Send + Sync + 'static, + { + async move { + // 認証アカウントを確認 + let auth_account = get_auth_account(self, signal, auth_info).await?; + let mut transaction = self.database_connection().begin_transaction().await?; + + // Nanoidからアカウントを検索 + let nanoid = Nanoid::::new(account_id); + let account = self + .account_query() + .find_by_nanoid(&mut transaction, &nanoid) + .await? + .ok_or_else(|| { + Report::new(KernelError::NotFound).attach_printable(format!( + "Account not found with nanoid: {}", + nanoid.as_ref() + )) + })?; + + // 権限チェック (認証アカウントに紐づくアカウントのみ削除可能) + let auth_account_id = auth_account.id().clone(); + let accounts = self + .account_query() + .find_by_auth_id(&mut transaction, &auth_account_id) + .await?; + + let found = accounts.iter().any(|a| a.id() == account.id()); + if !found { + return Err(Report::new(KernelError::PermissionDenied) + .attach_printable("This account does not belong to the authenticated user")); + } + + // 削除イベントの生成と保存 + let account_id = account.id().clone(); + let delete_command = Account::delete(account_id.clone()); + + self.event_modifier() + .persist_and_transform(&mut transaction, delete_command) + .await?; + signal.emit(account_id).await?; + + Ok(()) + } + } +} + +impl DeleteAccountService for T where + T: 'static + + DependOnAccountQuery + + DependOnAuthAccountQuery + + DependOnAuthAccountModifier + + DependOnAuthHostQuery + + DependOnAuthHostModifier + + DependOnEventModifier + + DependOnEventQuery + + UpdateAccountService +{ +} diff --git a/kernel/src/error.rs b/kernel/src/error.rs index e0f40b9..72d56be 100644 --- a/kernel/src/error.rs +++ b/kernel/src/error.rs @@ -6,6 +6,8 @@ pub enum KernelError { Concurrency, Timeout, Internal, + PermissionDenied, + NotFound, } impl Display for KernelError { @@ -14,6 +16,8 @@ impl Display for KernelError { KernelError::Concurrency => write!(f, "Concurrency error"), KernelError::Timeout => write!(f, "Process Timed out"), KernelError::Internal => write!(f, "Internal kernel error"), + KernelError::PermissionDenied => write!(f, "Permission denied"), + KernelError::NotFound => write!(f, "Resource not found"), } } } diff --git a/server/src/error.rs b/server/src/error.rs index ad13373..f67c7e1 100644 --- a/server/src/error.rs +++ b/server/src/error.rs @@ -51,6 +51,8 @@ impl IntoResponse for ErrorStatus { KernelError::Concurrency => StatusCode::CONFLICT, KernelError::Timeout => StatusCode::REQUEST_TIMEOUT, KernelError::Internal => StatusCode::INTERNAL_SERVER_ERROR, + KernelError::PermissionDenied => StatusCode::FORBIDDEN, + KernelError::NotFound => StatusCode::NOT_FOUND, } .into_response(), ErrorStatus::StatusCode(code) => code.into_response(), From e1f99db73ca2f276d26d5a6843179b6d8d9352c0 Mon Sep 17 00:00:00 2001 From: turtton Date: Thu, 1 Jan 2026 18:50:55 +0900 Subject: [PATCH 36/59] :hammer: Implement create/get/delete account methods --- server/src/keycloak.rs | 6 +- server/src/route/account.rs | 134 +++++++++++++++++++++++++++++++++--- 2 files changed, 129 insertions(+), 11 deletions(-) diff --git a/server/src/keycloak.rs b/server/src/keycloak.rs index 23efca9..d080d67 100644 --- a/server/src/keycloak.rs +++ b/server/src/keycloak.rs @@ -49,16 +49,16 @@ impl From for AuthAccountInfo { #[macro_export] macro_rules! expect_role { - ($token: expr, $req: expr) => { + ($token: expr, $uri: expr, $method: expr) => { let expected_roles = - $crate::route::to_permission_strings(&$req.uri().to_string(), $req.method().as_str()); + $crate::route::to_permission_strings(&$uri.to_string(), $method.as_str()); let role_result = expected_roles .iter() .map(|role| axum_keycloak_auth::role::ExpectRoles::expect_roles($token, &[role])) .collect::>>(); if !role_result.iter().any(|r| r.is_ok()) { return Err($crate::error::ErrorStatus::from(( - StatusCode::FORBIDDEN, + axum::http::StatusCode::FORBIDDEN, format!( "Permission denied: required roles(any) = {:?}", expected_roles diff --git a/server/src/route/account.rs b/server/src/route/account.rs index b018162..e09673f 100644 --- a/server/src/route/account.rs +++ b/server/src/route/account.rs @@ -3,11 +3,14 @@ use crate::expect_role; use crate::handler::AppModule; use crate::keycloak::KeycloakAuthAccount; use crate::route::DirectionConverter; -use application::service::account::GetAccountService; +use application::service::account::{ + CreateAccountService, DeleteAccountService, GetAccountService, +}; use application::transfer::pagination::Pagination; -use axum::extract::{Query, Request, State}; +use axum::extract::{Path, Query, State}; use axum::http::StatusCode; -use axum::routing::get; +use axum::http::{Method, Uri}; +use axum::routing::{delete, get, post}; use axum::{Extension, Json, Router}; use axum_keycloak_auth::decode::KeycloakToken; use axum_keycloak_auth::instance::KeycloakAuthInstance; @@ -24,6 +27,12 @@ struct GetAllAccountQuery { direction: Option, } +#[derive(Debug, Deserialize)] +struct CreateAccountRequest { + name: String, + is_bot: bool, +} + #[derive(Debug, Serialize)] struct AccountResponse { id: String, @@ -47,14 +56,15 @@ pub trait AccountRouter { async fn get_accounts( Extension(token): Extension>, State(module): State, + method: Method, + uri: Uri, Query(GetAllAccountQuery { direction, limit, cursor, }): Query, - req: Request, ) -> Result, ErrorStatus> { - expect_role!(&token, req); + expect_role!(&token, uri, method); let auth_info = KeycloakAuthAccount::from(token); let direction = direction.convert_to_direction()?; let pagination = Pagination::new(limit, cursor, direction); @@ -89,12 +99,120 @@ async fn get_accounts( Ok(Json(response)) } +async fn create_account( + Extension(token): Extension>, + State(module): State, + method: Method, + uri: Uri, + Json(request): Json, +) -> Result, ErrorStatus> { + expect_role!(&token, uri, method); + let auth_info = KeycloakAuthAccount::from(token); + + // バリデーション + if request.name.trim().is_empty() { + return Err(ErrorStatus::from(( + StatusCode::BAD_REQUEST, + "Account name cannot be empty".to_string(), + ))); + } + + // サービス層でのアカウント作成処理の呼び出し + let account = module + .handler() + .pgpool() + .create_account( + module.applier_container().deref(), + auth_info.into(), + request.name, + request.is_bot, + ) + .await + .map_err(ErrorStatus::from)?; + + let response = AccountResponse { + id: account.nanoid, + name: account.name, + public_key: account.public_key, + is_bot: account.is_bot, + created_at: account.created_at, + }; + + Ok(Json(response)) +} + +async fn get_account_by_id( + Extension(token): Extension>, + State(module): State, + method: Method, + uri: Uri, + Path(id): Path, +) -> Result, ErrorStatus> { + expect_role!(&token, uri, method); + let auth_info = KeycloakAuthAccount::from(token); + + // IDの検証 + if id.trim().is_empty() { + return Err(ErrorStatus::from(( + StatusCode::BAD_REQUEST, + "Account ID cannot be empty".to_string(), + ))); + } + + // サービス層での特定アカウント取得処理 + let account = module + .handler() + .pgpool() + .get_account_by_id(module.applier_container().deref(), auth_info.into(), id) + .await + .map_err(ErrorStatus::from)?; + + let response = AccountResponse { + id: account.nanoid, + name: account.name, + public_key: account.public_key, + is_bot: account.is_bot, + created_at: account.created_at, + }; + + Ok(Json(response)) +} + +async fn delete_account_by_id( + Extension(token): Extension>, + State(module): State, + method: Method, + uri: Uri, + Path(id): Path, +) -> Result { + expect_role!(&token, uri, method); + let auth_info = KeycloakAuthAccount::from(token); + + // IDの検証 + if id.trim().is_empty() { + return Err(ErrorStatus::from(( + StatusCode::BAD_REQUEST, + "Account ID cannot be empty".to_string(), + ))); + } + + // サービス層でのアカウント削除処理 + module + .handler() + .pgpool() + .delete_account(module.applier_container().deref(), auth_info.into(), id) + .await + .map_err(ErrorStatus::from)?; + + Ok(StatusCode::NO_CONTENT) +} + impl AccountRouter for Router { fn route_account(self, instance: KeycloakAuthInstance) -> Self { self.route("/accounts", get(get_accounts)) - // .route("/accounts", post(todo!())) - // .route("/accounts/:id", get(todo!())) - // .route("/accounts/:id", delete(todo!())) + .route("/accounts", post(create_account)) + .route("/accounts/:id", get(get_account_by_id)) + .route("/accounts/:id", delete(delete_account_by_id)) .layer( KeycloakAuthLayer::::builder() .instance(instance) From e0c768cfc51803d461a32a958052451f671e6a93 Mon Sep 17 00:00:00 2001 From: turtton Date: Thu, 1 Jan 2026 18:58:36 +0900 Subject: [PATCH 37/59] :wrench: Add agent settings --- .claude/settings.json | 16 ++++++++ AGENTS.md | 1 + CLAUDE.md | 92 +++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 109 insertions(+) create mode 100644 .claude/settings.json create mode 120000 AGENTS.md create mode 100644 CLAUDE.md diff --git a/.claude/settings.json b/.claude/settings.json new file mode 100644 index 0000000..79aaaa9 --- /dev/null +++ b/.claude/settings.json @@ -0,0 +1,16 @@ +{ + "hooks": { + "PostToolUse": [ + { + "matcher": "Edit|Write", + "hooks": [ + { + "type": "command", + "command": "jq -r '.tool_input.file_path' | { read file_path; if [[ \"$file_path\" == *.rs ]]; then cargo fmt 2>/dev/null || true; fi; }", + "timeout": 30 + } + ] + } + ] + } +} \ No newline at end of file diff --git a/AGENTS.md b/AGENTS.md new file mode 120000 index 0000000..681311e --- /dev/null +++ b/AGENTS.md @@ -0,0 +1 @@ +CLAUDE.md \ No newline at end of file diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..76596f1 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,92 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Emumet is an Account Service for ShuttlePub, implementing Event Sourcing with CQRS pattern. The name derives from EMU (Extravehicular Mobility Unit) + Helmet. + +## Build & Development Commands + +```bash +# Build +cargo build + +# Run tests (requires DATABASE_URL environment variable) +cargo test + +# Run single test +cargo test + +# Run server +cargo run -p server +``` + +## Required Services + +### PostgreSQL +```bash +podman run --rm --name emumet-postgres -e POSTGRES_PASSWORD=develop -p 5432:5432 docker.io/postgres +``` +- User: postgres / Password: develop + +### Redis +Required for message queue (rikka-mq). + +### Keycloak +```bash +mkdir -p keycloak-data/h2 +podman run --rm -it -v ./keycloak-data/h2:/opt/keycloak/data/h2:Z,U -v ./keycloak-data/import:/opt/keycloak/data/import:Z,U -p 18080:8080 -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin --name emumet-keycloak quay.io/keycloak/keycloak:26.1 start-dev --import-realm +``` +- URL: http://localhost:18080 +- Admin: admin / admin +- Realm: emumet, Client: myclient, User: testuser / testuser + +## Environment Variables + +Copy `.env.example` to `.env`: +- `DATABASE_URL` or individual `DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_USER`, `DATABASE_PASSWORD`, `DATABASE_NAME` +- `KEYCLOAK_SERVER`, `KEYCLOAK_REALM` + +## Architecture + +### Workspace Structure (4 crates with dependency flow) + +``` +kernel → application → driver → server +``` + +- **kernel**: Domain entities, traits for Query/Modify/Event interfaces, Event Sourcing core +- **application**: Business logic services, cryptography (RSA-2048, Argon2id, AES-GCM), DTOs +- **driver**: PostgreSQL/Redis implementations of kernel interfaces +- **server**: Axum HTTP server, Keycloak auth, route handlers, event appliers + +### Event Sourcing Pattern + +Commands create events via `CommandEnvelope` → persisted to `event_streams` table → applied via `EventApplier` trait → projections updated in entity tables. + +Key components: +- `EventApplier` trait (kernel/src/event.rs): Defines how events reconstruct entity state +- `Signal` trait (kernel/src/signal.rs): Triggers async event application via Redis message queue +- `ApplierContainer` (server/src/applier.rs): Manages entity-specific appliers using rikka-mq + +### Interface Trait Pattern + +Kernel defines interface traits, driver implements them: +- `DatabaseConnection` / `Transaction`: Database abstraction +- `*Query` traits: Read operations (e.g., `AccountQuery`) +- `*Modifier` traits: Write operations (e.g., `AccountModifier`) +- `DependOn*` traits: Dependency injection pattern + +### Entity Structure + +Entities use vodca macros (`References`, `Newln`, `Nameln`) and destructure for field access. Each entity has: +- ID type (UUID-based) +- Event enum with variants (Created, Updated, Deleted) +- `EventApplier` implementation + +Domain entities: Account, AuthAccount, AuthHost, Profile, Metadata, Follow, Image, RemoteAccount + +### Testing + +Database tests use `#[test_with::env(DATABASE_URL)]` attribute to skip when database is unavailable. \ No newline at end of file From 5ecbd87cc220ced3e8f97839951223d5484c7bb5 Mon Sep 17 00:00:00 2001 From: turtton Date: Thu, 1 Jan 2026 18:59:21 +0900 Subject: [PATCH 38/59] :hammer: Add default generator --- kernel/src/entity/account/id.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/kernel/src/entity/account/id.rs b/kernel/src/entity/account/id.rs index ed87d86..6c29b2e 100644 --- a/kernel/src/entity/account/id.rs +++ b/kernel/src/entity/account/id.rs @@ -19,6 +19,12 @@ use vodca::{AsRefln, Fromln, Newln}; )] pub struct AccountId(Uuid); +impl Default for AccountId { + fn default() -> Self { + AccountId(Uuid::now_v7()) + } +} + impl From for EventId { fn from(account_id: AccountId) -> Self { EventId::new(account_id.0) From cbd06b011b8d9d8c26559e1c1a9acf4378910849 Mon Sep 17 00:00:00 2001 From: turtton Date: Fri, 2 Jan 2026 13:47:45 +0900 Subject: [PATCH 39/59] :recycle: Add adapter layer and improve crypto security MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add adapter crate with SigningKeyGenerator composed trait via blanket impl - Move crypto implementations from application to driver layer - Improve password memory management with Zeroizing> - Standardize decrypt error messages to prevent information leakage - Add comprehensive documentation for blanket implementations 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- Cargo.lock | 25 ++- Cargo.toml | 1 + adapter/Cargo.toml | 10 + adapter/src/crypto.rs | 75 +++++++ adapter/src/lib.rs | 1 + application/Cargo.toml | 8 +- application/src/crypto/algorithm.rs | 18 -- application/src/crypto/encryption.rs | 183 --------------- application/src/crypto/key_pair.rs | 32 --- application/src/crypto/mod.rs | 11 - application/src/crypto/rsa.rs | 122 ---------- application/src/lib.rs | 1 - application/src/service/account.rs | 20 +- driver/Cargo.toml | 9 + driver/src/crypto/encryption.rs | 212 ++++++++++++++++++ driver/src/crypto/mod.rs | 7 + .../src/crypto/password.rs | 44 ++-- driver/src/crypto/rsa.rs | 173 ++++++++++++++ driver/src/lib.rs | 1 + flake.lock | 6 +- flake.nix | 3 + kernel/Cargo.toml | 1 + kernel/src/crypto.rs | 132 +++++++++++ kernel/src/lib.rs | 88 ++++++++ server/Cargo.toml | 1 + server/src/handler.rs | 67 +++++- server/src/route/account.rs | 4 - 27 files changed, 835 insertions(+), 420 deletions(-) create mode 100644 adapter/Cargo.toml create mode 100644 adapter/src/crypto.rs create mode 100644 adapter/src/lib.rs delete mode 100644 application/src/crypto/algorithm.rs delete mode 100644 application/src/crypto/encryption.rs delete mode 100644 application/src/crypto/key_pair.rs delete mode 100644 application/src/crypto/mod.rs delete mode 100644 application/src/crypto/rsa.rs create mode 100644 driver/src/crypto/encryption.rs create mode 100644 driver/src/crypto/mod.rs rename {application => driver}/src/crypto/password.rs (82%) create mode 100644 driver/src/crypto/rsa.rs create mode 100644 kernel/src/crypto.rs diff --git a/Cargo.lock b/Cargo.lock index 50f398f..792bda3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,15 @@ # It is not intended for manual editing. version = 4 +[[package]] +name = "adapter" +version = "0.1.0" +dependencies = [ + "error-stack", + "kernel", + "zeroize", +] + [[package]] name = "addr2line" version = "0.22.0" @@ -116,14 +125,10 @@ checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da" name = "application" version = "0.1.0" dependencies = [ - "aes-gcm", - "argon2", - "base64 0.22.1", + "adapter", "driver", "error-stack", "kernel", - "rand", - "rsa", "serde", "serde_json", "tempfile", @@ -131,7 +136,6 @@ dependencies = [ "tokio", "uuid", "vodca", - "zeroize", ] [[package]] @@ -755,19 +759,26 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" name = "driver" version = "0.1.0" dependencies = [ + "aes-gcm", + "argon2", "async-trait", + "base64 0.22.1", "deadpool-redis", "dotenvy", "error-stack", "kernel", + "rand", + "rsa", "serde", "serde_json", "sqlx", + "tempfile", "test-with", "time", "tokio", "uuid", "vodca", + "zeroize", ] [[package]] @@ -1548,6 +1559,7 @@ dependencies = [ "time", "uuid", "vodca", + "zeroize", ] [[package]] @@ -2698,6 +2710,7 @@ dependencies = [ name = "server" version = "0.1.0" dependencies = [ + "adapter", "application", "axum", "axum-extra", diff --git a/Cargo.toml b/Cargo.toml index e8f9c16..4da0980 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,5 +1,6 @@ [workspace] members = [ + "adapter", "application", "driver", "kernel", diff --git a/adapter/Cargo.toml b/adapter/Cargo.toml new file mode 100644 index 0000000..2096e6e --- /dev/null +++ b/adapter/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "adapter" +version.workspace = true +edition.workspace = true +authors.workspace = true + +[dependencies] +kernel = { path = "../kernel" } +error-stack = { workspace = true } +zeroize = "1.7" diff --git a/adapter/src/crypto.rs b/adapter/src/crypto.rs new file mode 100644 index 0000000..ce1e2d5 --- /dev/null +++ b/adapter/src/crypto.rs @@ -0,0 +1,75 @@ +use error_stack::Result; +use kernel::interfaces::crypto::{ + DependOnKeyEncryptor, DependOnRawKeyGenerator, GeneratedKeyPair, KeyEncryptor, RawKeyGenerator, + SigningAlgorithm, +}; +use kernel::KernelError; + +/// Trait for signing key pair generation (composed from RawKeyGenerator + KeyEncryptor) +/// +/// This trait is automatically implemented for any type that implements both +/// [`DependOnRawKeyGenerator`] and [`DependOnKeyEncryptor`] via blanket implementation. +/// +/// # Architecture +/// +/// ```text +/// Application (uses SigningKeyGenerator) +/// ↓ +/// Adapter (composes RawKeyGenerator + KeyEncryptor) +/// ↓ +/// Kernel (defines traits) +/// ↑ +/// Driver (implements concrete crypto) +/// ``` +/// +/// # Example +/// +/// ```ignore +/// // If Handler implements DependOnRawKeyGenerator + DependOnKeyEncryptor, +/// // SigningKeyGenerator is automatically implemented. +/// let key_pair = handler.signing_key_generator().generate(password)?; +/// ``` +pub trait SigningKeyGenerator: Send + Sync { + /// Generate a new key pair, encrypting the private key with the given password + fn generate(&self, password: &[u8]) -> Result; + + /// Returns the algorithm used by this generator + fn algorithm(&self) -> SigningAlgorithm; +} + +pub trait DependOnSigningKeyGenerator: Send + Sync { + type SigningKeyGenerator: SigningKeyGenerator; + fn signing_key_generator(&self) -> &Self::SigningKeyGenerator; +} + +// Blanket implementation: any type with RawKeyGenerator + KeyEncryptor can generate signing keys +impl SigningKeyGenerator for T +where + T: DependOnRawKeyGenerator + DependOnKeyEncryptor + Send + Sync, +{ + fn generate(&self, password: &[u8]) -> Result { + let raw = self.raw_key_generator().generate_raw()?; + let encrypted = + self.key_encryptor() + .encrypt(&raw.private_key_pem, password, raw.algorithm)?; + Ok(GeneratedKeyPair { + public_key_pem: raw.public_key_pem, + encrypted_private_key: encrypted, + }) + } + + fn algorithm(&self) -> SigningAlgorithm { + self.raw_key_generator().algorithm() + } +} + +// Blanket implementation: any type that can generate signing keys provides DependOnSigningKeyGenerator +impl DependOnSigningKeyGenerator for T +where + T: DependOnRawKeyGenerator + DependOnKeyEncryptor + Send + Sync, +{ + type SigningKeyGenerator = Self; + fn signing_key_generator(&self) -> &Self::SigningKeyGenerator { + self + } +} diff --git a/adapter/src/lib.rs b/adapter/src/lib.rs new file mode 100644 index 0000000..274f0ed --- /dev/null +++ b/adapter/src/lib.rs @@ -0,0 +1 @@ +pub mod crypto; diff --git a/application/Cargo.toml b/application/Cargo.toml index 72741ce..375f067 100644 --- a/application/Cargo.toml +++ b/application/Cargo.toml @@ -13,15 +13,9 @@ serde = { workspace = true } error-stack = { workspace = true } -# Cryptography -rsa = { version = "0.9", features = ["sha2", "pem"] } -argon2 = "0.5" -aes-gcm = "0.10" -base64 = "0.22" -rand = "0.8" serde_json = "1" -zeroize = "1.7" +adapter = { path = "../adapter" } kernel = { path = "../kernel" } [dev-dependencies] diff --git a/application/src/crypto/algorithm.rs b/application/src/crypto/algorithm.rs deleted file mode 100644 index 6b15a7f..0000000 --- a/application/src/crypto/algorithm.rs +++ /dev/null @@ -1,18 +0,0 @@ -use serde::{Deserialize, Serialize}; - -/// Supported key algorithms for account key pairs -#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize, Default)] -#[serde(rename_all = "lowercase")] -pub enum KeyAlgorithm { - #[default] - Rsa2048, - // Future: Ed25519, Rsa4096, etc. -} - -impl std::fmt::Display for KeyAlgorithm { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Rsa2048 => write!(f, "rsa2048"), - } - } -} diff --git a/application/src/crypto/encryption.rs b/application/src/crypto/encryption.rs deleted file mode 100644 index bf17428..0000000 --- a/application/src/crypto/encryption.rs +++ /dev/null @@ -1,183 +0,0 @@ -use aes_gcm::{ - aead::{Aead, KeyInit}, - Aes256Gcm, Nonce, -}; -use argon2::Argon2; -use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; -use error_stack::{Report, Result}; -use kernel::KernelError; -use rand::{rngs::OsRng, RngCore}; -use zeroize::Zeroizing; - -use super::{algorithm::KeyAlgorithm, key_pair::EncryptedPrivateKey}; - -/// Argon2id parameters (OWASP recommended) -pub struct Argon2Params { - /// Memory cost in KiB (default: 64 MiB = 65536 KiB) - pub memory_cost: u32, - /// Number of iterations (default: 3) - pub time_cost: u32, - /// Degree of parallelism (default: 4) - pub parallelism: u32, -} - -impl Default for Argon2Params { - fn default() -> Self { - Self { - memory_cost: 65536, // 64 MiB - time_cost: 3, - parallelism: 4, - } - } -} - -/// Encrypt private key PEM with Argon2id key derivation and AES-256-GCM -pub fn encrypt_private_key( - private_key_pem: &[u8], - password: &[u8], - algorithm: KeyAlgorithm, - params: &Argon2Params, -) -> Result { - // Generate random salt (16 bytes) - let mut salt = [0u8; 16]; - OsRng.fill_bytes(&mut salt); - - // Derive key using Argon2id (32 bytes for AES-256) - let argon2_params = argon2::Params::new( - params.memory_cost, - params.time_cost, - params.parallelism, - Some(32), - ) - .map_err(|e| { - Report::new(KernelError::Internal) - .attach_printable(format!("Invalid Argon2 parameters: {e}")) - })?; - - let argon2 = Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, argon2_params); - - // Derived key is zeroized on drop - let mut derived_key = Zeroizing::new([0u8; 32]); - argon2 - .hash_password_into(password, &salt, &mut *derived_key) - .map_err(|e| { - Report::new(KernelError::Internal) - .attach_printable(format!("Argon2id key derivation failed: {e}")) - })?; - - // Encrypt with AES-256-GCM - let cipher = Aes256Gcm::new_from_slice(&*derived_key).map_err(|e| { - Report::new(KernelError::Internal) - .attach_printable(format!("Failed to create AES-GCM cipher: {e}")) - })?; - - let mut nonce_bytes = [0u8; 12]; - OsRng.fill_bytes(&mut nonce_bytes); - let nonce = Nonce::from_slice(&nonce_bytes); - - let ciphertext = cipher.encrypt(nonce, private_key_pem).map_err(|e| { - Report::new(KernelError::Internal) - .attach_printable(format!("AES-GCM encryption failed: {e}")) - })?; - - Ok(EncryptedPrivateKey { - ciphertext: BASE64.encode(&ciphertext), - nonce: BASE64.encode(nonce_bytes), - salt: BASE64.encode(salt), - algorithm, - }) -} - -/// Decrypt private key using the stored metadata and password -pub fn decrypt_private_key( - encrypted: &EncryptedPrivateKey, - password: &[u8], - params: &Argon2Params, -) -> Result, KernelError> { - // Decode Base64 fields (use generic error message to prevent information leakage) - let salt = BASE64.decode(&encrypted.salt).map_err(|_| { - Report::new(KernelError::Internal).attach_printable("Invalid encrypted data format") - })?; - - let nonce_bytes = BASE64.decode(&encrypted.nonce).map_err(|_| { - Report::new(KernelError::Internal).attach_printable("Invalid encrypted data format") - })?; - - let ciphertext = BASE64.decode(&encrypted.ciphertext).map_err(|_| { - Report::new(KernelError::Internal).attach_printable("Invalid encrypted data format") - })?; - - // Derive key using Argon2id - let argon2_params = argon2::Params::new( - params.memory_cost, - params.time_cost, - params.parallelism, - Some(32), - ) - .map_err(|e| { - Report::new(KernelError::Internal) - .attach_printable(format!("Invalid Argon2 parameters: {e}")) - })?; - - let argon2 = Argon2::new(argon2::Algorithm::Argon2id, argon2::Version::V0x13, argon2_params); - - // Derived key is zeroized on drop - let mut derived_key = Zeroizing::new([0u8; 32]); - argon2 - .hash_password_into(password, &salt, &mut *derived_key) - .map_err(|e| { - Report::new(KernelError::Internal) - .attach_printable(format!("Argon2id key derivation failed: {e}")) - })?; - - // Decrypt with AES-256-GCM - let cipher = Aes256Gcm::new_from_slice(&*derived_key).map_err(|e| { - Report::new(KernelError::Internal) - .attach_printable(format!("Failed to create AES-GCM cipher: {e}")) - })?; - - let nonce = Nonce::from_slice(&nonce_bytes); - - // Use generic error message to prevent timing attacks - cipher.decrypt(nonce, ciphertext.as_ref()).map_err(|_| { - Report::new(KernelError::Internal) - .attach_printable("Decryption failed: invalid password or corrupted data") - }) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn test_encrypt_decrypt_roundtrip() { - let original = b"-----BEGIN PRIVATE KEY-----\ntest data\n-----END PRIVATE KEY-----"; - let password = b"test-password-123"; - let params = Argon2Params::default(); - - let encrypted = - encrypt_private_key(original, password, KeyAlgorithm::Rsa2048, ¶ms).unwrap(); - - assert!(!encrypted.ciphertext.is_empty()); - assert!(!encrypted.nonce.is_empty()); - assert!(!encrypted.salt.is_empty()); - assert_eq!(encrypted.algorithm, KeyAlgorithm::Rsa2048); - - let decrypted = decrypt_private_key(&encrypted, password, ¶ms).unwrap(); - assert_eq!(decrypted, original); - } - - #[test] - fn test_wrong_password_fails() { - let original = b"secret data"; - let password = b"correct-password"; - let wrong_password = b"wrong-password"; - let params = Argon2Params::default(); - - let encrypted = - encrypt_private_key(original, password, KeyAlgorithm::Rsa2048, ¶ms).unwrap(); - - let result = decrypt_private_key(&encrypted, wrong_password, ¶ms); - assert!(result.is_err()); - } -} diff --git a/application/src/crypto/key_pair.rs b/application/src/crypto/key_pair.rs deleted file mode 100644 index 17633bd..0000000 --- a/application/src/crypto/key_pair.rs +++ /dev/null @@ -1,32 +0,0 @@ -use error_stack::Result; -use kernel::KernelError; -use serde::{Deserialize, Serialize}; - -use super::algorithm::KeyAlgorithm; - -/// Encrypted private key with metadata for decryption -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct EncryptedPrivateKey { - /// Base64-encoded ciphertext (encrypted PEM) - pub ciphertext: String, - /// Base64-encoded nonce (12 bytes for AES-GCM) - pub nonce: String, - /// Base64-encoded salt (16 bytes for Argon2id) - pub salt: String, - /// Algorithm used to generate the key pair - pub algorithm: KeyAlgorithm, -} - -/// Generated key pair with public key in PEM format and encrypted private key -pub struct GeneratedKeyPair { - /// Public key in PEM format - pub public_key_pem: String, - /// Encrypted private key with metadata - pub encrypted_private_key: EncryptedPrivateKey, -} - -/// Trait for key pair generation -pub trait KeyPairGenerator: Send + Sync { - /// Generate a new key pair, encrypting the private key with the given password - fn generate(&self, password: &[u8]) -> Result; -} diff --git a/application/src/crypto/mod.rs b/application/src/crypto/mod.rs deleted file mode 100644 index f83ff10..0000000 --- a/application/src/crypto/mod.rs +++ /dev/null @@ -1,11 +0,0 @@ -mod algorithm; -mod encryption; -mod key_pair; -mod password; -mod rsa; - -pub use algorithm::KeyAlgorithm; -pub use encryption::{decrypt_private_key, encrypt_private_key, Argon2Params}; -pub use key_pair::{EncryptedPrivateKey, GeneratedKeyPair, KeyPairGenerator}; -pub use password::{FilePasswordProvider, PasswordProvider}; -pub use rsa::Rsa2048Generator; diff --git a/application/src/crypto/rsa.rs b/application/src/crypto/rsa.rs deleted file mode 100644 index 27589c9..0000000 --- a/application/src/crypto/rsa.rs +++ /dev/null @@ -1,122 +0,0 @@ -use error_stack::{Report, Result}; -use kernel::KernelError; -use rand::rngs::OsRng; -use rsa::{ - pkcs8::{EncodePrivateKey, EncodePublicKey, LineEnding}, - RsaPrivateKey, RsaPublicKey, -}; -use zeroize::Zeroizing; - -use super::{ - algorithm::KeyAlgorithm, - encryption::{encrypt_private_key, Argon2Params}, - key_pair::{GeneratedKeyPair, KeyPairGenerator}, -}; - -/// RSA-2048 key pair generator -pub struct Rsa2048Generator { - argon2_params: Argon2Params, -} - -impl Rsa2048Generator { - /// Create a new generator with default Argon2 parameters - pub fn new() -> Self { - Self { - argon2_params: Argon2Params::default(), - } - } - - /// Create a generator with custom Argon2 parameters - pub fn with_argon2_params(params: Argon2Params) -> Self { - Self { - argon2_params: params, - } - } -} - -impl Default for Rsa2048Generator { - fn default() -> Self { - Self::new() - } -} - -impl KeyPairGenerator for Rsa2048Generator { - fn generate(&self, password: &[u8]) -> Result { - // Generate RSA-2048 key pair - let private_key = RsaPrivateKey::new(&mut OsRng, 2048).map_err(|e| { - Report::new(KernelError::Internal) - .attach_printable(format!("Failed to generate RSA-2048 key: {e}")) - })?; - - let public_key = RsaPublicKey::from(&private_key); - - // Convert to PEM format - // Note: SecretDocument already implements Zeroize on drop - let private_key_pem = private_key.to_pkcs8_pem(LineEnding::LF).map_err(|e| { - Report::new(KernelError::Internal) - .attach_printable(format!("Failed to encode private key as PEM: {e}")) - })?; - - let public_key_pem = public_key.to_public_key_pem(LineEnding::LF).map_err(|e| { - Report::new(KernelError::Internal) - .attach_printable(format!("Failed to encode public key as PEM: {e}")) - })?; - - // Copy private key bytes with zeroize protection - let private_key_bytes = Zeroizing::new(private_key_pem.as_bytes().to_vec()); - - // Encrypt private key - let encrypted_private_key = encrypt_private_key( - &private_key_bytes, - password, - KeyAlgorithm::Rsa2048, - &self.argon2_params, - )?; - - Ok(GeneratedKeyPair { - public_key_pem, - encrypted_private_key, - }) - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::crypto::encryption::decrypt_private_key; - - #[test] - fn test_rsa2048_generation() { - let generator = Rsa2048Generator::new(); - let password = b"test-password-123"; - - let result = generator.generate(password); - assert!(result.is_ok()); - - let key_pair = result.unwrap(); - assert!(key_pair.public_key_pem.contains("BEGIN PUBLIC KEY")); - assert!(!key_pair.encrypted_private_key.ciphertext.is_empty()); - assert_eq!( - key_pair.encrypted_private_key.algorithm, - KeyAlgorithm::Rsa2048 - ); - } - - #[test] - fn test_decrypt_generated_key() { - let generator = Rsa2048Generator::new(); - let password = b"test-password-456"; - - let key_pair = generator.generate(password).unwrap(); - - let decrypted = decrypt_private_key( - &key_pair.encrypted_private_key, - password, - &Argon2Params::default(), - ) - .unwrap(); - - let decrypted_str = String::from_utf8_lossy(&decrypted); - assert!(decrypted_str.contains("BEGIN PRIVATE KEY")); - } -} diff --git a/application/src/lib.rs b/application/src/lib.rs index a38c2fb..bef0d9e 100644 --- a/application/src/lib.rs +++ b/application/src/lib.rs @@ -1,3 +1,2 @@ -pub mod crypto; pub mod service; pub mod transfer; diff --git a/application/src/service/account.rs b/application/src/service/account.rs index 05116b6..6a190f4 100644 --- a/application/src/service/account.rs +++ b/application/src/service/account.rs @@ -1,9 +1,10 @@ -use crate::crypto::{FilePasswordProvider, KeyPairGenerator, PasswordProvider, Rsa2048Generator}; use crate::service::auth_account::get_auth_account; use crate::transfer::account::AccountDto; use crate::transfer::auth_account::AuthAccountInfo; use crate::transfer::pagination::{apply_pagination, Pagination}; +use adapter::crypto::{DependOnSigningKeyGenerator, SigningKeyGenerator}; use error_stack::Report; +use kernel::interfaces::crypto::{DependOnPasswordProvider, PasswordProvider}; use kernel::interfaces::database::DatabaseConnection; use kernel::interfaces::event::EventApplier; use kernel::interfaces::modify::{ @@ -129,6 +130,8 @@ pub trait CreateAccountService: + DependOnAuthHostModifier + DependOnEventModifier + DependOnEventQuery + + DependOnPasswordProvider + + DependOnSigningKeyGenerator { fn create_account( &self, @@ -148,16 +151,13 @@ pub trait CreateAccountService: // アカウントID生成 let account_id = AccountId::default(); - // 鍵ペア生成 (RSA-2048, Argon2id + AES-GCM暗号化) - let password_provider = FilePasswordProvider::new(); - let master_password = password_provider.get_password()?; - - let generator = Rsa2048Generator::new(); - let key_pair = generator.generate(&master_password)?; + // 鍵ペア生成 (DI経由で取得したPasswordProviderとSigningKeyGeneratorを使用) + let master_password = self.password_provider().get_password()?; + let key_pair = self.signing_key_generator().generate(&master_password)?; // 暗号化された秘密鍵をJSON文字列として保存 - let encrypted_private_key_json = - serde_json::to_string(&key_pair.encrypted_private_key).map_err(|e| { + let encrypted_private_key_json = serde_json::to_string(&key_pair.encrypted_private_key) + .map_err(|e| { Report::new(KernelError::Internal) .attach_printable(format!("Failed to serialize encrypted private key: {e}")) })?; @@ -211,6 +211,8 @@ impl CreateAccountService for T where + DependOnAuthHostModifier + DependOnEventModifier + DependOnEventQuery + + DependOnPasswordProvider + + DependOnSigningKeyGenerator { } diff --git a/driver/Cargo.toml b/driver/Cargo.toml index 2a9ae65..bf3fd9d 100644 --- a/driver/Cargo.toml +++ b/driver/Cargo.toml @@ -19,8 +19,17 @@ async-trait = "0.1" vodca.workspace = true error-stack = { workspace = true } +# Cryptography (moved from application) +rsa = { version = "0.9", features = ["sha2", "pem"] } +argon2 = "0.5" +aes-gcm = "0.10" +base64 = "0.22" +rand = "0.8" +zeroize = "1.7" + kernel = { path = "../kernel" } [dev-dependencies] tokio = { workspace = true, features = ["macros", "test-util"] } test-with.workspace = true +tempfile = "3" diff --git a/driver/src/crypto/encryption.rs b/driver/src/crypto/encryption.rs new file mode 100644 index 0000000..617945b --- /dev/null +++ b/driver/src/crypto/encryption.rs @@ -0,0 +1,212 @@ +use aes_gcm::{ + aead::{Aead, KeyInit}, + Aes256Gcm, Nonce, +}; +use argon2::Argon2; +use base64::{engine::general_purpose::STANDARD as BASE64, Engine}; +use error_stack::{Report, Result}; +use kernel::interfaces::crypto::{EncryptedPrivateKey, KeyEncryptor, SigningAlgorithm}; +use kernel::KernelError; +use rand::{rngs::OsRng, RngCore}; +use zeroize::Zeroizing; + +/// Argon2id parameters (OWASP recommended) +#[derive(Debug, Clone)] +pub struct Argon2Params { + /// Memory cost in KiB (default: 64 MiB = 65536 KiB) + pub memory_cost: u32, + /// Number of iterations (default: 3) + pub time_cost: u32, + /// Degree of parallelism (default: 4) + pub parallelism: u32, +} + +impl Default for Argon2Params { + fn default() -> Self { + Self { + memory_cost: 65536, // 64 MiB + time_cost: 3, + parallelism: 4, + } + } +} + +/// Argon2id-based private key encryptor using AES-256-GCM +#[derive(Debug, Clone)] +pub struct Argon2Encryptor { + params: Argon2Params, +} + +impl Argon2Encryptor { + pub fn new(params: Argon2Params) -> Self { + Self { params } + } +} + +impl Default for Argon2Encryptor { + fn default() -> Self { + Self::new(Argon2Params::default()) + } +} + +impl KeyEncryptor for Argon2Encryptor { + fn encrypt( + &self, + private_key_pem: &[u8], + password: &[u8], + algorithm: SigningAlgorithm, + ) -> Result { + // Generate random salt (16 bytes) + let mut salt = [0u8; 16]; + OsRng.fill_bytes(&mut salt); + + // Derive key using Argon2id (32 bytes for AES-256) + let argon2_params = argon2::Params::new( + self.params.memory_cost, + self.params.time_cost, + self.params.parallelism, + Some(32), + ) + .map_err(|e| { + Report::new(KernelError::Internal) + .attach_printable(format!("Invalid Argon2 parameters: {e}")) + })?; + + let argon2 = Argon2::new( + argon2::Algorithm::Argon2id, + argon2::Version::V0x13, + argon2_params, + ); + + // Derived key is zeroized on drop + let mut derived_key = Zeroizing::new([0u8; 32]); + argon2 + .hash_password_into(password, &salt, &mut *derived_key) + .map_err(|e| { + Report::new(KernelError::Internal) + .attach_printable(format!("Argon2id key derivation failed: {e}")) + })?; + + // Encrypt with AES-256-GCM + let cipher = Aes256Gcm::new_from_slice(&*derived_key).map_err(|e| { + Report::new(KernelError::Internal) + .attach_printable(format!("Failed to create AES-GCM cipher: {e}")) + })?; + + let mut nonce_bytes = [0u8; 12]; + OsRng.fill_bytes(&mut nonce_bytes); + let nonce = Nonce::from_slice(&nonce_bytes); + + let ciphertext = cipher.encrypt(nonce, private_key_pem).map_err(|e| { + Report::new(KernelError::Internal) + .attach_printable(format!("AES-GCM encryption failed: {e}")) + })?; + + Ok(EncryptedPrivateKey { + ciphertext: BASE64.encode(&ciphertext), + nonce: BASE64.encode(nonce_bytes), + salt: BASE64.encode(salt), + algorithm, + }) + } + + fn decrypt( + &self, + encrypted: &EncryptedPrivateKey, + password: &[u8], + ) -> Result, KernelError> { + // Decode Base64 fields (use generic error message to prevent information leakage) + let salt = BASE64.decode(&encrypted.salt).map_err(|_| { + Report::new(KernelError::Internal).attach_printable("Invalid encrypted data format") + })?; + + let nonce_bytes = BASE64.decode(&encrypted.nonce).map_err(|_| { + Report::new(KernelError::Internal).attach_printable("Invalid encrypted data format") + })?; + + let ciphertext = BASE64.decode(&encrypted.ciphertext).map_err(|_| { + Report::new(KernelError::Internal).attach_printable("Invalid encrypted data format") + })?; + + // Derive key using Argon2id + // Use generic error messages to prevent information leakage during decryption + let argon2_params = argon2::Params::new( + self.params.memory_cost, + self.params.time_cost, + self.params.parallelism, + Some(32), + ) + .map_err(|_| { + Report::new(KernelError::Internal) + .attach_printable("Decryption failed: invalid password or corrupted data") + })?; + + let argon2 = Argon2::new( + argon2::Algorithm::Argon2id, + argon2::Version::V0x13, + argon2_params, + ); + + // Derived key is zeroized on drop + let mut derived_key = Zeroizing::new([0u8; 32]); + argon2 + .hash_password_into(password, &salt, &mut *derived_key) + .map_err(|_| { + Report::new(KernelError::Internal) + .attach_printable("Decryption failed: invalid password or corrupted data") + })?; + + // Decrypt with AES-256-GCM + let cipher = Aes256Gcm::new_from_slice(&*derived_key).map_err(|_| { + Report::new(KernelError::Internal) + .attach_printable("Decryption failed: invalid password or corrupted data") + })?; + + let nonce = Nonce::from_slice(&nonce_bytes); + + // Use generic error message to prevent timing attacks + cipher.decrypt(nonce, ciphertext.as_ref()).map_err(|_| { + Report::new(KernelError::Internal) + .attach_printable("Decryption failed: invalid password or corrupted data") + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_encrypt_decrypt_roundtrip() { + let original = b"-----BEGIN PRIVATE KEY-----\ntest data\n-----END PRIVATE KEY-----"; + let password = b"test-password-123"; + let encryptor = Argon2Encryptor::default(); + + let encrypted = encryptor + .encrypt(original, password, SigningAlgorithm::Rsa2048) + .unwrap(); + + assert!(!encrypted.ciphertext.is_empty()); + assert!(!encrypted.nonce.is_empty()); + assert!(!encrypted.salt.is_empty()); + assert_eq!(encrypted.algorithm, SigningAlgorithm::Rsa2048); + + let decrypted = encryptor.decrypt(&encrypted, password).unwrap(); + assert_eq!(decrypted, original); + } + + #[test] + fn test_wrong_password_fails() { + let original = b"secret data"; + let password = b"correct-password"; + let wrong_password = b"wrong-password"; + let encryptor = Argon2Encryptor::default(); + + let encrypted = encryptor + .encrypt(original, password, SigningAlgorithm::Rsa2048) + .unwrap(); + + let result = encryptor.decrypt(&encrypted, wrong_password); + assert!(result.is_err()); + } +} diff --git a/driver/src/crypto/mod.rs b/driver/src/crypto/mod.rs new file mode 100644 index 0000000..e9ed78a --- /dev/null +++ b/driver/src/crypto/mod.rs @@ -0,0 +1,7 @@ +mod encryption; +mod password; +mod rsa; + +pub use encryption::{Argon2Encryptor, Argon2Params}; +pub use password::FilePasswordProvider; +pub use rsa::{Rsa2048RawGenerator, Rsa2048Signer, Rsa2048Verifier}; diff --git a/application/src/crypto/password.rs b/driver/src/crypto/password.rs similarity index 82% rename from application/src/crypto/password.rs rename to driver/src/crypto/password.rs index 2bfc1f6..dce944c 100644 --- a/application/src/crypto/password.rs +++ b/driver/src/crypto/password.rs @@ -1,6 +1,8 @@ use error_stack::{Report, Result}; +use kernel::interfaces::crypto::PasswordProvider; use kernel::KernelError; use std::path::Path; +use zeroize::Zeroizing; const SECRETS_PATH: &str = "/run/secrets/master-key-password"; const FALLBACK_PATH: &str = "./master-key-password"; @@ -12,8 +14,10 @@ fn validate_file_permissions(path: &str) -> Result<(), KernelError> { use std::os::unix::fs::PermissionsExt; let metadata = std::fs::metadata(path).map_err(|e| { - Report::new(KernelError::Internal) - .attach_printable(format!("Failed to read file metadata for '{}': {}", path, e)) + Report::new(KernelError::Internal).attach_printable(format!( + "Failed to read file metadata for '{}': {}", + path, e + )) })?; let mode = metadata.permissions().mode(); @@ -34,17 +38,13 @@ fn validate_file_permissions(_path: &str) -> Result<(), KernelError> { Ok(()) } -/// Trait for providing master password (allows mocking in tests) -pub trait PasswordProvider: Send + Sync { - fn get_password(&self) -> Result, KernelError>; -} - /// File-based password provider with fallback support /// /// Tries to read from `/run/secrets/master-key-password` first, /// then falls back to `./master-key-password` if not found. /// /// On Unix systems, validates that the file has secure permissions (0o600 or 0o400). +#[derive(Clone)] pub struct FilePasswordProvider { secrets_path: String, fallback_path: String, @@ -60,7 +60,10 @@ impl FilePasswordProvider { } /// Create a provider with custom paths (useful for testing) - pub fn with_paths, P2: AsRef>(secrets_path: P1, fallback_path: P2) -> Self { + pub fn with_paths, P2: AsRef>( + secrets_path: P1, + fallback_path: P2, + ) -> Self { Self { secrets_path: secrets_path.as_ref().to_string_lossy().into_owned(), fallback_path: fallback_path.as_ref().to_string_lossy().into_owned(), @@ -75,7 +78,7 @@ impl Default for FilePasswordProvider { } impl PasswordProvider for FilePasswordProvider { - fn get_password(&self) -> Result, KernelError> { + fn get_password(&self) -> Result>, KernelError> { // Try secrets path first if Path::new(&self.secrets_path).exists() { validate_file_permissions(&self.secrets_path)?; @@ -89,7 +92,7 @@ impl PasswordProvider for FilePasswordProvider { return Err(Report::new(KernelError::Internal) .attach_printable("Master password file is empty")); } - return Ok(password); + return Ok(Zeroizing::new(password)); } // Fall back to local path @@ -105,7 +108,7 @@ impl PasswordProvider for FilePasswordProvider { return Err(Report::new(KernelError::Internal) .attach_printable("Master password file is empty")); } - return Ok(password); + return Ok(Zeroizing::new(password)); } Err(Report::new(KernelError::Internal).attach_printable(format!( @@ -132,8 +135,8 @@ impl InMemoryPasswordProvider { #[cfg(test)] impl PasswordProvider for InMemoryPasswordProvider { - fn get_password(&self) -> Result, KernelError> { - Ok(self.password.clone()) + fn get_password(&self) -> Result>, KernelError> { + Ok(Zeroizing::new(self.password.clone())) } } @@ -147,7 +150,7 @@ mod tests { fn test_in_memory_provider() { let provider = InMemoryPasswordProvider::new(b"test-password".to_vec()); let password = provider.get_password().unwrap(); - assert_eq!(password, b"test-password"); + assert_eq!(password.as_slice(), b"test-password"); } #[test] @@ -155,21 +158,16 @@ mod tests { let mut temp_file = NamedTempFile::new().unwrap(); temp_file.write_all(b"file-password").unwrap(); - let provider = FilePasswordProvider::with_paths( - "/nonexistent/path", - temp_file.path(), - ); + let provider = FilePasswordProvider::with_paths("/nonexistent/path", temp_file.path()); let password = provider.get_password().unwrap(); - assert_eq!(password, b"file-password"); + assert_eq!(password.as_slice(), b"file-password"); } #[test] fn test_file_provider_no_file() { - let provider = FilePasswordProvider::with_paths( - "/nonexistent/secrets", - "/nonexistent/fallback", - ); + let provider = + FilePasswordProvider::with_paths("/nonexistent/secrets", "/nonexistent/fallback"); let result = provider.get_password(); assert!(result.is_err()); diff --git a/driver/src/crypto/rsa.rs b/driver/src/crypto/rsa.rs new file mode 100644 index 0000000..9403374 --- /dev/null +++ b/driver/src/crypto/rsa.rs @@ -0,0 +1,173 @@ +use error_stack::{Report, Result}; +use kernel::interfaces::crypto::{ + RawKeyGenerator, RawKeyPair, SignatureVerifier, Signer, SigningAlgorithm, +}; +use kernel::KernelError; +use rand::rngs::OsRng; +use rsa::sha2::Sha256; +use rsa::{ + pkcs1v15::{SigningKey, VerifyingKey}, + pkcs8::{DecodePrivateKey, DecodePublicKey, EncodePrivateKey, EncodePublicKey, LineEnding}, + signature::{SignatureEncoding, Signer as RsaSigner, Verifier}, + RsaPrivateKey, RsaPublicKey, +}; +use zeroize::Zeroizing; + +/// RSA-2048 raw key pair generator (without encryption) +#[derive(Debug, Clone, Copy, Default)] +pub struct Rsa2048RawGenerator; + +impl RawKeyGenerator for Rsa2048RawGenerator { + fn generate_raw(&self) -> Result { + // Generate RSA-2048 key pair + let private_key = RsaPrivateKey::new(&mut OsRng, 2048).map_err(|e| { + Report::new(KernelError::Internal) + .attach_printable(format!("Failed to generate RSA-2048 key: {e}")) + })?; + + let public_key = RsaPublicKey::from(&private_key); + + // Convert to PEM format + // Note: SecretDocument already implements Zeroize on drop + let private_key_pem = private_key.to_pkcs8_pem(LineEnding::LF).map_err(|e| { + Report::new(KernelError::Internal) + .attach_printable(format!("Failed to encode private key as PEM: {e}")) + })?; + + let public_key_pem = public_key.to_public_key_pem(LineEnding::LF).map_err(|e| { + Report::new(KernelError::Internal) + .attach_printable(format!("Failed to encode public key as PEM: {e}")) + })?; + + Ok(RawKeyPair { + public_key_pem, + // Note: SecretDocument (private_key_pem source) already implements Zeroize on drop. + // The copy to Vec is wrapped in Zeroizing for defense in depth. + private_key_pem: Zeroizing::new(private_key_pem.as_bytes().to_vec()), + algorithm: SigningAlgorithm::Rsa2048, + }) + } + + fn algorithm(&self) -> SigningAlgorithm { + SigningAlgorithm::Rsa2048 + } +} + +/// RSA-2048 signer using PKCS#1 v1.5 + SHA-256 +#[derive(Debug, Clone, Copy, Default)] +pub struct Rsa2048Signer; + +impl Signer for Rsa2048Signer { + fn sign(&self, data: &[u8], private_key_pem: &[u8]) -> Result, KernelError> { + let pem_str = std::str::from_utf8(private_key_pem).map_err(|e| { + Report::new(KernelError::Internal) + .attach_printable(format!("Invalid UTF-8 in private key PEM: {e}")) + })?; + + let private_key = RsaPrivateKey::from_pkcs8_pem(pem_str).map_err(|e| { + Report::new(KernelError::Internal) + .attach_printable(format!("Failed to parse private key PEM: {e}")) + })?; + + let signing_key = SigningKey::::new(private_key); + let signature = signing_key.sign(data); + + Ok(signature.to_bytes().to_vec()) + } +} + +/// RSA-2048 signature verifier using PKCS#1 v1.5 + SHA-256 +#[derive(Debug, Clone, Copy, Default)] +pub struct Rsa2048Verifier; + +impl SignatureVerifier for Rsa2048Verifier { + fn verify( + &self, + data: &[u8], + signature: &[u8], + public_key_pem: &[u8], + ) -> Result { + let pem_str = std::str::from_utf8(public_key_pem).map_err(|e| { + Report::new(KernelError::Internal) + .attach_printable(format!("Invalid UTF-8 in public key PEM: {e}")) + })?; + + let public_key = RsaPublicKey::from_public_key_pem(pem_str).map_err(|e| { + Report::new(KernelError::Internal) + .attach_printable(format!("Failed to parse public key PEM: {e}")) + })?; + + let verifying_key = VerifyingKey::::new(public_key); + + let sig = rsa::pkcs1v15::Signature::try_from(signature).map_err(|e| { + Report::new(KernelError::Internal) + .attach_printable(format!("Invalid signature format: {e}")) + })?; + + match verifying_key.verify(data, &sig) { + Ok(()) => Ok(true), + Err(_) => Ok(false), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_rsa2048_raw_generation() { + let generator = Rsa2048RawGenerator; + + let result = generator.generate_raw(); + assert!(result.is_ok()); + + let key_pair = result.unwrap(); + assert!(key_pair.public_key_pem.contains("BEGIN PUBLIC KEY")); + assert!(!key_pair.private_key_pem.is_empty()); + assert_eq!(key_pair.algorithm, SigningAlgorithm::Rsa2048); + + // Verify private key is valid PEM + let private_key_str = String::from_utf8_lossy(&key_pair.private_key_pem); + assert!(private_key_str.contains("BEGIN PRIVATE KEY")); + } + + #[test] + fn test_sign_and_verify() { + let generator = Rsa2048RawGenerator; + let key_pair = generator.generate_raw().unwrap(); + + let signer = Rsa2048Signer; + let verifier = Rsa2048Verifier; + + let data = b"Hello, ActivityPub!"; + let signature = signer.sign(data, &key_pair.private_key_pem).unwrap(); + + let is_valid = verifier + .verify(data, &signature, key_pair.public_key_pem.as_bytes()) + .unwrap(); + assert!(is_valid); + } + + #[test] + fn test_verify_wrong_data_fails() { + let generator = Rsa2048RawGenerator; + let key_pair = generator.generate_raw().unwrap(); + + let signer = Rsa2048Signer; + let verifier = Rsa2048Verifier; + + let data = b"Original message"; + let signature = signer.sign(data, &key_pair.private_key_pem).unwrap(); + + let tampered_data = b"Tampered message"; + let is_valid = verifier + .verify( + tampered_data, + &signature, + key_pair.public_key_pem.as_bytes(), + ) + .unwrap(); + assert!(!is_valid); + } +} diff --git a/driver/src/lib.rs b/driver/src/lib.rs index da08c67..bdf8e69 100644 --- a/driver/src/lib.rs +++ b/driver/src/lib.rs @@ -1,3 +1,4 @@ +pub mod crypto; pub mod database; mod error; diff --git a/flake.lock b/flake.lock index bc699ab..bcf2e8b 100644 --- a/flake.lock +++ b/flake.lock @@ -19,11 +19,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1739419412, - "narHash": "sha256-NCWZQg4DbYVFWg+MOFrxWRaVsLA7yvRWAf6o0xPR1hI=", + "lastModified": 1767242400, + "narHash": "sha256-knFaYjeg7swqG1dljj1hOxfg39zrIy8pfGuicjm9s+o=", "owner": "nixos", "repo": "nixpkgs", - "rev": "2d55b4c1531187926c2a423f6940b3b1301399b5", + "rev": "c04833a1e584401bb63c1a63ddc51a71e6aa457a", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 3da7b8a..90f8d0c 100644 --- a/flake.nix +++ b/flake.nix @@ -31,6 +31,9 @@ nodePackages.pnpm sqlx-cli ]; + env = { + LD_LIBRARY_PATH = lib.makeLibraryPath buildInputs; + }; shellHook = '' #export CARGO_TARGET_X86_64_UNKNOWN_LINUX_GNU_LINKER="${pkgs.clang}/bin/clang" #export CARGO_TARGET_X86_64-UNKNOWN_LINUX_GNU_RUSTFLAGS="-C link-arg=-fuse-ld=${pkgs.mold-wrapped.override(old: { extraPackages = nativeBuildInputs ++ buildInputs; })}/bin/mold" diff --git a/kernel/Cargo.toml b/kernel/Cargo.toml index f39275b..969c4c2 100644 --- a/kernel/Cargo.toml +++ b/kernel/Cargo.toml @@ -14,6 +14,7 @@ serde = { workspace = true } uuid = { workspace = true } time = { workspace = true } nanoid = { workspace = true } +zeroize = "1.7" async-trait = "0.1" error-stack = { workspace = true } diff --git a/kernel/src/crypto.rs b/kernel/src/crypto.rs new file mode 100644 index 0000000..81dfdfc --- /dev/null +++ b/kernel/src/crypto.rs @@ -0,0 +1,132 @@ +use crate::KernelError; +use error_stack::Result; +use serde::{Deserialize, Serialize}; +use zeroize::Zeroizing; + +/// Supported signing algorithms +#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum SigningAlgorithm { + Rsa2048, + Ed25519, +} + +impl std::fmt::Display for SigningAlgorithm { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + Self::Rsa2048 => write!(f, "rsa2048"), + Self::Ed25519 => write!(f, "ed25519"), + } + } +} + +impl Default for SigningAlgorithm { + fn default() -> Self { + Self::Rsa2048 + } +} + +/// Encrypted private key with metadata for decryption +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EncryptedPrivateKey { + /// Base64-encoded ciphertext (encrypted PEM) + pub ciphertext: String, + /// Base64-encoded nonce (12 bytes for AES-GCM) + pub nonce: String, + /// Base64-encoded salt (16 bytes for Argon2id) + pub salt: String, + /// Algorithm used to generate the key pair + pub algorithm: SigningAlgorithm, +} + +/// Generated key pair with public key in PEM format and encrypted private key +pub struct GeneratedKeyPair { + /// Public key in PEM format + pub public_key_pem: String, + /// Encrypted private key with metadata + pub encrypted_private_key: EncryptedPrivateKey, +} + +/// Raw key pair before encryption (private key is zeroized on drop) +pub struct RawKeyPair { + /// Public key in PEM format + pub public_key_pem: String, + /// Private key in PEM format (zeroized on drop) + pub private_key_pem: Zeroizing>, + /// Algorithm used to generate the key pair + pub algorithm: SigningAlgorithm, +} + +/// Trait for providing master password +pub trait PasswordProvider: Send + Sync { + fn get_password(&self) -> Result>, KernelError>; +} + +/// Trait for raw key pair generation (without encryption) +pub trait RawKeyGenerator: Send + Sync { + /// Generate a new raw key pair (unencrypted) + fn generate_raw(&self) -> Result; + + /// Returns the algorithm used by this generator + fn algorithm(&self) -> SigningAlgorithm; +} + +/// Trait for encrypting/decrypting private keys +pub trait KeyEncryptor: Send + Sync { + fn encrypt( + &self, + private_key_pem: &[u8], + password: &[u8], + algorithm: SigningAlgorithm, + ) -> Result; + + fn decrypt( + &self, + encrypted: &EncryptedPrivateKey, + password: &[u8], + ) -> Result, KernelError>; +} + +/// Trait for signing data with a private key +pub trait Signer: Send + Sync { + /// Sign data using PKCS#1 v1.5 + SHA-256 (for RSA) or Ed25519 + fn sign(&self, data: &[u8], private_key_pem: &[u8]) -> Result, KernelError>; +} + +/// Trait for verifying signatures with a public key +pub trait SignatureVerifier: Send + Sync { + /// Verify a signature using the public key + fn verify( + &self, + data: &[u8], + signature: &[u8], + public_key_pem: &[u8], + ) -> Result; +} + +// --- DI Traits --- + +pub trait DependOnPasswordProvider: Send + Sync { + type PasswordProvider: PasswordProvider; + fn password_provider(&self) -> &Self::PasswordProvider; +} + +pub trait DependOnRawKeyGenerator: Send + Sync { + type RawKeyGenerator: RawKeyGenerator; + fn raw_key_generator(&self) -> &Self::RawKeyGenerator; +} + +pub trait DependOnKeyEncryptor: Send + Sync { + type KeyEncryptor: KeyEncryptor; + fn key_encryptor(&self) -> &Self::KeyEncryptor; +} + +pub trait DependOnSigner: Send + Sync { + type Signer: Signer; + fn signer(&self) -> &Self::Signer; +} + +pub trait DependOnSignatureVerifier: Send + Sync { + type SignatureVerifier: SignatureVerifier; + fn signature_verifier(&self) -> &Self::SignatureVerifier; +} diff --git a/kernel/src/lib.rs b/kernel/src/lib.rs index 4f0a600..236f5ae 100644 --- a/kernel/src/lib.rs +++ b/kernel/src/lib.rs @@ -1,3 +1,4 @@ +mod crypto; mod database; mod entity; mod error; @@ -17,6 +18,9 @@ pub mod prelude { #[cfg(feature = "interfaces")] pub mod interfaces { + pub mod crypto { + pub use crate::crypto::*; + } pub mod database { pub use crate::database::*; } @@ -34,3 +38,87 @@ pub mod interfaces { pub use crate::signal::*; } } + +/// Macro to delegate database-related DependOn* traits to a field. +/// +/// This macro generates implementations for: +/// - DependOnDatabaseConnection +/// - DependOnAccountQuery, DependOnAuthAccountQuery, DependOnAuthHostQuery, DependOnEventQuery +/// - DependOnAccountModifier, DependOnAuthAccountModifier, DependOnAuthHostModifier, DependOnEventModifier +/// +/// # Usage +/// ```ignore +/// impl_database_delegation!(Handler, pgpool, PostgresDatabase); +/// ``` +/// +/// When switching DB implementation, only the macro arguments need to change: +/// ```ignore +/// impl_database_delegation!(Handler, db, MysqlDatabase); +/// ``` +#[macro_export] +macro_rules! impl_database_delegation { + ($impl_type:ty, $field:ident, $db_type:ty) => { + impl $crate::interfaces::database::DependOnDatabaseConnection for $impl_type { + type DatabaseConnection = $db_type; + fn database_connection(&self) -> &Self::DatabaseConnection { + &self.$field + } + } + + impl $crate::interfaces::query::DependOnAccountQuery for $impl_type { + type AccountQuery = <$db_type as $crate::interfaces::query::DependOnAccountQuery>::AccountQuery; + fn account_query(&self) -> &Self::AccountQuery { + $crate::interfaces::query::DependOnAccountQuery::account_query(&self.$field) + } + } + + impl $crate::interfaces::query::DependOnAuthAccountQuery for $impl_type { + type AuthAccountQuery = <$db_type as $crate::interfaces::query::DependOnAuthAccountQuery>::AuthAccountQuery; + fn auth_account_query(&self) -> &Self::AuthAccountQuery { + $crate::interfaces::query::DependOnAuthAccountQuery::auth_account_query(&self.$field) + } + } + + impl $crate::interfaces::query::DependOnAuthHostQuery for $impl_type { + type AuthHostQuery = <$db_type as $crate::interfaces::query::DependOnAuthHostQuery>::AuthHostQuery; + fn auth_host_query(&self) -> &Self::AuthHostQuery { + $crate::interfaces::query::DependOnAuthHostQuery::auth_host_query(&self.$field) + } + } + + impl $crate::interfaces::query::DependOnEventQuery for $impl_type { + type EventQuery = <$db_type as $crate::interfaces::query::DependOnEventQuery>::EventQuery; + fn event_query(&self) -> &Self::EventQuery { + $crate::interfaces::query::DependOnEventQuery::event_query(&self.$field) + } + } + + impl $crate::interfaces::modify::DependOnAccountModifier for $impl_type { + type AccountModifier = <$db_type as $crate::interfaces::modify::DependOnAccountModifier>::AccountModifier; + fn account_modifier(&self) -> &Self::AccountModifier { + $crate::interfaces::modify::DependOnAccountModifier::account_modifier(&self.$field) + } + } + + impl $crate::interfaces::modify::DependOnAuthAccountModifier for $impl_type { + type AuthAccountModifier = <$db_type as $crate::interfaces::modify::DependOnAuthAccountModifier>::AuthAccountModifier; + fn auth_account_modifier(&self) -> &Self::AuthAccountModifier { + $crate::interfaces::modify::DependOnAuthAccountModifier::auth_account_modifier(&self.$field) + } + } + + impl $crate::interfaces::modify::DependOnAuthHostModifier for $impl_type { + type AuthHostModifier = <$db_type as $crate::interfaces::modify::DependOnAuthHostModifier>::AuthHostModifier; + fn auth_host_modifier(&self) -> &Self::AuthHostModifier { + $crate::interfaces::modify::DependOnAuthHostModifier::auth_host_modifier(&self.$field) + } + } + + impl $crate::interfaces::modify::DependOnEventModifier for $impl_type { + type EventModifier = <$db_type as $crate::interfaces::modify::DependOnEventModifier>::EventModifier; + fn event_modifier(&self) -> &Self::EventModifier { + $crate::interfaces::modify::DependOnEventModifier::event_modifier(&self.$field) + } + } + }; +} diff --git a/server/Cargo.toml b/server/Cargo.toml index 2d8cad1..24bf60b 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -32,6 +32,7 @@ vodca = { workspace = true } destructure = { workspace = true } serde = { workspace = true } +adapter = { path = "../adapter" } application = { path = "../application" } driver = { path = "../driver" } kernel = { path = "../kernel" } diff --git a/server/src/handler.rs b/server/src/handler.rs index f67350a..7799bc1 100644 --- a/server/src/handler.rs +++ b/server/src/handler.rs @@ -1,5 +1,12 @@ use crate::applier::ApplierContainer; +use driver::crypto::{ + Argon2Encryptor, FilePasswordProvider, Rsa2048RawGenerator, Rsa2048Signer, Rsa2048Verifier, +}; use driver::database::{PostgresDatabase, RedisDatabase}; +use kernel::interfaces::crypto::{ + DependOnKeyEncryptor, DependOnPasswordProvider, DependOnRawKeyGenerator, + DependOnSignatureVerifier, DependOnSigner, +}; use kernel::KernelError; use std::sync::Arc; use vodca::References; @@ -25,6 +32,12 @@ impl AppModule { pub struct Handler { pgpool: PostgresDatabase, redis: RedisDatabase, + // Crypto providers + password_provider: FilePasswordProvider, + raw_key_generator: Rsa2048RawGenerator, + key_encryptor: Argon2Encryptor, + signer: Rsa2048Signer, + verifier: Rsa2048Verifier, } impl Handler { @@ -32,6 +45,58 @@ impl Handler { let pgpool = PostgresDatabase::new().await?; let redis = RedisDatabase::new()?; - Ok(Self { pgpool, redis }) + Ok(Self { + pgpool, + redis, + password_provider: FilePasswordProvider::new(), + raw_key_generator: Rsa2048RawGenerator, + key_encryptor: Argon2Encryptor::default(), + signer: Rsa2048Signer, + verifier: Rsa2048Verifier, + }) + } +} + +// --- Database DI implementations (via macro) --- + +kernel::impl_database_delegation!(Handler, pgpool, PostgresDatabase); + +// --- Crypto DI implementations --- + +impl DependOnPasswordProvider for Handler { + type PasswordProvider = FilePasswordProvider; + fn password_provider(&self) -> &Self::PasswordProvider { + &self.password_provider + } +} + +impl DependOnRawKeyGenerator for Handler { + type RawKeyGenerator = Rsa2048RawGenerator; + fn raw_key_generator(&self) -> &Self::RawKeyGenerator { + &self.raw_key_generator + } +} + +impl DependOnKeyEncryptor for Handler { + type KeyEncryptor = Argon2Encryptor; + fn key_encryptor(&self) -> &Self::KeyEncryptor { + &self.key_encryptor + } +} + +// Note: DependOnSigningKeyGenerator is provided automatically via blanket impl in adapter +// when Handler implements DependOnRawKeyGenerator + DependOnKeyEncryptor + +impl DependOnSigner for Handler { + type Signer = Rsa2048Signer; + fn signer(&self) -> &Self::Signer { + &self.signer + } +} + +impl DependOnSignatureVerifier for Handler { + type SignatureVerifier = Rsa2048Verifier; + fn signature_verifier(&self) -> &Self::SignatureVerifier { + &self.verifier } } diff --git a/server/src/route/account.rs b/server/src/route/account.rs index e09673f..a43fd35 100644 --- a/server/src/route/account.rs +++ b/server/src/route/account.rs @@ -70,7 +70,6 @@ async fn get_accounts( let pagination = Pagination::new(limit, cursor, direction); let result = module .handler() - .pgpool() .get_all_accounts( module.applier_container().deref(), auth_info.into(), @@ -120,7 +119,6 @@ async fn create_account( // サービス層でのアカウント作成処理の呼び出し let account = module .handler() - .pgpool() .create_account( module.applier_container().deref(), auth_info.into(), @@ -162,7 +160,6 @@ async fn get_account_by_id( // サービス層での特定アカウント取得処理 let account = module .handler() - .pgpool() .get_account_by_id(module.applier_container().deref(), auth_info.into(), id) .await .map_err(ErrorStatus::from)?; @@ -199,7 +196,6 @@ async fn delete_account_by_id( // サービス層でのアカウント削除処理 module .handler() - .pgpool() .delete_account(module.applier_container().deref(), auth_info.into(), id) .await .map_err(ErrorStatus::from)?; From dfe4ce3900473b1b357cde306dd3636af900ff2d Mon Sep 17 00:00:00 2001 From: turtton Date: Mon, 16 Feb 2026 19:31:13 +0900 Subject: [PATCH 40/59] :recycle: Replace Account creation event sourcing with direct INSERT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Account作成をイベントソーシング経由(CommandEnvelope → event_streams → 非同期Applier) から直接INSERT方式に変更し、auth_emumet_accountsリンクも同一フローで作成するように修正。 - AccountModifierトレイトにlink_auth_accountメソッド追加 - CreateAccountServiceをAccount::new() + 直接INSERT方式に書き換え - Account::create()メソッド削除(update/deleteは既存のイベントソーシングを維持) - EventVersionにDefault実装追加 - 関連テストをAccountEvent::Created直接構築に更新 --- application/src/service/account.rs | 43 +++++++------- driver/src/database/postgres/account.rs | 21 +++++++ driver/src/database/postgres/event.rs | 78 +++++++++++++------------ kernel/src/entity/account.rs | 68 +++++++-------------- kernel/src/entity/event/version.rs | 6 ++ kernel/src/modify/account.rs | 9 ++- 6 files changed, 117 insertions(+), 108 deletions(-) diff --git a/application/src/service/account.rs b/application/src/service/account.rs index 6a190f4..169784b 100644 --- a/application/src/service/account.rs +++ b/application/src/service/account.rs @@ -18,7 +18,7 @@ use kernel::interfaces::query::{ use kernel::interfaces::signal::Signal; use kernel::prelude::entity::{ Account, AccountId, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, - AuthAccountId, EventId, Nanoid, + AuthAccountId, CreatedAt, EventId, EventVersion, Nanoid, }; use kernel::KernelError; use serde_json; @@ -124,6 +124,7 @@ pub trait CreateAccountService: + Sync + Send + DependOnAccountQuery + + DependOnAccountModifier + DependOnAuthAccountQuery + DependOnAuthAccountModifier + DependOnAuthHostQuery @@ -133,19 +134,16 @@ pub trait CreateAccountService: + DependOnPasswordProvider + DependOnSigningKeyGenerator { - fn create_account( + fn create_account( &self, - signal: &S, + signal: &impl Signal, auth_info: AuthAccountInfo, name: String, is_bot: bool, - ) -> impl Future> - where - S: Signal + Signal + Send + Sync + 'static, - { + ) -> impl Future> { async move { // 認証アカウントを確認 - let _auth_account = get_auth_account(self, signal, auth_info).await?; + let auth_account = get_auth_account(self, signal, auth_info).await?; let mut transaction = self.database_connection().begin_transaction().await?; // アカウントID生成 @@ -172,30 +170,30 @@ pub trait CreateAccountService: // NanoIDの生成 let nanoid = Nanoid::::default(); - // アカウント作成イベントの生成と保存 - let create_command = Account::create( + // 直接エンティティ構築 + let created_at = CreatedAt::now(); + let version = EventVersion::default(); + let account = Account::new( account_id.clone(), account_name, private_key, public_key, account_is_bot, + None, + version, nanoid, + created_at, ); - let create_event = self - .event_modifier() - .persist_and_transform(&mut transaction, create_command) + // accountsテーブルにINSERT + self.account_modifier() + .create(&mut transaction, &account) .await?; - signal.emit(account_id).await?; - // イベントの適用 - let mut account = None; - Account::apply(&mut account, create_event)?; - - let account = account.ok_or_else(|| { - Report::new(KernelError::Internal) - .attach_printable("Failed to apply account creation event") - })?; + // auth_emumet_accountsにINSERT + self.account_modifier() + .link_auth_account(&mut transaction, &account_id, auth_account.id()) + .await?; Ok(AccountDto::from(account)) } @@ -205,6 +203,7 @@ pub trait CreateAccountService: impl CreateAccountService for T where T: 'static + DependOnAccountQuery + + DependOnAccountModifier + DependOnAuthAccountQuery + DependOnAuthAccountModifier + DependOnAuthHostQuery diff --git a/driver/src/database/postgres/account.rs b/driver/src/database/postgres/account.rs index 7d256e9..a1ec072 100644 --- a/driver/src/database/postgres/account.rs +++ b/driver/src/database/postgres/account.rs @@ -215,6 +215,27 @@ impl AccountModifier for PostgresAccountRepository { .convert_error()?; Ok(()) } + + async fn link_auth_account( + &self, + transaction: &mut Self::Transaction, + account_id: &AccountId, + auth_account_id: &AuthAccountId, + ) -> error_stack::Result<(), KernelError> { + let con: &mut PgConnection = transaction; + sqlx::query( + //language=postgresql + r#" + INSERT INTO auth_emumet_accounts (emumet_id, auth_id) VALUES ($1, $2) + "#, + ) + .bind(account_id.as_ref()) + .bind(auth_account_id.as_ref()) + .execute(con) + .await + .convert_error()?; + Ok(()) + } } impl DependOnAccountModifier for PostgresDatabase { diff --git a/driver/src/database/postgres/event.rs b/driver/src/database/postgres/event.rs index 17b8abc..a181ec3 100644 --- a/driver/src/database/postgres/event.rs +++ b/driver/src/database/postgres/event.rs @@ -216,15 +216,30 @@ mod test { mod query { use uuid::Uuid; + use crate::database::PostgresDatabase; use kernel::interfaces::database::DatabaseConnection; use kernel::interfaces::modify::{DependOnEventModifier, EventModifier}; use kernel::interfaces::query::{DependOnEventQuery, EventQuery}; use kernel::prelude::entity::{ - Account, AccountId, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, - EventId, Nanoid, + Account, AccountEvent, AccountId, AccountIsBot, AccountName, AccountPrivateKey, + AccountPublicKey, CommandEnvelope, EventId, KnownEventVersion, Nanoid, }; - use crate::database::PostgresDatabase; + fn create_account_command(account_id: AccountId) -> CommandEnvelope { + let event = AccountEvent::Created { + name: AccountName::new("test"), + private_key: AccountPrivateKey::new("test"), + public_key: AccountPublicKey::new("test"), + is_bot: AccountIsBot::new(false), + nanoid: Nanoid::default(), + }; + CommandEnvelope::new( + EventId::from(account_id), + event.name(), + event, + Some(KnownEventVersion::Nothing), + ) + } #[test_with::env(DATABASE_URL)] #[tokio::test] @@ -239,14 +254,7 @@ mod test { .await .unwrap(); assert_eq!(events.len(), 0); - let created_account = Account::create( - account_id.clone(), - AccountName::new("test"), - AccountPrivateKey::new("test"), - AccountPublicKey::new("test"), - AccountIsBot::new(false), - Nanoid::default(), - ); + let created_account = create_account_command(account_id.clone()); let updated_account = Account::update(account_id.clone(), AccountIsBot::new(true)); let deleted_account = Account::delete(account_id.clone()); @@ -281,14 +289,7 @@ mod test { let mut transaction = db.begin_transaction().await.unwrap(); let account_id = AccountId::new(Uuid::now_v7()); let event_id = EventId::from(account_id.clone()); - let created_account = Account::create( - account_id.clone(), - AccountName::new("test"), - AccountPrivateKey::new("test"), - AccountPublicKey::new("test"), - AccountIsBot::new(false), - Nanoid::default(), - ); + let created_account = create_account_command(account_id.clone()); let updated_account = Account::update(account_id.clone(), AccountIsBot::new(true)); db.event_modifier() .persist(&mut transaction, &created_account) @@ -318,15 +319,30 @@ mod test { mod modify { use uuid::Uuid; + use crate::database::PostgresDatabase; use kernel::interfaces::database::DatabaseConnection; use kernel::interfaces::modify::{DependOnEventModifier, EventModifier}; use kernel::interfaces::query::{DependOnEventQuery, EventQuery}; use kernel::prelude::entity::{ - Account, AccountId, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, - EventId, Nanoid, + Account, AccountEvent, AccountId, AccountIsBot, AccountName, AccountPrivateKey, + AccountPublicKey, CommandEnvelope, EventId, KnownEventVersion, Nanoid, }; - use crate::database::PostgresDatabase; + fn create_account_command(account_id: AccountId) -> CommandEnvelope { + let event = AccountEvent::Created { + name: AccountName::new("test"), + private_key: AccountPrivateKey::new("test"), + public_key: AccountPublicKey::new("test"), + is_bot: AccountIsBot::new(false), + nanoid: Nanoid::default(), + }; + CommandEnvelope::new( + EventId::from(account_id), + event.name(), + event, + Some(KnownEventVersion::Nothing), + ) + } #[test_with::env(DATABASE_URL)] #[tokio::test] @@ -334,14 +350,7 @@ mod test { let db = PostgresDatabase::new().await.unwrap(); let mut transaction = db.begin_transaction().await.unwrap(); let account_id = AccountId::new(Uuid::now_v7()); - let created_account = Account::create( - account_id.clone(), - AccountName::new("test"), - AccountPrivateKey::new("test"), - AccountPublicKey::new("test"), - AccountIsBot::new(false), - Nanoid::default(), - ); + let created_account = create_account_command(account_id.clone()); db.event_modifier() .persist(&mut transaction, &created_account) .await @@ -360,14 +369,7 @@ mod test { let db = PostgresDatabase::new().await.unwrap(); let mut transaction = db.begin_transaction().await.unwrap(); let account_id = AccountId::new(Uuid::now_v7()); - let created_account = Account::create( - account_id.clone(), - AccountName::new("test"), - AccountPrivateKey::new("test"), - AccountPublicKey::new("test"), - AccountIsBot::new(false), - Nanoid::default(), - ); + let created_account = create_account_command(account_id.clone()); // Test persist_and_transform let event_envelope = db diff --git a/kernel/src/entity/account.rs b/kernel/src/entity/account.rs index dda13c6..1d3503f 100644 --- a/kernel/src/entity/account.rs +++ b/kernel/src/entity/account.rs @@ -5,8 +5,7 @@ use serde::Serialize; use vodca::{Nameln, Newln, References}; use crate::entity::{ - CommandEnvelope, CreatedAt, DeletedAt, EventEnvelope, EventId, EventVersion, KnownEventVersion, - Nanoid, + CommandEnvelope, CreatedAt, DeletedAt, EventEnvelope, EventId, EventVersion, Nanoid, }; use crate::event::EventApplier; use crate::KernelError; @@ -56,29 +55,6 @@ pub enum AccountEvent { } impl Account { - pub fn create( - id: AccountId, - name: AccountName, - private_key: AccountPrivateKey, - public_key: AccountPublicKey, - is_bot: AccountIsBot, - nano_id: Nanoid, - ) -> CommandEnvelope { - let event = AccountEvent::Created { - name, - private_key, - public_key, - is_bot, - nanoid: nano_id, - }; - CommandEnvelope::new( - EventId::from(id), - event.name(), - event, - Some(KnownEventVersion::Nothing), - ) - } - pub fn update(id: AccountId, is_bot: AccountIsBot) -> CommandEnvelope { let event = AccountEvent::Updated { is_bot }; CommandEnvelope::new(EventId::from(id), event.name(), event, None) @@ -165,8 +141,8 @@ impl EventApplier for Account { #[cfg(test)] mod test { use crate::entity::{ - Account, AccountId, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, - CreatedAt, EventEnvelope, EventVersion, Nanoid, + Account, AccountEvent, AccountId, AccountIsBot, AccountName, AccountPrivateKey, + AccountPublicKey, CreatedAt, EventEnvelope, EventId, EventVersion, Nanoid, }; use crate::event::EventApplier; use crate::KernelError; @@ -180,17 +156,16 @@ mod test { let public_key = AccountPublicKey::new("public_key".to_string()); let is_bot = AccountIsBot::new(false); let nano_id = Nanoid::default(); - let event = Account::create( - id.clone(), - name.clone(), - private_key.clone(), - public_key.clone(), - is_bot.clone(), - nano_id.clone(), - ); + let event = AccountEvent::Created { + name: name.clone(), + private_key: private_key.clone(), + public_key: public_key.clone(), + is_bot: is_bot.clone(), + nanoid: nano_id.clone(), + }; let envelope = EventEnvelope::new( - event.id().clone(), - event.event().clone(), + EventId::from(id.clone()), + event, EventVersion::new(Uuid::now_v7()), ); let mut account = None; @@ -224,17 +199,16 @@ mod test { nano_id.clone(), CreatedAt::now(), ); - let event = Account::create( - id.clone(), - name.clone(), - private_key.clone(), - public_key.clone(), - is_bot.clone(), - nano_id.clone(), - ); + let event = AccountEvent::Created { + name: name.clone(), + private_key: private_key.clone(), + public_key: public_key.clone(), + is_bot: is_bot.clone(), + nanoid: nano_id.clone(), + }; let envelope = EventEnvelope::new( - event.id().clone(), - event.event().clone(), + EventId::from(id.clone()), + event, EventVersion::new(Uuid::now_v7()), ); let mut account = Some(account); diff --git a/kernel/src/entity/event/version.rs b/kernel/src/entity/event/version.rs index 3373ccd..66b760f 100644 --- a/kernel/src/entity/event/version.rs +++ b/kernel/src/entity/event/version.rs @@ -12,6 +12,12 @@ impl EventVersion { } } +impl Default for EventVersion { + fn default() -> Self { + Self(Uuid::now_v7(), PhantomData) + } +} + impl Serialize for EventVersion { fn serialize(&self, serializer: S) -> Result where diff --git a/kernel/src/modify/account.rs b/kernel/src/modify/account.rs index 63d5d3b..4483d53 100644 --- a/kernel/src/modify/account.rs +++ b/kernel/src/modify/account.rs @@ -1,5 +1,5 @@ use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Transaction}; -use crate::entity::{Account, AccountId}; +use crate::entity::{Account, AccountId, AuthAccountId}; use crate::KernelError; use std::future::Future; @@ -23,6 +23,13 @@ pub trait AccountModifier: Sync + Send + 'static { transaction: &mut Self::Transaction, account_id: &AccountId, ) -> impl Future> + Send; + + fn link_auth_account( + &self, + transaction: &mut Self::Transaction, + account_id: &AccountId, + auth_account_id: &AuthAccountId, + ) -> impl Future> + Send; } pub trait DependOnAccountModifier: Sync + Send + DependOnDatabaseConnection { From f38a1d1fcf8e5b5878810418a60f5c1b9f965f8b Mon Sep 17 00:00:00 2001 From: turtton Date: Tue, 17 Feb 2026 22:47:04 +0900 Subject: [PATCH 41/59] :sparkles: Add PUT /accounts/:id endpoint for updating account is_bot field MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit EditAccountServiceトレイトとupdate_account_by_idハンドラを追加し、 イベントソーシングによるアカウントのis_botフィールド更新を実装 --- application/src/service/account.rs | 90 ++++++++++++++++++++++++++++++ server/src/route/account.rs | 52 ++++++++++++++++- 2 files changed, 140 insertions(+), 2 deletions(-) diff --git a/application/src/service/account.rs b/application/src/service/account.rs index 169784b..be0afdd 100644 --- a/application/src/service/account.rs +++ b/application/src/service/account.rs @@ -350,3 +350,93 @@ impl DeleteAccountService for T where + UpdateAccountService { } + +pub trait EditAccountService: + 'static + + Sync + + Send + + DependOnAccountQuery + + DependOnAuthAccountQuery + + DependOnAuthAccountModifier + + DependOnAuthHostQuery + + DependOnAuthHostModifier + + DependOnEventModifier + + DependOnEventQuery +{ + fn edit_account( + &self, + signal: &S, + auth_info: AuthAccountInfo, + account_id: String, + is_bot: bool, + ) -> impl Future> + where + S: Signal + Signal + Send + Sync + 'static, + { + async move { + // 認証アカウントを確認 + let auth_account = get_auth_account(self, signal, auth_info).await?; + let mut transaction = self.database_connection().begin_transaction().await?; + + // Nanoidからアカウントを検索 + let nanoid = Nanoid::::new(account_id); + let account = self + .account_query() + .find_by_nanoid(&mut transaction, &nanoid) + .await? + .ok_or_else(|| { + Report::new(KernelError::NotFound).attach_printable(format!( + "Account not found with nanoid: {}", + nanoid.as_ref() + )) + })?; + + // 権限チェック (認証アカウントに紐づくアカウントのみ更新可能) + let auth_account_id = auth_account.id().clone(); + let accounts = self + .account_query() + .find_by_auth_id(&mut transaction, &auth_account_id) + .await?; + + let found = accounts.iter().any(|a| a.id() == account.id()); + if !found { + return Err(Report::new(KernelError::PermissionDenied) + .attach_printable("This account does not belong to the authenticated user")); + } + + // 更新イベントの生成と保存 + let account_id = account.id().clone(); + let update_command = Account::update(account_id.clone(), AccountIsBot::new(is_bot)); + + self.event_modifier() + .persist_and_transform(&mut transaction, update_command) + .await?; + signal.emit(account_id).await?; + + // 更新後のアカウント情報を取得して返却 + let updated_account = self + .account_query() + .find_by_nanoid(&mut transaction, &nanoid) + .await? + .ok_or_else(|| { + Report::new(KernelError::Internal) + .attach_printable("Account disappeared after update") + })?; + + Ok(AccountDto::from(updated_account)) + } + } +} + +impl EditAccountService for T where + T: 'static + + DependOnAccountQuery + + DependOnAuthAccountQuery + + DependOnAuthAccountModifier + + DependOnAuthHostQuery + + DependOnAuthHostModifier + + DependOnEventModifier + + DependOnEventQuery + + UpdateAccountService +{ +} diff --git a/server/src/route/account.rs b/server/src/route/account.rs index a43fd35..6898989 100644 --- a/server/src/route/account.rs +++ b/server/src/route/account.rs @@ -4,13 +4,13 @@ use crate::handler::AppModule; use crate::keycloak::KeycloakAuthAccount; use crate::route::DirectionConverter; use application::service::account::{ - CreateAccountService, DeleteAccountService, GetAccountService, + CreateAccountService, DeleteAccountService, EditAccountService, GetAccountService, }; use application::transfer::pagination::Pagination; use axum::extract::{Path, Query, State}; use axum::http::StatusCode; use axum::http::{Method, Uri}; -use axum::routing::{delete, get, post}; +use axum::routing::{delete, get, post, put}; use axum::{Extension, Json, Router}; use axum_keycloak_auth::decode::KeycloakToken; use axum_keycloak_auth::instance::KeycloakAuthInstance; @@ -33,6 +33,11 @@ struct CreateAccountRequest { is_bot: bool, } +#[derive(Debug, Deserialize)] +struct UpdateAccountRequest { + is_bot: bool, +} + #[derive(Debug, Serialize)] struct AccountResponse { id: String, @@ -175,6 +180,48 @@ async fn get_account_by_id( Ok(Json(response)) } +async fn update_account_by_id( + Extension(token): Extension>, + State(module): State, + method: Method, + uri: Uri, + Path(id): Path, + Json(request): Json, +) -> Result, ErrorStatus> { + expect_role!(&token, uri, method); + let auth_info = KeycloakAuthAccount::from(token); + + // IDの検証 + if id.trim().is_empty() { + return Err(ErrorStatus::from(( + StatusCode::BAD_REQUEST, + "Account ID cannot be empty".to_string(), + ))); + } + + // サービス層でのアカウント更新処理 + let account = module + .handler() + .edit_account( + module.applier_container().deref(), + auth_info.into(), + id, + request.is_bot, + ) + .await + .map_err(ErrorStatus::from)?; + + let response = AccountResponse { + id: account.nanoid, + name: account.name, + public_key: account.public_key, + is_bot: account.is_bot, + created_at: account.created_at, + }; + + Ok(Json(response)) +} + async fn delete_account_by_id( Extension(token): Extension>, State(module): State, @@ -208,6 +255,7 @@ impl AccountRouter for Router { self.route("/accounts", get(get_accounts)) .route("/accounts", post(create_account)) .route("/accounts/:id", get(get_account_by_id)) + .route("/accounts/:id", put(update_account_by_id)) .route("/accounts/:id", delete(delete_account_by_id)) .layer( KeycloakAuthLayer::::builder() From 6f27c9a95d4d12e4e75f934184d4f2df6c24057b Mon Sep 17 00:00:00 2001 From: turtton Date: Wed, 18 Feb 2026 11:25:57 +0900 Subject: [PATCH 42/59] :recycle: Return 204 No Content from PUT /accounts/:id MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 非同期プロジェクション適用前に再取得すると古いデータを返す可能性があるため、 DELETEと同様に204 No Contentを返すように変更 --- application/src/service/account.rs | 14 ++------------ server/src/route/account.rs | 14 +++----------- 2 files changed, 5 insertions(+), 23 deletions(-) diff --git a/application/src/service/account.rs b/application/src/service/account.rs index be0afdd..91464a9 100644 --- a/application/src/service/account.rs +++ b/application/src/service/account.rs @@ -369,7 +369,7 @@ pub trait EditAccountService: auth_info: AuthAccountInfo, account_id: String, is_bot: bool, - ) -> impl Future> + ) -> impl Future> where S: Signal + Signal + Send + Sync + 'static, { @@ -413,17 +413,7 @@ pub trait EditAccountService: .await?; signal.emit(account_id).await?; - // 更新後のアカウント情報を取得して返却 - let updated_account = self - .account_query() - .find_by_nanoid(&mut transaction, &nanoid) - .await? - .ok_or_else(|| { - Report::new(KernelError::Internal) - .attach_printable("Account disappeared after update") - })?; - - Ok(AccountDto::from(updated_account)) + Ok(()) } } } diff --git a/server/src/route/account.rs b/server/src/route/account.rs index 6898989..828f011 100644 --- a/server/src/route/account.rs +++ b/server/src/route/account.rs @@ -187,7 +187,7 @@ async fn update_account_by_id( uri: Uri, Path(id): Path, Json(request): Json, -) -> Result, ErrorStatus> { +) -> Result { expect_role!(&token, uri, method); let auth_info = KeycloakAuthAccount::from(token); @@ -200,7 +200,7 @@ async fn update_account_by_id( } // サービス層でのアカウント更新処理 - let account = module + module .handler() .edit_account( module.applier_container().deref(), @@ -211,15 +211,7 @@ async fn update_account_by_id( .await .map_err(ErrorStatus::from)?; - let response = AccountResponse { - id: account.nanoid, - name: account.name, - public_key: account.public_key, - is_bot: account.is_bot, - created_at: account.created_at, - }; - - Ok(Json(response)) + Ok(StatusCode::NO_CONTENT) } async fn delete_account_by_id( From 43bee1579cdc027f715eaee17a731f107426f1b7 Mon Sep 17 00:00:00 2001 From: turtton Date: Sun, 8 Mar 2026 22:33:06 +0900 Subject: [PATCH 43/59] :recycle: Add AccountRepository adapter to unify Query/Modifier access MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit cryptoで採用したadapterパターン(低レベルtrait合成)をaccountにも適用。 AccountQuery + AccountModifier → AccountRepository として合成し、 application層からはaccount_repository()経由で統一的にアクセスする。 blanket implによりkernel/driver/serverの変更は不要。 --- adapter/src/account.rs | 169 +++++++++++++++++++++++++++++ adapter/src/lib.rs | 1 + application/src/service/account.rs | 63 +++++------ 3 files changed, 196 insertions(+), 37 deletions(-) create mode 100644 adapter/src/account.rs diff --git a/adapter/src/account.rs b/adapter/src/account.rs new file mode 100644 index 0000000..9b0e076 --- /dev/null +++ b/adapter/src/account.rs @@ -0,0 +1,169 @@ +use kernel::interfaces::database::{DatabaseConnection, DependOnDatabaseConnection, Transaction}; +use kernel::interfaces::modify::{AccountModifier, DependOnAccountModifier}; +use kernel::interfaces::query::{AccountQuery, DependOnAccountQuery}; +use kernel::prelude::entity::{Account, AccountId, AccountName, AuthAccountId, Nanoid}; +use kernel::KernelError; +use std::future::Future; + +/// Trait for account repository operations (composed from AccountQuery + AccountModifier) +/// +/// This trait is automatically implemented for any type that implements both +/// [`DependOnAccountQuery`] and [`DependOnAccountModifier`] via blanket implementation. +/// +/// # Architecture +/// +/// ```text +/// Application (uses AccountRepository) +/// ↓ +/// Adapter (composes AccountQuery + AccountModifier) +/// ↓ +/// Kernel (defines traits) +/// ↑ +/// Driver (implements concrete datastore) +/// ``` +pub trait AccountRepository: Send + Sync { + type Transaction: Transaction; + + // Query operations + fn find_by_id( + &self, + transaction: &mut Self::Transaction, + id: &AccountId, + ) -> impl Future, KernelError>> + Send; + + fn find_by_auth_id( + &self, + transaction: &mut Self::Transaction, + auth_id: &AuthAccountId, + ) -> impl Future, KernelError>> + Send; + + fn find_by_name( + &self, + transaction: &mut Self::Transaction, + name: &AccountName, + ) -> impl Future, KernelError>> + Send; + + fn find_by_nanoid( + &self, + transaction: &mut Self::Transaction, + nanoid: &Nanoid, + ) -> impl Future, KernelError>> + Send; + + // Modifier operations + fn create( + &self, + transaction: &mut Self::Transaction, + account: &Account, + ) -> impl Future> + Send; + + fn update( + &self, + transaction: &mut Self::Transaction, + account: &Account, + ) -> impl Future> + Send; + + fn delete( + &self, + transaction: &mut Self::Transaction, + account_id: &AccountId, + ) -> impl Future> + Send; + + fn link_auth_account( + &self, + transaction: &mut Self::Transaction, + account_id: &AccountId, + auth_account_id: &AuthAccountId, + ) -> impl Future> + Send; +} + +pub trait DependOnAccountRepository: DependOnDatabaseConnection + Send + Sync { + type AccountRepository: AccountRepository< + Transaction = ::Transaction, + >; + fn account_repository(&self) -> &Self::AccountRepository; +} + +// Blanket implementation: any type with AccountQuery + AccountModifier can act as AccountRepository +impl AccountRepository for T +where + T: DependOnAccountQuery + DependOnAccountModifier + Send + Sync, +{ + type Transaction = ::Transaction; + + fn find_by_id( + &self, + transaction: &mut Self::Transaction, + id: &AccountId, + ) -> impl Future, KernelError>> + Send { + self.account_query().find_by_id(transaction, id) + } + + fn find_by_auth_id( + &self, + transaction: &mut Self::Transaction, + auth_id: &AuthAccountId, + ) -> impl Future, KernelError>> + Send { + self.account_query().find_by_auth_id(transaction, auth_id) + } + + fn find_by_name( + &self, + transaction: &mut Self::Transaction, + name: &AccountName, + ) -> impl Future, KernelError>> + Send { + self.account_query().find_by_name(transaction, name) + } + + fn find_by_nanoid( + &self, + transaction: &mut Self::Transaction, + nanoid: &Nanoid, + ) -> impl Future, KernelError>> + Send { + self.account_query().find_by_nanoid(transaction, nanoid) + } + + fn create( + &self, + transaction: &mut Self::Transaction, + account: &Account, + ) -> impl Future> + Send { + self.account_modifier().create(transaction, account) + } + + fn update( + &self, + transaction: &mut Self::Transaction, + account: &Account, + ) -> impl Future> + Send { + self.account_modifier().update(transaction, account) + } + + fn delete( + &self, + transaction: &mut Self::Transaction, + account_id: &AccountId, + ) -> impl Future> + Send { + self.account_modifier().delete(transaction, account_id) + } + + fn link_auth_account( + &self, + transaction: &mut Self::Transaction, + account_id: &AccountId, + auth_account_id: &AuthAccountId, + ) -> impl Future> + Send { + self.account_modifier() + .link_auth_account(transaction, account_id, auth_account_id) + } +} + +// Blanket implementation: any type that satisfies the requirements provides DependOnAccountRepository +impl DependOnAccountRepository for T +where + T: DependOnAccountQuery + DependOnAccountModifier + DependOnDatabaseConnection + Send + Sync, +{ + type AccountRepository = Self; + fn account_repository(&self) -> &Self::AccountRepository { + self + } +} diff --git a/adapter/src/lib.rs b/adapter/src/lib.rs index 274f0ed..3832592 100644 --- a/adapter/src/lib.rs +++ b/adapter/src/lib.rs @@ -1 +1,2 @@ +pub mod account; pub mod crypto; diff --git a/application/src/service/account.rs b/application/src/service/account.rs index 91464a9..2fa50bf 100644 --- a/application/src/service/account.rs +++ b/application/src/service/account.rs @@ -2,18 +2,17 @@ use crate::service::auth_account::get_auth_account; use crate::transfer::account::AccountDto; use crate::transfer::auth_account::AuthAccountInfo; use crate::transfer::pagination::{apply_pagination, Pagination}; +use adapter::account::{AccountRepository, DependOnAccountRepository}; use adapter::crypto::{DependOnSigningKeyGenerator, SigningKeyGenerator}; use error_stack::Report; use kernel::interfaces::crypto::{DependOnPasswordProvider, PasswordProvider}; use kernel::interfaces::database::DatabaseConnection; use kernel::interfaces::event::EventApplier; use kernel::interfaces::modify::{ - AccountModifier, DependOnAccountModifier, DependOnAuthAccountModifier, - DependOnAuthHostModifier, DependOnEventModifier, EventModifier, + DependOnAuthAccountModifier, DependOnAuthHostModifier, DependOnEventModifier, EventModifier, }; use kernel::interfaces::query::{ - AccountQuery, DependOnAccountQuery, DependOnAuthAccountQuery, DependOnAuthHostQuery, - DependOnEventQuery, EventQuery, + DependOnAuthAccountQuery, DependOnAuthHostQuery, DependOnEventQuery, EventQuery, }; use kernel::interfaces::signal::Signal; use kernel::prelude::entity::{ @@ -28,7 +27,7 @@ pub trait GetAccountService: 'static + Sync + Send - + DependOnAccountQuery + + DependOnAccountRepository + DependOnAuthAccountQuery + DependOnAuthAccountModifier + DependOnAuthHostQuery @@ -50,12 +49,12 @@ pub trait GetAccountService: let auth_account = get_auth_account(self, signal, account).await?; let mut transaction = self.database_connection().begin_transaction().await?; let accounts = self - .account_query() + .account_repository() .find_by_auth_id(&mut transaction, auth_account.id()) .await?; let cursor = if let Some(cursor) = cursor { let id: Nanoid = Nanoid::new(cursor); - self.account_query() + self.account_repository() .find_by_nanoid(&mut transaction, &id) .await? } else { @@ -79,7 +78,7 @@ pub trait GetAccountService: // Nanoidからアカウントを検索 let nanoid = Nanoid::::new(account_id); let account = self - .account_query() + .account_repository() .find_by_nanoid(&mut transaction, &nanoid) .await? .ok_or_else(|| { @@ -92,7 +91,7 @@ pub trait GetAccountService: // 権限チェック (認証アカウントに紐づくアカウントのみアクセス可能) let auth_account_id = auth_account.id(); let accounts = self - .account_query() + .account_repository() .find_by_auth_id(&mut transaction, auth_account_id) .await?; @@ -109,7 +108,7 @@ pub trait GetAccountService: impl GetAccountService for T where T: 'static - + DependOnAccountQuery + + DependOnAccountRepository + DependOnAuthAccountQuery + DependOnAuthAccountModifier + DependOnAuthHostQuery @@ -123,8 +122,7 @@ pub trait CreateAccountService: 'static + Sync + Send - + DependOnAccountQuery - + DependOnAccountModifier + + DependOnAccountRepository + DependOnAuthAccountQuery + DependOnAuthAccountModifier + DependOnAuthHostQuery @@ -186,12 +184,12 @@ pub trait CreateAccountService: ); // accountsテーブルにINSERT - self.account_modifier() + self.account_repository() .create(&mut transaction, &account) .await?; // auth_emumet_accountsにINSERT - self.account_modifier() + self.account_repository() .link_auth_account(&mut transaction, &account_id, auth_account.id()) .await?; @@ -202,8 +200,7 @@ pub trait CreateAccountService: impl CreateAccountService for T where T: 'static - + DependOnAccountQuery - + DependOnAccountModifier + + DependOnAccountRepository + DependOnAuthAccountQuery + DependOnAuthAccountModifier + DependOnAuthHostQuery @@ -216,11 +213,7 @@ impl CreateAccountService for T where } pub trait UpdateAccountService: - 'static - + DependOnAccountQuery - + DependOnAccountModifier - + DependOnEventQuery - + DependOnEventModifier + 'static + DependOnAccountRepository + DependOnEventQuery + DependOnEventModifier { fn update_account( &self, @@ -229,7 +222,7 @@ pub trait UpdateAccountService: async move { let mut transaction = self.database_connection().begin_transaction().await?; let account = self - .account_query() + .account_repository() .find_by_id(&mut transaction, &account_id) .await?; if let Some(account) = account { @@ -246,11 +239,11 @@ pub trait UpdateAccountService: Account::apply(&mut account, event)?; } if let Some(account) = account { - self.account_modifier() + self.account_repository() .update(&mut transaction, &account) .await?; } else { - self.account_modifier() + self.account_repository() .delete(&mut transaction, &account_id) .await?; } @@ -264,11 +257,7 @@ pub trait UpdateAccountService: } impl UpdateAccountService for T where - T: 'static - + DependOnAccountQuery - + DependOnAccountModifier - + DependOnEventQuery - + DependOnEventModifier + T: 'static + DependOnAccountRepository + DependOnEventQuery + DependOnEventModifier { } @@ -276,7 +265,7 @@ pub trait DeleteAccountService: 'static + Sync + Send - + DependOnAccountQuery + + DependOnAccountRepository + DependOnAuthAccountQuery + DependOnAuthAccountModifier + DependOnAuthHostQuery @@ -301,7 +290,7 @@ pub trait DeleteAccountService: // Nanoidからアカウントを検索 let nanoid = Nanoid::::new(account_id); let account = self - .account_query() + .account_repository() .find_by_nanoid(&mut transaction, &nanoid) .await? .ok_or_else(|| { @@ -314,7 +303,7 @@ pub trait DeleteAccountService: // 権限チェック (認証アカウントに紐づくアカウントのみ削除可能) let auth_account_id = auth_account.id().clone(); let accounts = self - .account_query() + .account_repository() .find_by_auth_id(&mut transaction, &auth_account_id) .await?; @@ -340,7 +329,7 @@ pub trait DeleteAccountService: impl DeleteAccountService for T where T: 'static - + DependOnAccountQuery + + DependOnAccountRepository + DependOnAuthAccountQuery + DependOnAuthAccountModifier + DependOnAuthHostQuery @@ -355,7 +344,7 @@ pub trait EditAccountService: 'static + Sync + Send - + DependOnAccountQuery + + DependOnAccountRepository + DependOnAuthAccountQuery + DependOnAuthAccountModifier + DependOnAuthHostQuery @@ -381,7 +370,7 @@ pub trait EditAccountService: // Nanoidからアカウントを検索 let nanoid = Nanoid::::new(account_id); let account = self - .account_query() + .account_repository() .find_by_nanoid(&mut transaction, &nanoid) .await? .ok_or_else(|| { @@ -394,7 +383,7 @@ pub trait EditAccountService: // 権限チェック (認証アカウントに紐づくアカウントのみ更新可能) let auth_account_id = auth_account.id().clone(); let accounts = self - .account_query() + .account_repository() .find_by_auth_id(&mut transaction, &auth_account_id) .await?; @@ -420,7 +409,7 @@ pub trait EditAccountService: impl EditAccountService for T where T: 'static - + DependOnAccountQuery + + DependOnAccountRepository + DependOnAuthAccountQuery + DependOnAuthAccountModifier + DependOnAuthHostQuery From 70157f58989ec8b48e43073add81e6a41ec78417 Mon Sep 17 00:00:00 2001 From: turtton Date: Mon, 9 Mar 2026 10:50:08 +0900 Subject: [PATCH 44/59] :recycle: Introduce AccountReadModel and AccountEventStore for proper CQRS separation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AccountQuery/AccountModifier/AccountRepositoryを廃止し、CQRS設計に沿った AccountReadModel(projection読み書き)とAccountEventStore(エンティティ別 イベント永続化)に置き換え。 - kernel: AccountReadModel trait, AccountEventStore trait 新設 - driver: PostgresAccountReadModel, PostgresAccountEventStore 実装 - migration: account_events テーブル追加(エンティティ別イベントストア) - application: 全Account操作をイベント経由に統一(Command→Event→Projection) - adapter: AccountRepository 削除(C/Q分離により不要に) --- adapter/src/account.rs | 169 --------- adapter/src/lib.rs | 1 - application/src/service/account.rs | 134 +++---- driver/src/database/postgres.rs | 1 + driver/src/database/postgres/account.rs | 88 ++--- .../database/postgres/account_event_store.rs | 337 ++++++++++++++++++ driver/src/database/postgres/follow.rs | 50 ++- driver/src/database/postgres/metadata.rs | 20 +- driver/src/database/postgres/profile.rs | 22 +- kernel/src/event_store.rs | 3 + kernel/src/event_store/account.rs | 38 ++ kernel/src/lib.rs | 36 +- kernel/src/modify.rs | 5 +- kernel/src/modify/account.rs | 41 --- kernel/src/query.rs | 5 +- kernel/src/read_model.rs | 3 + kernel/src/{query => read_model}/account.rs | 35 +- migrations/20260308000000_account_events.sql | 7 + server/src/applier/account_applier.rs | 2 +- 19 files changed, 591 insertions(+), 406 deletions(-) delete mode 100644 adapter/src/account.rs create mode 100644 driver/src/database/postgres/account_event_store.rs create mode 100644 kernel/src/event_store.rs create mode 100644 kernel/src/event_store/account.rs delete mode 100644 kernel/src/modify/account.rs create mode 100644 kernel/src/read_model.rs rename kernel/src/{query => read_model}/account.rs (50%) create mode 100644 migrations/20260308000000_account_events.sql diff --git a/adapter/src/account.rs b/adapter/src/account.rs deleted file mode 100644 index 9b0e076..0000000 --- a/adapter/src/account.rs +++ /dev/null @@ -1,169 +0,0 @@ -use kernel::interfaces::database::{DatabaseConnection, DependOnDatabaseConnection, Transaction}; -use kernel::interfaces::modify::{AccountModifier, DependOnAccountModifier}; -use kernel::interfaces::query::{AccountQuery, DependOnAccountQuery}; -use kernel::prelude::entity::{Account, AccountId, AccountName, AuthAccountId, Nanoid}; -use kernel::KernelError; -use std::future::Future; - -/// Trait for account repository operations (composed from AccountQuery + AccountModifier) -/// -/// This trait is automatically implemented for any type that implements both -/// [`DependOnAccountQuery`] and [`DependOnAccountModifier`] via blanket implementation. -/// -/// # Architecture -/// -/// ```text -/// Application (uses AccountRepository) -/// ↓ -/// Adapter (composes AccountQuery + AccountModifier) -/// ↓ -/// Kernel (defines traits) -/// ↑ -/// Driver (implements concrete datastore) -/// ``` -pub trait AccountRepository: Send + Sync { - type Transaction: Transaction; - - // Query operations - fn find_by_id( - &self, - transaction: &mut Self::Transaction, - id: &AccountId, - ) -> impl Future, KernelError>> + Send; - - fn find_by_auth_id( - &self, - transaction: &mut Self::Transaction, - auth_id: &AuthAccountId, - ) -> impl Future, KernelError>> + Send; - - fn find_by_name( - &self, - transaction: &mut Self::Transaction, - name: &AccountName, - ) -> impl Future, KernelError>> + Send; - - fn find_by_nanoid( - &self, - transaction: &mut Self::Transaction, - nanoid: &Nanoid, - ) -> impl Future, KernelError>> + Send; - - // Modifier operations - fn create( - &self, - transaction: &mut Self::Transaction, - account: &Account, - ) -> impl Future> + Send; - - fn update( - &self, - transaction: &mut Self::Transaction, - account: &Account, - ) -> impl Future> + Send; - - fn delete( - &self, - transaction: &mut Self::Transaction, - account_id: &AccountId, - ) -> impl Future> + Send; - - fn link_auth_account( - &self, - transaction: &mut Self::Transaction, - account_id: &AccountId, - auth_account_id: &AuthAccountId, - ) -> impl Future> + Send; -} - -pub trait DependOnAccountRepository: DependOnDatabaseConnection + Send + Sync { - type AccountRepository: AccountRepository< - Transaction = ::Transaction, - >; - fn account_repository(&self) -> &Self::AccountRepository; -} - -// Blanket implementation: any type with AccountQuery + AccountModifier can act as AccountRepository -impl AccountRepository for T -where - T: DependOnAccountQuery + DependOnAccountModifier + Send + Sync, -{ - type Transaction = ::Transaction; - - fn find_by_id( - &self, - transaction: &mut Self::Transaction, - id: &AccountId, - ) -> impl Future, KernelError>> + Send { - self.account_query().find_by_id(transaction, id) - } - - fn find_by_auth_id( - &self, - transaction: &mut Self::Transaction, - auth_id: &AuthAccountId, - ) -> impl Future, KernelError>> + Send { - self.account_query().find_by_auth_id(transaction, auth_id) - } - - fn find_by_name( - &self, - transaction: &mut Self::Transaction, - name: &AccountName, - ) -> impl Future, KernelError>> + Send { - self.account_query().find_by_name(transaction, name) - } - - fn find_by_nanoid( - &self, - transaction: &mut Self::Transaction, - nanoid: &Nanoid, - ) -> impl Future, KernelError>> + Send { - self.account_query().find_by_nanoid(transaction, nanoid) - } - - fn create( - &self, - transaction: &mut Self::Transaction, - account: &Account, - ) -> impl Future> + Send { - self.account_modifier().create(transaction, account) - } - - fn update( - &self, - transaction: &mut Self::Transaction, - account: &Account, - ) -> impl Future> + Send { - self.account_modifier().update(transaction, account) - } - - fn delete( - &self, - transaction: &mut Self::Transaction, - account_id: &AccountId, - ) -> impl Future> + Send { - self.account_modifier().delete(transaction, account_id) - } - - fn link_auth_account( - &self, - transaction: &mut Self::Transaction, - account_id: &AccountId, - auth_account_id: &AuthAccountId, - ) -> impl Future> + Send { - self.account_modifier() - .link_auth_account(transaction, account_id, auth_account_id) - } -} - -// Blanket implementation: any type that satisfies the requirements provides DependOnAccountRepository -impl DependOnAccountRepository for T -where - T: DependOnAccountQuery + DependOnAccountModifier + DependOnDatabaseConnection + Send + Sync, -{ - type AccountRepository = Self; - fn account_repository(&self) -> &Self::AccountRepository { - self - } -} diff --git a/adapter/src/lib.rs b/adapter/src/lib.rs index 3832592..274f0ed 100644 --- a/adapter/src/lib.rs +++ b/adapter/src/lib.rs @@ -1,2 +1 @@ -pub mod account; pub mod crypto; diff --git a/application/src/service/account.rs b/application/src/service/account.rs index 2fa50bf..42272ca 100644 --- a/application/src/service/account.rs +++ b/application/src/service/account.rs @@ -2,22 +2,23 @@ use crate::service::auth_account::get_auth_account; use crate::transfer::account::AccountDto; use crate::transfer::auth_account::AuthAccountInfo; use crate::transfer::pagination::{apply_pagination, Pagination}; -use adapter::account::{AccountRepository, DependOnAccountRepository}; use adapter::crypto::{DependOnSigningKeyGenerator, SigningKeyGenerator}; use error_stack::Report; use kernel::interfaces::crypto::{DependOnPasswordProvider, PasswordProvider}; use kernel::interfaces::database::DatabaseConnection; use kernel::interfaces::event::EventApplier; +use kernel::interfaces::event_store::{AccountEventStore, DependOnAccountEventStore}; use kernel::interfaces::modify::{ - DependOnAuthAccountModifier, DependOnAuthHostModifier, DependOnEventModifier, EventModifier, + DependOnAuthAccountModifier, DependOnAuthHostModifier, DependOnEventModifier, }; use kernel::interfaces::query::{ - DependOnAuthAccountQuery, DependOnAuthHostQuery, DependOnEventQuery, EventQuery, + DependOnAuthAccountQuery, DependOnAuthHostQuery, DependOnEventQuery, }; +use kernel::interfaces::read_model::{AccountReadModel, DependOnAccountReadModel}; use kernel::interfaces::signal::Signal; use kernel::prelude::entity::{ - Account, AccountId, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, - AuthAccountId, CreatedAt, EventId, EventVersion, Nanoid, + Account, AccountEvent, AccountId, AccountIsBot, AccountName, AccountPrivateKey, + AccountPublicKey, AuthAccountId, CommandEnvelope, EventId, Nanoid, }; use kernel::KernelError; use serde_json; @@ -27,7 +28,7 @@ pub trait GetAccountService: 'static + Sync + Send - + DependOnAccountRepository + + DependOnAccountReadModel + DependOnAuthAccountQuery + DependOnAuthAccountModifier + DependOnAuthHostQuery @@ -49,12 +50,12 @@ pub trait GetAccountService: let auth_account = get_auth_account(self, signal, account).await?; let mut transaction = self.database_connection().begin_transaction().await?; let accounts = self - .account_repository() + .account_read_model() .find_by_auth_id(&mut transaction, auth_account.id()) .await?; let cursor = if let Some(cursor) = cursor { let id: Nanoid = Nanoid::new(cursor); - self.account_repository() + self.account_read_model() .find_by_nanoid(&mut transaction, &id) .await? } else { @@ -75,10 +76,9 @@ pub trait GetAccountService: let auth_account = get_auth_account(self, signal, auth_info).await?; let mut transaction = self.database_connection().begin_transaction().await?; - // Nanoidからアカウントを検索 let nanoid = Nanoid::::new(account_id); let account = self - .account_repository() + .account_read_model() .find_by_nanoid(&mut transaction, &nanoid) .await? .ok_or_else(|| { @@ -88,10 +88,9 @@ pub trait GetAccountService: )) })?; - // 権限チェック (認証アカウントに紐づくアカウントのみアクセス可能) let auth_account_id = auth_account.id(); let accounts = self - .account_repository() + .account_read_model() .find_by_auth_id(&mut transaction, auth_account_id) .await?; @@ -108,7 +107,7 @@ pub trait GetAccountService: impl GetAccountService for T where T: 'static - + DependOnAccountRepository + + DependOnAccountReadModel + DependOnAuthAccountQuery + DependOnAuthAccountModifier + DependOnAuthHostQuery @@ -122,7 +121,8 @@ pub trait CreateAccountService: 'static + Sync + Send - + DependOnAccountRepository + + DependOnAccountReadModel + + DependOnAccountEventStore + DependOnAuthAccountQuery + DependOnAuthAccountModifier + DependOnAuthHostQuery @@ -132,26 +132,26 @@ pub trait CreateAccountService: + DependOnPasswordProvider + DependOnSigningKeyGenerator { - fn create_account( + fn create_account( &self, - signal: &impl Signal, + signal: &S, auth_info: AuthAccountInfo, name: String, is_bot: bool, - ) -> impl Future> { + ) -> impl Future> + where + S: Signal + Signal + Send + Sync + 'static, + { async move { - // 認証アカウントを確認 let auth_account = get_auth_account(self, signal, auth_info).await?; let mut transaction = self.database_connection().begin_transaction().await?; - // アカウントID生成 let account_id = AccountId::default(); - // 鍵ペア生成 (DI経由で取得したPasswordProviderとSigningKeyGeneratorを使用) + // Generate key pair let master_password = self.password_provider().get_password()?; let key_pair = self.signing_key_generator().generate(&master_password)?; - // 暗号化された秘密鍵をJSON文字列として保存 let encrypted_private_key_json = serde_json::to_string(&key_pair.encrypted_private_key) .map_err(|e| { Report::new(KernelError::Internal) @@ -160,36 +160,45 @@ pub trait CreateAccountService: let private_key = AccountPrivateKey::new(encrypted_private_key_json); let public_key = AccountPublicKey::new(key_pair.public_key_pem); - - // アカウント名とbot状態設定 let account_name = AccountName::new(name); let account_is_bot = AccountIsBot::new(is_bot); - - // NanoIDの生成 let nanoid = Nanoid::::default(); - // 直接エンティティ構築 - let created_at = CreatedAt::now(); - let version = EventVersion::default(); - let account = Account::new( - account_id.clone(), - account_name, + // Create command and persist event + let event = AccountEvent::Created { + name: account_name, private_key, public_key, - account_is_bot, - None, - version, + is_bot: account_is_bot, nanoid, - created_at, + }; + let command = CommandEnvelope::new( + EventId::from(account_id.clone()), + event.name(), + event, + Some(kernel::prelude::entity::KnownEventVersion::Nothing), ); - // accountsテーブルにINSERT - self.account_repository() + let event_envelope = self + .account_event_store() + .persist_and_transform(&mut transaction, command) + .await?; + + // Apply event to build entity + let mut account = None; + Account::apply(&mut account, event_envelope)?; + let account = account.ok_or_else(|| { + Report::new(KernelError::Internal) + .attach_printable("Failed to construct account from created event") + })?; + + // Update projection + self.account_read_model() .create(&mut transaction, &account) .await?; - // auth_emumet_accountsにINSERT - self.account_repository() + // Link auth account + self.account_read_model() .link_auth_account(&mut transaction, &account_id, auth_account.id()) .await?; @@ -200,7 +209,8 @@ pub trait CreateAccountService: impl CreateAccountService for T where T: 'static - + DependOnAccountRepository + + DependOnAccountReadModel + + DependOnAccountEventStore + DependOnAuthAccountQuery + DependOnAuthAccountModifier + DependOnAuthHostQuery @@ -213,7 +223,7 @@ impl CreateAccountService for T where } pub trait UpdateAccountService: - 'static + DependOnAccountRepository + DependOnEventQuery + DependOnEventModifier + 'static + DependOnAccountReadModel + DependOnAccountEventStore { fn update_account( &self, @@ -222,13 +232,13 @@ pub trait UpdateAccountService: async move { let mut transaction = self.database_connection().begin_transaction().await?; let account = self - .account_repository() + .account_read_model() .find_by_id(&mut transaction, &account_id) .await?; if let Some(account) = account { let event_id = EventId::from(account_id.clone()); let events = self - .event_query() + .account_event_store() .find_by_id(&mut transaction, &event_id, Some(account.version())) .await?; if events.is_empty() { @@ -239,11 +249,11 @@ pub trait UpdateAccountService: Account::apply(&mut account, event)?; } if let Some(account) = account { - self.account_repository() + self.account_read_model() .update(&mut transaction, &account) .await?; } else { - self.account_repository() + self.account_read_model() .delete(&mut transaction, &account_id) .await?; } @@ -257,7 +267,7 @@ pub trait UpdateAccountService: } impl UpdateAccountService for T where - T: 'static + DependOnAccountRepository + DependOnEventQuery + DependOnEventModifier + T: 'static + DependOnAccountReadModel + DependOnAccountEventStore { } @@ -265,7 +275,8 @@ pub trait DeleteAccountService: 'static + Sync + Send - + DependOnAccountRepository + + DependOnAccountReadModel + + DependOnAccountEventStore + DependOnAuthAccountQuery + DependOnAuthAccountModifier + DependOnAuthHostQuery @@ -283,14 +294,12 @@ pub trait DeleteAccountService: S: Signal + Signal + Send + Sync + 'static, { async move { - // 認証アカウントを確認 let auth_account = get_auth_account(self, signal, auth_info).await?; let mut transaction = self.database_connection().begin_transaction().await?; - // Nanoidからアカウントを検索 let nanoid = Nanoid::::new(account_id); let account = self - .account_repository() + .account_read_model() .find_by_nanoid(&mut transaction, &nanoid) .await? .ok_or_else(|| { @@ -300,10 +309,9 @@ pub trait DeleteAccountService: )) })?; - // 権限チェック (認証アカウントに紐づくアカウントのみ削除可能) let auth_account_id = auth_account.id().clone(); let accounts = self - .account_repository() + .account_read_model() .find_by_auth_id(&mut transaction, &auth_account_id) .await?; @@ -313,11 +321,10 @@ pub trait DeleteAccountService: .attach_printable("This account does not belong to the authenticated user")); } - // 削除イベントの生成と保存 let account_id = account.id().clone(); let delete_command = Account::delete(account_id.clone()); - self.event_modifier() + self.account_event_store() .persist_and_transform(&mut transaction, delete_command) .await?; signal.emit(account_id).await?; @@ -329,7 +336,8 @@ pub trait DeleteAccountService: impl DeleteAccountService for T where T: 'static - + DependOnAccountRepository + + DependOnAccountReadModel + + DependOnAccountEventStore + DependOnAuthAccountQuery + DependOnAuthAccountModifier + DependOnAuthHostQuery @@ -344,7 +352,8 @@ pub trait EditAccountService: 'static + Sync + Send - + DependOnAccountRepository + + DependOnAccountReadModel + + DependOnAccountEventStore + DependOnAuthAccountQuery + DependOnAuthAccountModifier + DependOnAuthHostQuery @@ -363,14 +372,12 @@ pub trait EditAccountService: S: Signal + Signal + Send + Sync + 'static, { async move { - // 認証アカウントを確認 let auth_account = get_auth_account(self, signal, auth_info).await?; let mut transaction = self.database_connection().begin_transaction().await?; - // Nanoidからアカウントを検索 let nanoid = Nanoid::::new(account_id); let account = self - .account_repository() + .account_read_model() .find_by_nanoid(&mut transaction, &nanoid) .await? .ok_or_else(|| { @@ -380,10 +387,9 @@ pub trait EditAccountService: )) })?; - // 権限チェック (認証アカウントに紐づくアカウントのみ更新可能) let auth_account_id = auth_account.id().clone(); let accounts = self - .account_repository() + .account_read_model() .find_by_auth_id(&mut transaction, &auth_account_id) .await?; @@ -393,11 +399,10 @@ pub trait EditAccountService: .attach_printable("This account does not belong to the authenticated user")); } - // 更新イベントの生成と保存 let account_id = account.id().clone(); let update_command = Account::update(account_id.clone(), AccountIsBot::new(is_bot)); - self.event_modifier() + self.account_event_store() .persist_and_transform(&mut transaction, update_command) .await?; signal.emit(account_id).await?; @@ -409,7 +414,8 @@ pub trait EditAccountService: impl EditAccountService for T where T: 'static - + DependOnAccountRepository + + DependOnAccountReadModel + + DependOnAccountEventStore + DependOnAuthAccountQuery + DependOnAuthAccountModifier + DependOnAuthHostQuery diff --git a/driver/src/database/postgres.rs b/driver/src/database/postgres.rs index 5c37728..cf38363 100644 --- a/driver/src/database/postgres.rs +++ b/driver/src/database/postgres.rs @@ -1,4 +1,5 @@ mod account; +mod account_event_store; mod auth_account; mod auth_host; mod event; diff --git a/driver/src/database/postgres/account.rs b/driver/src/database/postgres/account.rs index a1ec072..20cc06f 100644 --- a/driver/src/database/postgres/account.rs +++ b/driver/src/database/postgres/account.rs @@ -1,7 +1,6 @@ use crate::database::{PostgresConnection, PostgresDatabase}; use crate::ConvertError; -use kernel::interfaces::modify::{AccountModifier, DependOnAccountModifier}; -use kernel::interfaces::query::{AccountQuery, DependOnAccountQuery}; +use kernel::interfaces::read_model::{AccountReadModel, DependOnAccountReadModel}; use kernel::prelude::entity::{ Account, AccountId, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, AuthAccountId, CreatedAt, DeletedAt, EventVersion, Nanoid, @@ -40,9 +39,9 @@ impl From for Account { } } -pub struct PostgresAccountRepository; +pub struct PostgresAccountReadModel; -impl AccountQuery for PostgresAccountRepository { +impl AccountReadModel for PostgresAccountReadModel { type Transaction = PostgresConnection; async fn find_by_id( @@ -129,18 +128,6 @@ impl AccountQuery for PostgresAccountRepository { .convert_error() .map(|option| option.map(Account::from)) } -} - -impl DependOnAccountQuery for PostgresDatabase { - type AccountQuery = PostgresAccountRepository; - - fn account_query(&self) -> &Self::AccountQuery { - &PostgresAccountRepository - } -} - -impl AccountModifier for PostgresAccountRepository { - type Transaction = PostgresConnection; async fn create( &self, @@ -238,25 +225,25 @@ impl AccountModifier for PostgresAccountRepository { } } -impl DependOnAccountModifier for PostgresDatabase { - type AccountModifier = PostgresAccountRepository; +impl DependOnAccountReadModel for PostgresDatabase { + type AccountReadModel = PostgresAccountReadModel; - fn account_modifier(&self) -> &Self::AccountModifier { - &PostgresAccountRepository + fn account_read_model(&self) -> &Self::AccountReadModel { + &PostgresAccountReadModel } } #[cfg(test)] mod test { - mod query { + mod read_model { use crate::database::PostgresDatabase; use kernel::interfaces::database::DatabaseConnection; - use kernel::interfaces::modify::{AccountModifier, DependOnAccountModifier}; - use kernel::interfaces::query::{AccountQuery, DependOnAccountQuery}; + use kernel::interfaces::read_model::{AccountReadModel, DependOnAccountReadModel}; use kernel::prelude::entity::{ Account, AccountId, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, - AuthAccountId, CreatedAt, EventVersion, Nanoid, + AuthAccountId, CreatedAt, DeletedAt, EventVersion, Nanoid, }; + use sqlx::types::time::OffsetDateTime; use sqlx::types::Uuid; #[test_with::env(DATABASE_URL)] @@ -278,12 +265,12 @@ mod test { CreatedAt::now(), ); database - .account_modifier() + .account_read_model() .create(&mut transaction, &account) .await .unwrap(); let result = database - .account_query() + .account_read_model() .find_by_id(&mut transaction, &id) .await .unwrap(); @@ -297,7 +284,7 @@ mod test { let mut transaction = database.begin_transaction().await.unwrap(); let accounts = database - .account_query() + .account_read_model() .find_by_auth_id(&mut transaction, &AuthAccountId::new(Uuid::now_v7())) .await .unwrap(); @@ -323,19 +310,19 @@ mod test { CreatedAt::now(), ); database - .account_modifier() + .account_read_model() .create(&mut transaction, &account) .await .unwrap(); let result = database - .account_query() + .account_read_model() .find_by_name(&mut transaction, &name) .await .unwrap(); assert_eq!(result.as_ref().map(Account::id), Some(account.id())); database - .account_modifier() + .account_read_model() .delete(&mut transaction, account.id()) .await .unwrap(); @@ -360,36 +347,23 @@ mod test { CreatedAt::now(), ); database - .account_modifier() + .account_read_model() .create(&mut transaction, &account) .await .unwrap(); let result = database - .account_query() + .account_read_model() .find_by_nanoid(&mut transaction, &nanoid) .await .unwrap(); assert_eq!(result.as_ref().map(Account::id), Some(account.id())); database - .account_modifier() + .account_read_model() .delete(&mut transaction, account.id()) .await .unwrap(); } - } - - mod modify { - use crate::database::PostgresDatabase; - use kernel::interfaces::database::DatabaseConnection; - use kernel::interfaces::modify::{AccountModifier, DependOnAccountModifier}; - use kernel::interfaces::query::{AccountQuery, DependOnAccountQuery}; - use kernel::prelude::entity::{ - Account, AccountId, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, - CreatedAt, DeletedAt, EventVersion, Nanoid, - }; - use sqlx::types::time::OffsetDateTime; - use sqlx::types::Uuid; #[test_with::env(DATABASE_URL)] #[tokio::test] @@ -409,12 +383,12 @@ mod test { CreatedAt::now(), ); database - .account_modifier() + .account_read_model() .create(&mut transaction, &account) .await .unwrap(); let result = database - .account_query() + .account_read_model() .find_by_id(&mut transaction, account.id()) .await .unwrap() @@ -440,7 +414,7 @@ mod test { CreatedAt::now(), ); database - .account_modifier() + .account_read_model() .create(&mut transaction, &account) .await .unwrap(); @@ -456,12 +430,12 @@ mod test { CreatedAt::now(), ); database - .account_modifier() + .account_read_model() .update(&mut transaction, &updated_account) .await .unwrap(); let result = database - .account_query() + .account_read_model() .find_by_id(&mut transaction, account.id()) .await .unwrap(); @@ -486,18 +460,18 @@ mod test { CreatedAt::now(), ); database - .account_modifier() + .account_read_model() .create(&mut transaction, &account) .await .unwrap(); database - .account_modifier() + .account_read_model() .delete(&mut transaction, account.id()) .await .unwrap(); let result = database - .account_query() + .account_read_model() .find_by_id(&mut transaction, account.id()) .await .unwrap(); @@ -516,18 +490,18 @@ mod test { CreatedAt::now(), ); database - .account_modifier() + .account_read_model() .create(&mut transaction, &account) .await .unwrap(); database - .account_modifier() + .account_read_model() .delete(&mut transaction, account.id()) .await .unwrap(); let result = database - .account_query() + .account_read_model() .find_by_id(&mut transaction, account.id()) .await .unwrap(); diff --git a/driver/src/database/postgres/account_event_store.rs b/driver/src/database/postgres/account_event_store.rs new file mode 100644 index 0000000..1fd2f24 --- /dev/null +++ b/driver/src/database/postgres/account_event_store.rs @@ -0,0 +1,337 @@ +use crate::database::postgres::{CountRow, VersionRow}; +use crate::database::{PostgresConnection, PostgresDatabase}; +use crate::ConvertError; +use error_stack::Report; +use kernel::interfaces::event_store::{AccountEventStore, DependOnAccountEventStore}; +use kernel::prelude::entity::{ + Account, AccountEvent, CommandEnvelope, EventEnvelope, EventId, EventVersion, KnownEventVersion, +}; +use kernel::KernelError; +use serde_json; +use sqlx::PgConnection; +use uuid::Uuid; + +#[derive(sqlx::FromRow)] +struct EventRow { + version: Uuid, + id: Uuid, + #[allow(dead_code)] + event_name: String, + data: serde_json::Value, +} + +impl TryFrom for EventEnvelope { + type Error = Report; + fn try_from(value: EventRow) -> Result { + let event: AccountEvent = serde_json::from_value(value.data).convert_error()?; + Ok(EventEnvelope::new( + EventId::new(value.id), + event, + EventVersion::new(value.version), + )) + } +} + +pub struct PostgresAccountEventStore; + +impl AccountEventStore for PostgresAccountEventStore { + type Transaction = PostgresConnection; + + async fn find_by_id( + &self, + transaction: &mut Self::Transaction, + id: &EventId, + since: Option<&EventVersion>, + ) -> error_stack::Result>, KernelError> { + let con: &mut PgConnection = transaction; + if let Some(version) = since { + sqlx::query_as::<_, EventRow>( + //language=postgresql + r#" + SELECT version, id, event_name, data + FROM account_events + WHERE id = $2 AND version > $1 + ORDER BY version + "#, + ) + .bind(version.as_ref()) + } else { + sqlx::query_as::<_, EventRow>( + //language=postgresql + r#" + SELECT version, id, event_name, data + FROM account_events + WHERE id = $1 + ORDER BY version + "#, + ) + } + .bind(id.as_ref()) + .fetch_all(con) + .await + .convert_error() + .and_then(|rows| { + rows.into_iter() + .map(|row| row.try_into()) + .collect::, KernelError>>() + }) + } + + async fn persist( + &self, + transaction: &mut Self::Transaction, + command: &CommandEnvelope, + ) -> error_stack::Result<(), KernelError> { + self.persist_internal(transaction, command, Uuid::now_v7()) + .await + } + + async fn persist_and_transform( + &self, + transaction: &mut Self::Transaction, + command: CommandEnvelope, + ) -> error_stack::Result, KernelError> { + let version = Uuid::now_v7(); + self.persist_internal(transaction, &command, version) + .await?; + + let command = command.into_destruct(); + Ok(EventEnvelope::new( + command.id, + command.event, + EventVersion::new(version), + )) + } +} + +impl PostgresAccountEventStore { + async fn persist_internal( + &self, + transaction: &mut PostgresConnection, + command: &CommandEnvelope, + version: Uuid, + ) -> error_stack::Result<(), KernelError> { + let con: &mut PgConnection = transaction; + + let event_name = command.event_name(); + let prev_version = command.prev_version().as_ref(); + if let Some(prev_version) = prev_version { + match prev_version { + KnownEventVersion::Nothing => { + let amount = sqlx::query_as::<_, CountRow>( + //language=postgresql + r#" + SELECT COUNT(*) + FROM account_events + WHERE id = $1 + "#, + ) + .bind(command.id().as_ref()) + .fetch_one(&mut *con) + .await + .convert_error()?; + if amount.count != 0 { + return Err(Report::new(KernelError::Concurrency).attach_printable( + format!("Event {} already exists", command.id().as_ref()), + )); + } + } + KnownEventVersion::Prev(prev_version) => { + let last_version = sqlx::query_as::<_, VersionRow>( + //language=postgresql + r#" + SELECT version + FROM account_events + WHERE id = $1 + ORDER BY version DESC + LIMIT 1 + "#, + ) + .bind(command.id().as_ref()) + .fetch_optional(&mut *con) + .await + .convert_error()?; + if last_version + .map(|row: VersionRow| &row.version != prev_version.as_ref()) + .unwrap_or(true) + { + return Err(Report::new(KernelError::Concurrency).attach_printable( + format!( + "Event {} version {} already exists", + command.id().as_ref(), + prev_version.as_ref() + ), + )); + } + } + }; + } + + sqlx::query( + //language=postgresql + r#" + INSERT INTO account_events (version, id, event_name, data) + VALUES ($1, $2, $3, $4) + "#, + ) + .bind(version) + .bind(command.id().as_ref()) + .bind(event_name) + .bind(serde_json::to_value(command.event()).convert_error()?) + .execute(con) + .await + .convert_error()?; + + Ok(()) + } +} + +impl DependOnAccountEventStore for PostgresDatabase { + type AccountEventStore = PostgresAccountEventStore; + + fn account_event_store(&self) -> &Self::AccountEventStore { + &PostgresAccountEventStore + } +} + +#[cfg(test)] +mod test { + mod query { + use crate::database::PostgresDatabase; + use kernel::interfaces::database::DatabaseConnection; + use kernel::interfaces::event_store::{AccountEventStore, DependOnAccountEventStore}; + use kernel::prelude::entity::{ + Account, AccountEvent, AccountId, AccountIsBot, AccountName, AccountPrivateKey, + AccountPublicKey, CommandEnvelope, EventId, KnownEventVersion, Nanoid, + }; + use uuid::Uuid; + + fn create_account_command(account_id: AccountId) -> CommandEnvelope { + let event = AccountEvent::Created { + name: AccountName::new("test"), + private_key: AccountPrivateKey::new("test"), + public_key: AccountPublicKey::new("test"), + is_bot: AccountIsBot::new(false), + nanoid: Nanoid::default(), + }; + CommandEnvelope::new( + EventId::from(account_id), + event.name(), + event, + Some(KnownEventVersion::Nothing), + ) + } + + #[test_with::env(DATABASE_URL)] + #[tokio::test] + async fn find_by_id() { + let db = PostgresDatabase::new().await.unwrap(); + let mut transaction = db.begin_transaction().await.unwrap(); + let account_id = AccountId::new(Uuid::now_v7()); + let event_id = EventId::from(account_id.clone()); + let events = db + .account_event_store() + .find_by_id(&mut transaction, &event_id, None) + .await + .unwrap(); + assert_eq!(events.len(), 0); + let created_account = create_account_command(account_id.clone()); + let updated_account = Account::update(account_id.clone(), AccountIsBot::new(true)); + let deleted_account = Account::delete(account_id.clone()); + + db.account_event_store() + .persist(&mut transaction, &created_account) + .await + .unwrap(); + db.account_event_store() + .persist(&mut transaction, &updated_account) + .await + .unwrap(); + db.account_event_store() + .persist(&mut transaction, &deleted_account) + .await + .unwrap(); + let events = db + .account_event_store() + .find_by_id(&mut transaction, &event_id, None) + .await + .unwrap(); + assert_eq!(events.len(), 3); + assert_eq!(&events[0].event, created_account.event()); + assert_eq!(&events[1].event, updated_account.event()); + assert_eq!(&events[2].event, deleted_account.event()); + } + } + + mod persist { + use crate::database::PostgresDatabase; + use kernel::interfaces::database::DatabaseConnection; + use kernel::interfaces::event_store::{AccountEventStore, DependOnAccountEventStore}; + use kernel::prelude::entity::{ + Account, AccountEvent, AccountId, AccountIsBot, AccountName, AccountPrivateKey, + AccountPublicKey, CommandEnvelope, EventId, KnownEventVersion, Nanoid, + }; + use uuid::Uuid; + + fn create_account_command(account_id: AccountId) -> CommandEnvelope { + let event = AccountEvent::Created { + name: AccountName::new("test"), + private_key: AccountPrivateKey::new("test"), + public_key: AccountPublicKey::new("test"), + is_bot: AccountIsBot::new(false), + nanoid: Nanoid::default(), + }; + CommandEnvelope::new( + EventId::from(account_id), + event.name(), + event, + Some(KnownEventVersion::Nothing), + ) + } + + #[test_with::env(DATABASE_URL)] + #[tokio::test] + async fn basic_creation() { + let db = PostgresDatabase::new().await.unwrap(); + let mut transaction = db.begin_transaction().await.unwrap(); + let account_id = AccountId::new(Uuid::now_v7()); + let created_account = create_account_command(account_id.clone()); + db.account_event_store() + .persist(&mut transaction, &created_account) + .await + .unwrap(); + let events = db + .account_event_store() + .find_by_id(&mut transaction, &EventId::from(account_id), None) + .await + .unwrap(); + assert_eq!(events.len(), 1); + } + + #[test_with::env(DATABASE_URL)] + #[tokio::test] + async fn persist_and_transform_test() { + let db = PostgresDatabase::new().await.unwrap(); + let mut transaction = db.begin_transaction().await.unwrap(); + let account_id = AccountId::new(Uuid::now_v7()); + let created_account = create_account_command(account_id.clone()); + + let event_envelope = db + .account_event_store() + .persist_and_transform(&mut transaction, created_account.clone()) + .await + .unwrap(); + + assert_eq!(event_envelope.id, EventId::from(account_id.clone())); + assert_eq!(&event_envelope.event, created_account.event()); + + let events = db + .account_event_store() + .find_by_id(&mut transaction, &EventId::from(account_id), None) + .await + .unwrap(); + assert_eq!(events.len(), 1); + assert_eq!(&events[0].event, created_account.event()); + } + } +} diff --git a/driver/src/database/postgres/follow.rs b/driver/src/database/postgres/follow.rs index aa9644b..7a54402 100644 --- a/driver/src/database/postgres/follow.rs +++ b/driver/src/database/postgres/follow.rs @@ -236,10 +236,9 @@ mod test { mod query { use crate::database::PostgresDatabase; use kernel::interfaces::database::DatabaseConnection; - use kernel::interfaces::modify::{ - AccountModifier, DependOnAccountModifier, DependOnFollowModifier, FollowModifier, - }; + use kernel::interfaces::modify::{DependOnFollowModifier, FollowModifier}; use kernel::interfaces::query::{DependOnFollowQuery, FollowQuery}; + use kernel::interfaces::read_model::{AccountReadModel, DependOnAccountReadModel}; use kernel::prelude::entity::{ Account, AccountId, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, CreatedAt, EventVersion, Follow, FollowApprovedAt, FollowId, FollowTargetId, Nanoid, @@ -264,7 +263,7 @@ mod test { CreatedAt::now(), ); database - .account_modifier() + .account_read_model() .create(&mut transaction, &follower_account) .await .unwrap(); @@ -281,7 +280,7 @@ mod test { CreatedAt::now(), ); database - .account_modifier() + .account_read_model() .create(&mut transaction, &followee_account) .await .unwrap(); @@ -318,12 +317,12 @@ mod test { .await .unwrap(); database - .account_modifier() + .account_read_model() .delete(&mut transaction, follower_account.id()) .await .unwrap(); database - .account_modifier() + .account_read_model() .delete(&mut transaction, followee_account.id()) .await .unwrap(); @@ -347,7 +346,7 @@ mod test { CreatedAt::now(), ); database - .account_modifier() + .account_read_model() .create(&mut transaction, &follower_account) .await .unwrap(); @@ -364,7 +363,7 @@ mod test { CreatedAt::now(), ); database - .account_modifier() + .account_read_model() .create(&mut transaction, &followee_account) .await .unwrap(); @@ -401,12 +400,12 @@ mod test { .await .unwrap(); database - .account_modifier() + .account_read_model() .delete(&mut transaction, follower_account.id()) .await .unwrap(); database - .account_modifier() + .account_read_model() .delete(&mut transaction, followee_account.id()) .await .unwrap(); @@ -416,10 +415,9 @@ mod test { mod modify { use crate::database::PostgresDatabase; use kernel::interfaces::database::DatabaseConnection; - use kernel::interfaces::modify::{ - AccountModifier, DependOnAccountModifier, DependOnFollowModifier, FollowModifier, - }; + use kernel::interfaces::modify::{DependOnFollowModifier, FollowModifier}; use kernel::interfaces::query::{DependOnFollowQuery, FollowQuery}; + use kernel::interfaces::read_model::{AccountReadModel, DependOnAccountReadModel}; use kernel::prelude::entity::{ Account, AccountId, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, CreatedAt, EventVersion, Follow, FollowApprovedAt, FollowId, FollowTargetId, Nanoid, @@ -444,7 +442,7 @@ mod test { CreatedAt::now(), ); database - .account_modifier() + .account_read_model() .create(&mut transaction, &follower_account) .await .unwrap(); @@ -461,7 +459,7 @@ mod test { CreatedAt::now(), ); database - .account_modifier() + .account_read_model() .create(&mut transaction, &followee_account) .await .unwrap(); @@ -484,12 +482,12 @@ mod test { .await .unwrap(); database - .account_modifier() + .account_read_model() .delete(&mut transaction, follower_account.id()) .await .unwrap(); database - .account_modifier() + .account_read_model() .delete(&mut transaction, followee_account.id()) .await .unwrap(); @@ -514,7 +512,7 @@ mod test { CreatedAt::now(), ); database - .account_modifier() + .account_read_model() .create(&mut transaction, &follower_account) .await .unwrap(); @@ -531,7 +529,7 @@ mod test { CreatedAt::now(), ); database - .account_modifier() + .account_read_model() .create(&mut transaction, &followee_account) .await .unwrap(); @@ -581,12 +579,12 @@ mod test { .await .unwrap(); database - .account_modifier() + .account_read_model() .delete(&mut transaction, follower_account.id()) .await .unwrap(); database - .account_modifier() + .account_read_model() .delete(&mut transaction, followee_account.id()) .await .unwrap(); @@ -610,7 +608,7 @@ mod test { CreatedAt::now(), ); database - .account_modifier() + .account_read_model() .create(&mut transaction, &follower_account) .await .unwrap(); @@ -627,7 +625,7 @@ mod test { CreatedAt::now(), ); database - .account_modifier() + .account_read_model() .create(&mut transaction, &followee_account) .await .unwrap(); @@ -658,12 +656,12 @@ mod test { .unwrap(); assert!(following.is_empty()); database - .account_modifier() + .account_read_model() .delete(&mut transaction, follower_account.id()) .await .unwrap(); database - .account_modifier() + .account_read_model() .delete(&mut transaction, followee_account.id()) .await .unwrap(); diff --git a/driver/src/database/postgres/metadata.rs b/driver/src/database/postgres/metadata.rs index ab4561d..1bdf934 100644 --- a/driver/src/database/postgres/metadata.rs +++ b/driver/src/database/postgres/metadata.rs @@ -175,10 +175,9 @@ mod test { mod query { use crate::database::PostgresDatabase; use kernel::interfaces::database::DatabaseConnection; - use kernel::interfaces::modify::{ - AccountModifier, DependOnAccountModifier, DependOnMetadataModifier, MetadataModifier, - }; + use kernel::interfaces::modify::{DependOnMetadataModifier, MetadataModifier}; use kernel::interfaces::query::{DependOnMetadataQuery, MetadataQuery}; + use kernel::interfaces::read_model::{AccountReadModel, DependOnAccountReadModel}; use kernel::prelude::entity::{ Account, AccountId, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, CreatedAt, EventVersion, Metadata, MetadataContent, MetadataId, MetadataLabel, Nanoid, @@ -206,7 +205,7 @@ mod test { ); database - .account_modifier() + .account_read_model() .create(&mut transaction, &account) .await .unwrap(); @@ -253,7 +252,7 @@ mod test { ); database - .account_modifier() + .account_read_model() .create(&mut transaction, &account) .await .unwrap(); @@ -299,10 +298,9 @@ mod test { mod modify { use crate::database::PostgresDatabase; use kernel::interfaces::database::DatabaseConnection; - use kernel::interfaces::modify::{ - AccountModifier, DependOnAccountModifier, DependOnMetadataModifier, MetadataModifier, - }; + use kernel::interfaces::modify::{DependOnMetadataModifier, MetadataModifier}; use kernel::interfaces::query::{DependOnMetadataQuery, MetadataQuery}; + use kernel::interfaces::read_model::{AccountReadModel, DependOnAccountReadModel}; use kernel::prelude::entity::{ Account, AccountId, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, CreatedAt, EventVersion, Metadata, MetadataContent, MetadataId, MetadataLabel, Nanoid, @@ -329,7 +327,7 @@ mod test { ); database - .account_modifier() + .account_read_model() .create(&mut transaction, &account) .await .unwrap(); @@ -376,7 +374,7 @@ mod test { ); database - .account_modifier() + .account_read_model() .create(&mut transaction, &account) .await .unwrap(); @@ -441,7 +439,7 @@ mod test { ); database - .account_modifier() + .account_read_model() .create(&mut transaction, &account) .await .unwrap(); diff --git a/driver/src/database/postgres/profile.rs b/driver/src/database/postgres/profile.rs index 39a0f72..58b5c7a 100644 --- a/driver/src/database/postgres/profile.rs +++ b/driver/src/database/postgres/profile.rs @@ -148,10 +148,9 @@ mod test { use uuid::Uuid; use kernel::interfaces::database::DatabaseConnection; - use kernel::interfaces::modify::{ - AccountModifier, DependOnAccountModifier, DependOnProfileModifier, ProfileModifier, - }; + use kernel::interfaces::modify::{DependOnProfileModifier, ProfileModifier}; use kernel::interfaces::query::{DependOnProfileQuery, ProfileQuery}; + use kernel::interfaces::read_model::{AccountReadModel, DependOnAccountReadModel}; use kernel::prelude::entity::{ Account, AccountId, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, CreatedAt, EventVersion, Nanoid, Profile, ProfileDisplayName, ProfileId, @@ -190,7 +189,7 @@ mod test { Nanoid::default(), ); database - .account_modifier() + .account_read_model() .create(&mut transaction, &account) .await .unwrap(); @@ -207,7 +206,7 @@ mod test { .unwrap(); assert_eq!(result.as_ref().map(Profile::id), Some(profile.id())); database - .account_modifier() + .account_read_model() .delete(&mut transaction, account.id()) .await .unwrap(); @@ -218,10 +217,9 @@ mod test { use uuid::Uuid; use kernel::interfaces::database::DatabaseConnection; - use kernel::interfaces::modify::{ - AccountModifier, DependOnAccountModifier, DependOnProfileModifier, ProfileModifier, - }; + use kernel::interfaces::modify::{DependOnProfileModifier, ProfileModifier}; use kernel::interfaces::query::{DependOnProfileQuery, ProfileQuery}; + use kernel::interfaces::read_model::{AccountReadModel, DependOnAccountReadModel}; use kernel::prelude::entity::{ Account, AccountId, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, CreatedAt, EventVersion, Nanoid, Profile, ProfileDisplayName, ProfileId, @@ -260,7 +258,7 @@ mod test { Nanoid::default(), ); database - .account_modifier() + .account_read_model() .create(&mut transaction, &account) .await .unwrap(); @@ -270,7 +268,7 @@ mod test { .await .unwrap(); database - .account_modifier() + .account_read_model() .delete(&mut transaction, account.id()) .await .unwrap(); @@ -306,7 +304,7 @@ mod test { Nanoid::default(), ); database - .account_modifier() + .account_read_model() .create(&mut transaction, &account) .await .unwrap(); @@ -339,7 +337,7 @@ mod test { .unwrap(); assert_eq!(result.as_ref().map(Profile::id), Some(updated_profile.id())); database - .account_modifier() + .account_read_model() .delete(&mut transaction, account.id()) .await .unwrap(); diff --git a/kernel/src/event_store.rs b/kernel/src/event_store.rs new file mode 100644 index 0000000..a727161 --- /dev/null +++ b/kernel/src/event_store.rs @@ -0,0 +1,3 @@ +mod account; + +pub use self::account::*; diff --git a/kernel/src/event_store/account.rs b/kernel/src/event_store/account.rs new file mode 100644 index 0000000..61c097d --- /dev/null +++ b/kernel/src/event_store/account.rs @@ -0,0 +1,38 @@ +use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Transaction}; +use crate::entity::{Account, AccountEvent, CommandEnvelope, EventEnvelope, EventId, EventVersion}; +use crate::KernelError; +use std::future::Future; + +pub trait AccountEventStore: Sync + Send + 'static { + type Transaction: Transaction; + + fn persist( + &self, + transaction: &mut Self::Transaction, + command: &CommandEnvelope, + ) -> impl Future> + Send; + + fn persist_and_transform( + &self, + transaction: &mut Self::Transaction, + command: CommandEnvelope, + ) -> impl Future, KernelError>> + + Send; + + fn find_by_id( + &self, + transaction: &mut Self::Transaction, + id: &EventId, + since: Option<&EventVersion>, + ) -> impl Future< + Output = error_stack::Result>, KernelError>, + > + Send; +} + +pub trait DependOnAccountEventStore: Sync + Send + DependOnDatabaseConnection { + type AccountEventStore: AccountEventStore< + Transaction = ::Transaction, + >; + + fn account_event_store(&self) -> &Self::AccountEventStore; +} diff --git a/kernel/src/lib.rs b/kernel/src/lib.rs index 236f5ae..4139c15 100644 --- a/kernel/src/lib.rs +++ b/kernel/src/lib.rs @@ -3,8 +3,10 @@ mod database; mod entity; mod error; mod event; +mod event_store; mod modify; mod query; +mod read_model; mod signal; pub use self::error::*; @@ -33,7 +35,12 @@ pub mod interfaces { pub mod event { pub use crate::event::*; } - + pub mod event_store { + pub use crate::event_store::*; + } + pub mod read_model { + pub use crate::read_model::*; + } pub mod signal { pub use crate::signal::*; } @@ -43,8 +50,9 @@ pub mod interfaces { /// /// This macro generates implementations for: /// - DependOnDatabaseConnection -/// - DependOnAccountQuery, DependOnAuthAccountQuery, DependOnAuthHostQuery, DependOnEventQuery -/// - DependOnAccountModifier, DependOnAuthAccountModifier, DependOnAuthHostModifier, DependOnEventModifier +/// - DependOnAccountReadModel, DependOnAccountEventStore +/// - DependOnAuthAccountQuery, DependOnAuthHostQuery, DependOnEventQuery +/// - DependOnAuthAccountModifier, DependOnAuthHostModifier, DependOnEventModifier /// /// # Usage /// ```ignore @@ -65,10 +73,17 @@ macro_rules! impl_database_delegation { } } - impl $crate::interfaces::query::DependOnAccountQuery for $impl_type { - type AccountQuery = <$db_type as $crate::interfaces::query::DependOnAccountQuery>::AccountQuery; - fn account_query(&self) -> &Self::AccountQuery { - $crate::interfaces::query::DependOnAccountQuery::account_query(&self.$field) + impl $crate::interfaces::read_model::DependOnAccountReadModel for $impl_type { + type AccountReadModel = <$db_type as $crate::interfaces::read_model::DependOnAccountReadModel>::AccountReadModel; + fn account_read_model(&self) -> &Self::AccountReadModel { + $crate::interfaces::read_model::DependOnAccountReadModel::account_read_model(&self.$field) + } + } + + impl $crate::interfaces::event_store::DependOnAccountEventStore for $impl_type { + type AccountEventStore = <$db_type as $crate::interfaces::event_store::DependOnAccountEventStore>::AccountEventStore; + fn account_event_store(&self) -> &Self::AccountEventStore { + $crate::interfaces::event_store::DependOnAccountEventStore::account_event_store(&self.$field) } } @@ -93,13 +108,6 @@ macro_rules! impl_database_delegation { } } - impl $crate::interfaces::modify::DependOnAccountModifier for $impl_type { - type AccountModifier = <$db_type as $crate::interfaces::modify::DependOnAccountModifier>::AccountModifier; - fn account_modifier(&self) -> &Self::AccountModifier { - $crate::interfaces::modify::DependOnAccountModifier::account_modifier(&self.$field) - } - } - impl $crate::interfaces::modify::DependOnAuthAccountModifier for $impl_type { type AuthAccountModifier = <$db_type as $crate::interfaces::modify::DependOnAuthAccountModifier>::AuthAccountModifier; fn auth_account_modifier(&self) -> &Self::AuthAccountModifier { diff --git a/kernel/src/modify.rs b/kernel/src/modify.rs index a79e3a1..dcc8a0d 100644 --- a/kernel/src/modify.rs +++ b/kernel/src/modify.rs @@ -1,4 +1,3 @@ -mod account; mod auth_account; mod auth_host; mod event; @@ -9,6 +8,6 @@ mod profile; mod remote_account; pub use self::{ - account::*, auth_account::*, auth_host::*, event::*, follow::*, image::*, metadata::*, - profile::*, remote_account::*, + auth_account::*, auth_host::*, event::*, follow::*, image::*, metadata::*, profile::*, + remote_account::*, }; diff --git a/kernel/src/modify/account.rs b/kernel/src/modify/account.rs deleted file mode 100644 index 4483d53..0000000 --- a/kernel/src/modify/account.rs +++ /dev/null @@ -1,41 +0,0 @@ -use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Transaction}; -use crate::entity::{Account, AccountId, AuthAccountId}; -use crate::KernelError; -use std::future::Future; - -pub trait AccountModifier: Sync + Send + 'static { - type Transaction: Transaction; - - fn create( - &self, - transaction: &mut Self::Transaction, - account: &Account, - ) -> impl Future> + Send; - - fn update( - &self, - transaction: &mut Self::Transaction, - account: &Account, - ) -> impl Future> + Send; - - fn delete( - &self, - transaction: &mut Self::Transaction, - account_id: &AccountId, - ) -> impl Future> + Send; - - fn link_auth_account( - &self, - transaction: &mut Self::Transaction, - account_id: &AccountId, - auth_account_id: &AuthAccountId, - ) -> impl Future> + Send; -} - -pub trait DependOnAccountModifier: Sync + Send + DependOnDatabaseConnection { - type AccountModifier: AccountModifier< - Transaction = ::Transaction, - >; - - fn account_modifier(&self) -> &Self::AccountModifier; -} diff --git a/kernel/src/query.rs b/kernel/src/query.rs index a79e3a1..dcc8a0d 100644 --- a/kernel/src/query.rs +++ b/kernel/src/query.rs @@ -1,4 +1,3 @@ -mod account; mod auth_account; mod auth_host; mod event; @@ -9,6 +8,6 @@ mod profile; mod remote_account; pub use self::{ - account::*, auth_account::*, auth_host::*, event::*, follow::*, image::*, metadata::*, - profile::*, remote_account::*, + auth_account::*, auth_host::*, event::*, follow::*, image::*, metadata::*, profile::*, + remote_account::*, }; diff --git a/kernel/src/read_model.rs b/kernel/src/read_model.rs new file mode 100644 index 0000000..a727161 --- /dev/null +++ b/kernel/src/read_model.rs @@ -0,0 +1,3 @@ +mod account; + +pub use self::account::*; diff --git a/kernel/src/query/account.rs b/kernel/src/read_model/account.rs similarity index 50% rename from kernel/src/query/account.rs rename to kernel/src/read_model/account.rs index bd5decf..3c8bf09 100644 --- a/kernel/src/query/account.rs +++ b/kernel/src/read_model/account.rs @@ -3,9 +3,10 @@ use crate::entity::{Account, AccountId, AccountName, AuthAccountId, Nanoid}; use crate::KernelError; use std::future::Future; -pub trait AccountQuery: Sync + Send + 'static { +pub trait AccountReadModel: Sync + Send + 'static { type Transaction: Transaction; + // Query operations (projection reads) fn find_by_id( &self, transaction: &mut Self::Transaction, @@ -29,12 +30,38 @@ pub trait AccountQuery: Sync + Send + 'static { transaction: &mut Self::Transaction, nanoid: &Nanoid, ) -> impl Future, KernelError>> + Send; + + // Projection update operations (called by EventApplier pipeline) + fn create( + &self, + transaction: &mut Self::Transaction, + account: &Account, + ) -> impl Future> + Send; + + fn update( + &self, + transaction: &mut Self::Transaction, + account: &Account, + ) -> impl Future> + Send; + + fn delete( + &self, + transaction: &mut Self::Transaction, + account_id: &AccountId, + ) -> impl Future> + Send; + + fn link_auth_account( + &self, + transaction: &mut Self::Transaction, + account_id: &AccountId, + auth_account_id: &AuthAccountId, + ) -> impl Future> + Send; } -pub trait DependOnAccountQuery: Sync + Send + DependOnDatabaseConnection { - type AccountQuery: AccountQuery< +pub trait DependOnAccountReadModel: Sync + Send + DependOnDatabaseConnection { + type AccountReadModel: AccountReadModel< Transaction = ::Transaction, >; - fn account_query(&self) -> &Self::AccountQuery; + fn account_read_model(&self) -> &Self::AccountReadModel; } diff --git a/migrations/20260308000000_account_events.sql b/migrations/20260308000000_account_events.sql new file mode 100644 index 0000000..323a65d --- /dev/null +++ b/migrations/20260308000000_account_events.sql @@ -0,0 +1,7 @@ +CREATE TABLE "account_events" ( + "version" UUID NOT NULL, + "id" UUID NOT NULL, + "event_name" TEXT NOT NULL, + "data" JSON NOT NULL, + PRIMARY KEY ("id", "version") +); diff --git a/server/src/applier/account_applier.rs b/server/src/applier/account_applier.rs index 87cff5c..a4af345 100644 --- a/server/src/applier/account_applier.rs +++ b/server/src/applier/account_applier.rs @@ -24,7 +24,7 @@ impl AccountApplier { Uuid::new_v4, |handler: Arc, id: AccountId| async move { handler - .pgpool() + .as_ref() .update_account(id) .await .map_err(|e| ErrorOperation::Delay(format!("{:?}", e))) From 93d17db51b2fbc11116ed013accc6919a19954ca Mon Sep 17 00:00:00 2001 From: turtton Date: Mon, 9 Mar 2026 11:15:48 +0900 Subject: [PATCH 45/59] :white_check_mark: Fix event store tests and add optimistic concurrency control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Account::update/deleteにcurrent_versionパラメータを追加し楽観的排他制御を実装 - Deletedイベントでentityをhard delete (None) に変更 - AccountEventStoreのfind_by_idのSQLバインド順序を修正 - event.rsのfind_by_id_with_versionテストのロジックバグを修正しshouldpanicを除去 - マイグレーションのdata列をJSONBに変更、occurred_at列を追加 - テストでドメインロジック(Account::update/delete)と永続化テストを分離 --- application/src/service/account.rs | 12 +- .../database/postgres/account_event_store.rs | 110 +++++++++++++++--- driver/src/database/postgres/event.rs | 63 ++++++++-- kernel/src/entity/account.rs | 50 +++++--- migrations/20260308000000_account_events.sql | 3 +- 5 files changed, 194 insertions(+), 44 deletions(-) diff --git a/application/src/service/account.rs b/application/src/service/account.rs index 42272ca..efa6d38 100644 --- a/application/src/service/account.rs +++ b/application/src/service/account.rs @@ -223,7 +223,7 @@ impl CreateAccountService for T where } pub trait UpdateAccountService: - 'static + DependOnAccountReadModel + DependOnAccountEventStore + 'static + Sync + Send + DependOnAccountReadModel + DependOnAccountEventStore { fn update_account( &self, @@ -322,7 +322,8 @@ pub trait DeleteAccountService: } let account_id = account.id().clone(); - let delete_command = Account::delete(account_id.clone()); + let current_version = account.version().clone(); + let delete_command = Account::delete(account_id.clone(), current_version); self.account_event_store() .persist_and_transform(&mut transaction, delete_command) @@ -400,7 +401,12 @@ pub trait EditAccountService: } let account_id = account.id().clone(); - let update_command = Account::update(account_id.clone(), AccountIsBot::new(is_bot)); + let current_version = account.version().clone(); + let update_command = Account::update( + account_id.clone(), + AccountIsBot::new(is_bot), + current_version, + ); self.account_event_store() .persist_and_transform(&mut transaction, update_command) diff --git a/driver/src/database/postgres/account_event_store.rs b/driver/src/database/postgres/account_event_store.rs index 1fd2f24..f268bc3 100644 --- a/driver/src/database/postgres/account_event_store.rs +++ b/driver/src/database/postgres/account_event_store.rs @@ -44,17 +44,21 @@ impl AccountEventStore for PostgresAccountEventStore { since: Option<&EventVersion>, ) -> error_stack::Result>, KernelError> { let con: &mut PgConnection = transaction; - if let Some(version) = since { + let rows = if let Some(version) = since { sqlx::query_as::<_, EventRow>( //language=postgresql r#" SELECT version, id, event_name, data FROM account_events - WHERE id = $2 AND version > $1 + WHERE id = $1 AND version > $2 ORDER BY version "#, ) + .bind(id.as_ref()) .bind(version.as_ref()) + .fetch_all(con) + .await + .convert_error()? } else { sqlx::query_as::<_, EventRow>( //language=postgresql @@ -65,16 +69,14 @@ impl AccountEventStore for PostgresAccountEventStore { ORDER BY version "#, ) - } - .bind(id.as_ref()) - .fetch_all(con) - .await - .convert_error() - .and_then(|rows| { - rows.into_iter() - .map(|row| row.try_into()) - .collect::, KernelError>>() - }) + .bind(id.as_ref()) + .fetch_all(con) + .await + .convert_error()? + }; + rows.into_iter() + .map(|row| row.try_into()) + .collect::, KernelError>>() } async fn persist( @@ -236,8 +238,22 @@ mod test { .unwrap(); assert_eq!(events.len(), 0); let created_account = create_account_command(account_id.clone()); - let updated_account = Account::update(account_id.clone(), AccountIsBot::new(true)); - let deleted_account = Account::delete(account_id.clone()); + let update_event = AccountEvent::Updated { + is_bot: AccountIsBot::new(true), + }; + let updated_account = CommandEnvelope::new( + EventId::from(account_id.clone()), + update_event.name(), + update_event, + None, + ); + let delete_event = AccountEvent::Deleted; + let deleted_account = CommandEnvelope::new( + EventId::from(account_id.clone()), + delete_event.name(), + delete_event, + None, + ); db.account_event_store() .persist(&mut transaction, &created_account) @@ -261,6 +277,72 @@ mod test { assert_eq!(&events[1].event, updated_account.event()); assert_eq!(&events[2].event, deleted_account.event()); } + + #[test_with::env(DATABASE_URL)] + #[tokio::test] + async fn find_by_id_since_version() { + let db = PostgresDatabase::new().await.unwrap(); + let mut transaction = db.begin_transaction().await.unwrap(); + let account_id = AccountId::new(Uuid::now_v7()); + let event_id = EventId::from(account_id.clone()); + + let created_account = create_account_command(account_id.clone()); + let update_event = AccountEvent::Updated { + is_bot: AccountIsBot::new(true), + }; + let updated_account = CommandEnvelope::new( + EventId::from(account_id.clone()), + update_event.name(), + update_event, + None, + ); + let delete_event = AccountEvent::Deleted; + let deleted_account = CommandEnvelope::new( + EventId::from(account_id.clone()), + delete_event.name(), + delete_event, + None, + ); + + db.account_event_store() + .persist(&mut transaction, &created_account) + .await + .unwrap(); + db.account_event_store() + .persist(&mut transaction, &updated_account) + .await + .unwrap(); + db.account_event_store() + .persist(&mut transaction, &deleted_account) + .await + .unwrap(); + + // Get all events to obtain the first version + let all_events = db + .account_event_store() + .find_by_id(&mut transaction, &event_id, None) + .await + .unwrap(); + assert_eq!(all_events.len(), 3); + + // Query since the first event's version — should return the 2nd and 3rd events + let since_events = db + .account_event_store() + .find_by_id(&mut transaction, &event_id, Some(&all_events[0].version)) + .await + .unwrap(); + assert_eq!(since_events.len(), 2); + assert_eq!(&since_events[0].event, updated_account.event()); + assert_eq!(&since_events[1].event, deleted_account.event()); + + // Query since the last event's version — should return no events + let no_events = db + .account_event_store() + .find_by_id(&mut transaction, &event_id, Some(&all_events[2].version)) + .await + .unwrap(); + assert_eq!(no_events.len(), 0); + } } mod persist { diff --git a/driver/src/database/postgres/event.rs b/driver/src/database/postgres/event.rs index a181ec3..4c4176d 100644 --- a/driver/src/database/postgres/event.rs +++ b/driver/src/database/postgres/event.rs @@ -255,8 +255,22 @@ mod test { .unwrap(); assert_eq!(events.len(), 0); let created_account = create_account_command(account_id.clone()); - let updated_account = Account::update(account_id.clone(), AccountIsBot::new(true)); - let deleted_account = Account::delete(account_id.clone()); + let update_event = AccountEvent::Updated { + is_bot: AccountIsBot::new(true), + }; + let updated_account = CommandEnvelope::new( + EventId::from(account_id.clone()), + update_event.name(), + update_event, + None, + ); + let delete_event = AccountEvent::Deleted; + let deleted_account = CommandEnvelope::new( + EventId::from(account_id.clone()), + delete_event.name(), + delete_event, + None, + ); db.event_modifier() .persist(&mut transaction, &created_account) @@ -283,14 +297,28 @@ mod test { #[test_with::env(DATABASE_URL)] #[tokio::test] - #[should_panic] - async fn find_by_id_with_version() { + async fn find_by_id_since_version() { let db = PostgresDatabase::new().await.unwrap(); let mut transaction = db.begin_transaction().await.unwrap(); let account_id = AccountId::new(Uuid::now_v7()); let event_id = EventId::from(account_id.clone()); let created_account = create_account_command(account_id.clone()); - let updated_account = Account::update(account_id.clone(), AccountIsBot::new(true)); + let update_event = AccountEvent::Updated { + is_bot: AccountIsBot::new(true), + }; + let updated_account = CommandEnvelope::new( + EventId::from(account_id.clone()), + update_event.name(), + update_event, + None, + ); + let delete_event = AccountEvent::Deleted; + let deleted_account = CommandEnvelope::new( + EventId::from(account_id.clone()), + delete_event.name(), + delete_event, + None, + ); db.event_modifier() .persist(&mut transaction, &created_account) .await @@ -299,20 +327,35 @@ mod test { .persist(&mut transaction, &updated_account) .await .unwrap(); + db.event_modifier() + .persist(&mut transaction, &deleted_account) + .await + .unwrap(); let all_events = db .event_query() .find_by_id(&mut transaction, &event_id, None) .await .unwrap(); - let events = db + assert_eq!(all_events.len(), 3); + + // Query since the first event's version — should return the 2nd and 3rd events + let since_events = db .event_query() - .find_by_id(&mut transaction, &event_id, Some(&all_events[1].version)) + .find_by_id(&mut transaction, &event_id, Some(&all_events[0].version)) .await .unwrap(); - assert_eq!(events.len(), 1); - let event = &events[0]; - assert_eq!(&event.event, updated_account.event()); + assert_eq!(since_events.len(), 2); + assert_eq!(&since_events[0].event, updated_account.event()); + assert_eq!(&since_events[1].event, deleted_account.event()); + + // Query since the last event's version — should return no events + let no_events = db + .event_query() + .find_by_id(&mut transaction, &event_id, Some(&all_events[2].version)) + .await + .unwrap(); + assert_eq!(no_events.len(), 0); } } diff --git a/kernel/src/entity/account.rs b/kernel/src/entity/account.rs index 1d3503f..1ffcf0a 100644 --- a/kernel/src/entity/account.rs +++ b/kernel/src/entity/account.rs @@ -5,7 +5,8 @@ use serde::Serialize; use vodca::{Nameln, Newln, References}; use crate::entity::{ - CommandEnvelope, CreatedAt, DeletedAt, EventEnvelope, EventId, EventVersion, Nanoid, + CommandEnvelope, CreatedAt, DeletedAt, EventEnvelope, EventId, EventVersion, KnownEventVersion, + Nanoid, }; use crate::event::EventApplier; use crate::KernelError; @@ -55,14 +56,31 @@ pub enum AccountEvent { } impl Account { - pub fn update(id: AccountId, is_bot: AccountIsBot) -> CommandEnvelope { + pub fn update( + id: AccountId, + is_bot: AccountIsBot, + current_version: EventVersion, + ) -> CommandEnvelope { let event = AccountEvent::Updated { is_bot }; - CommandEnvelope::new(EventId::from(id), event.name(), event, None) + CommandEnvelope::new( + EventId::from(id), + event.name(), + event, + Some(KnownEventVersion::Prev(current_version)), + ) } - pub fn delete(id: AccountId) -> CommandEnvelope { + pub fn delete( + id: AccountId, + current_version: EventVersion, + ) -> CommandEnvelope { let event = AccountEvent::Deleted; - CommandEnvelope::new(EventId::from(id), event.name(), event, None) + CommandEnvelope::new( + EventId::from(id), + event.name(), + event, + Some(KnownEventVersion::Prev(current_version)), + ) } } @@ -125,9 +143,8 @@ impl EventApplier for Account { } } AccountEvent::Deleted => { - if let Some(entity) = entity { - entity.deleted_at = Some(DeletedAt::now()); - entity.version = event.version; + if entity.is_some() { + *entity = None; } else { return Err(Report::new(KernelError::Internal) .attach_printable(Self::not_exists(event.id.as_ref()))); @@ -235,7 +252,8 @@ mod test { nano_id.clone(), CreatedAt::now(), ); - let event = Account::update(id.clone(), AccountIsBot::new(true)); + let version = account.version().clone(); + let event = Account::update(id.clone(), AccountIsBot::new(true), version); let envelope = EventEnvelope::new( event.id().clone(), event.event().clone(), @@ -252,7 +270,8 @@ mod test { #[test] fn update_not_exist_account() { let id = AccountId::new(Uuid::now_v7()); - let event = Account::update(id.clone(), AccountIsBot::new(true)); + let version = EventVersion::new(Uuid::now_v7()); + let event = Account::update(id.clone(), AccountIsBot::new(true), version); let envelope = EventEnvelope::new( event.id().clone(), event.event().clone(), @@ -282,7 +301,8 @@ mod test { nano_id.clone(), CreatedAt::now(), ); - let event = Account::delete(id.clone()); + let version = account.version().clone(); + let event = Account::delete(id.clone(), version); let envelope = EventEnvelope::new( event.id().clone(), event.event().clone(), @@ -290,16 +310,14 @@ mod test { ); let mut account = Some(account); Account::apply(&mut account, envelope.clone()).unwrap(); - let account = account.unwrap(); - assert!(account.deleted_at().is_some()); - assert_eq!(account.version(), &envelope.version); - assert_eq!(account.nanoid(), &nano_id); + assert!(account.is_none()); } #[test] fn delete_not_exist_account() { let id = AccountId::new(Uuid::now_v7()); - let event = Account::delete(id.clone()); + let version = EventVersion::new(Uuid::now_v7()); + let event = Account::delete(id.clone(), version); let envelope = EventEnvelope::new( event.id().clone(), event.event().clone(), diff --git a/migrations/20260308000000_account_events.sql b/migrations/20260308000000_account_events.sql index 323a65d..5d2245d 100644 --- a/migrations/20260308000000_account_events.sql +++ b/migrations/20260308000000_account_events.sql @@ -2,6 +2,7 @@ CREATE TABLE "account_events" ( "version" UUID NOT NULL, "id" UUID NOT NULL, "event_name" TEXT NOT NULL, - "data" JSON NOT NULL, + "data" JSONB NOT NULL, + "occurred_at" TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY ("id", "version") ); From e80e8780ec66ce289aa4b49e5c0ce7a344d712fd Mon Sep 17 00:00:00 2001 From: turtton Date: Mon, 9 Mar 2026 11:38:26 +0900 Subject: [PATCH 46/59] :recycle: Rename Transaction to Executor in trait definitions MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Query/Modify/EventStore/ReadModel の associated type `Transaction` を `Executor` にリネーム。引数名も `transaction` → `executor` に統一。 実態はコネクションラッパーであり、トランザクションではないため名前を修正。 --- driver/src/database/postgres.rs | 8 ++--- driver/src/database/postgres/account.rs | 34 +++++++++---------- .../database/postgres/account_event_store.rs | 19 +++++------ driver/src/database/postgres/auth_account.rs | 24 ++++++------- driver/src/database/postgres/auth_host.rs | 20 +++++------ driver/src/database/postgres/event.rs | 21 ++++++------ driver/src/database/postgres/follow.rs | 24 ++++++------- driver/src/database/postgres/image.rs | 20 +++++------ driver/src/database/postgres/metadata.rs | 24 ++++++------- driver/src/database/postgres/profile.rs | 16 ++++----- .../src/database/postgres/remote_account.rs | 28 +++++++-------- driver/src/database/redis.rs | 8 ++--- kernel/src/database.rs | 6 ++-- kernel/src/event_store/account.rs | 12 +++---- kernel/src/modify/auth_account.rs | 12 +++---- kernel/src/modify/auth_host.rs | 10 +++--- kernel/src/modify/event.rs | 10 +++--- kernel/src/modify/follow.rs | 12 +++---- kernel/src/modify/image.rs | 10 +++--- kernel/src/modify/metadata.rs | 12 +++---- kernel/src/modify/profile.rs | 10 +++--- kernel/src/modify/remote_account.rs | 12 +++---- kernel/src/query/auth_account.rs | 10 +++--- kernel/src/query/auth_host.rs | 10 +++--- kernel/src/query/event.rs | 8 ++--- kernel/src/query/follow.rs | 10 +++--- kernel/src/query/image.rs | 10 +++--- kernel/src/query/metadata.rs | 10 +++--- kernel/src/query/profile.rs | 8 ++--- kernel/src/query/remote_account.rs | 12 +++---- kernel/src/read_model/account.rs | 22 ++++++------ 31 files changed, 225 insertions(+), 227 deletions(-) diff --git a/driver/src/database/postgres.rs b/driver/src/database/postgres.rs index cf38363..d7dda70 100644 --- a/driver/src/database/postgres.rs +++ b/driver/src/database/postgres.rs @@ -12,7 +12,7 @@ mod remote_account; use crate::database::env; use crate::ConvertError; use error_stack::{Report, ResultExt}; -use kernel::interfaces::database::{DatabaseConnection, Transaction}; +use kernel::interfaces::database::{DatabaseConnection, Executor}; use kernel::KernelError; use sqlx::pool::PoolConnection; use sqlx::{Error, PgConnection, Pool, Postgres}; @@ -68,7 +68,7 @@ pub(in crate::database::postgres) struct CountRow { pub struct PostgresConnection(PoolConnection); -impl Transaction for PostgresConnection {} +impl Executor for PostgresConnection {} impl Deref for PostgresConnection { type Target = PgConnection; @@ -84,8 +84,8 @@ impl DerefMut for PostgresConnection { } impl DatabaseConnection for PostgresDatabase { - type Transaction = PostgresConnection; - async fn begin_transaction(&self) -> error_stack::Result { + type Executor = PostgresConnection; + async fn begin_transaction(&self) -> error_stack::Result { let connection = self.pool.acquire().await.convert_error()?; Ok(PostgresConnection(connection)) } diff --git a/driver/src/database/postgres/account.rs b/driver/src/database/postgres/account.rs index 20cc06f..fd96611 100644 --- a/driver/src/database/postgres/account.rs +++ b/driver/src/database/postgres/account.rs @@ -42,14 +42,14 @@ impl From for Account { pub struct PostgresAccountReadModel; impl AccountReadModel for PostgresAccountReadModel { - type Transaction = PostgresConnection; + type Executor = PostgresConnection; async fn find_by_id( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, id: &AccountId, ) -> error_stack::Result, KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; sqlx::query_as::<_, AccountRow>( //language=postgresql r#" @@ -67,10 +67,10 @@ impl AccountReadModel for PostgresAccountReadModel { async fn find_by_auth_id( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, auth_id: &AuthAccountId, ) -> error_stack::Result, KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; sqlx::query_as::<_, AccountRow>( //language=postgresql r#" @@ -89,10 +89,10 @@ impl AccountReadModel for PostgresAccountReadModel { async fn find_by_name( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, name: &AccountName, ) -> error_stack::Result, KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; sqlx::query_as::<_, AccountRow>( //language=postgresql r#" @@ -110,10 +110,10 @@ impl AccountReadModel for PostgresAccountReadModel { async fn find_by_nanoid( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, nanoid: &Nanoid, ) -> error_stack::Result, KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; sqlx::query_as::<_, AccountRow>( //language=postgresql r#" @@ -131,10 +131,10 @@ impl AccountReadModel for PostgresAccountReadModel { async fn create( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, account: &Account, ) -> error_stack::Result<(), KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; sqlx::query( //language=postgresql r#" @@ -158,10 +158,10 @@ impl AccountReadModel for PostgresAccountReadModel { async fn update( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, account: &Account, ) -> error_stack::Result<(), KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; sqlx::query( //language=postgresql r#" @@ -184,10 +184,10 @@ impl AccountReadModel for PostgresAccountReadModel { async fn delete( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, account_id: &AccountId, ) -> error_stack::Result<(), KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; sqlx::query( //language=postgresql r#" @@ -205,11 +205,11 @@ impl AccountReadModel for PostgresAccountReadModel { async fn link_auth_account( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, account_id: &AccountId, auth_account_id: &AuthAccountId, ) -> error_stack::Result<(), KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; sqlx::query( //language=postgresql r#" diff --git a/driver/src/database/postgres/account_event_store.rs b/driver/src/database/postgres/account_event_store.rs index f268bc3..cf778ed 100644 --- a/driver/src/database/postgres/account_event_store.rs +++ b/driver/src/database/postgres/account_event_store.rs @@ -35,15 +35,15 @@ impl TryFrom for EventEnvelope { pub struct PostgresAccountEventStore; impl AccountEventStore for PostgresAccountEventStore { - type Transaction = PostgresConnection; + type Executor = PostgresConnection; async fn find_by_id( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, id: &EventId, since: Option<&EventVersion>, ) -> error_stack::Result>, KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; let rows = if let Some(version) = since { sqlx::query_as::<_, EventRow>( //language=postgresql @@ -81,21 +81,20 @@ impl AccountEventStore for PostgresAccountEventStore { async fn persist( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, command: &CommandEnvelope, ) -> error_stack::Result<(), KernelError> { - self.persist_internal(transaction, command, Uuid::now_v7()) + self.persist_internal(executor, command, Uuid::now_v7()) .await } async fn persist_and_transform( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, command: CommandEnvelope, ) -> error_stack::Result, KernelError> { let version = Uuid::now_v7(); - self.persist_internal(transaction, &command, version) - .await?; + self.persist_internal(executor, &command, version).await?; let command = command.into_destruct(); Ok(EventEnvelope::new( @@ -109,11 +108,11 @@ impl AccountEventStore for PostgresAccountEventStore { impl PostgresAccountEventStore { async fn persist_internal( &self, - transaction: &mut PostgresConnection, + executor: &mut PostgresConnection, command: &CommandEnvelope, version: Uuid, ) -> error_stack::Result<(), KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; let event_name = command.event_name(); let prev_version = command.prev_version().as_ref(); diff --git a/driver/src/database/postgres/auth_account.rs b/driver/src/database/postgres/auth_account.rs index 3d0ff07..e6831b4 100644 --- a/driver/src/database/postgres/auth_account.rs +++ b/driver/src/database/postgres/auth_account.rs @@ -31,14 +31,14 @@ impl From for AuthAccount { pub struct PostgresAuthAccountRepository; impl AuthAccountQuery for PostgresAuthAccountRepository { - type Transaction = PostgresConnection; + type Executor = PostgresConnection; async fn find_by_id( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, account_id: &AuthAccountId, ) -> error_stack::Result, KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; sqlx::query_as::<_, AuthAccountRow>( //language=postgresql r#" @@ -56,10 +56,10 @@ impl AuthAccountQuery for PostgresAuthAccountRepository { async fn find_by_client_id( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, client_id: &AuthAccountClientId, ) -> error_stack::Result, KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; sqlx::query_as::<_, AuthAccountRow>( //language=postgresql r#" @@ -85,14 +85,14 @@ impl DependOnAuthAccountQuery for PostgresDatabase { } impl AuthAccountModifier for PostgresAuthAccountRepository { - type Transaction = PostgresConnection; + type Executor = PostgresConnection; async fn create( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, auth_account: &AuthAccount, ) -> error_stack::Result<(), KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; sqlx::query( //language=postgresql r#" @@ -111,10 +111,10 @@ impl AuthAccountModifier for PostgresAuthAccountRepository { async fn update( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, auth_account: &AuthAccount, ) -> error_stack::Result<(), KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; sqlx::query( //language=postgresql r#" @@ -134,10 +134,10 @@ impl AuthAccountModifier for PostgresAuthAccountRepository { async fn delete( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, account_id: &AuthAccountId, ) -> error_stack::Result<(), KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; sqlx::query( //language=postgresql r#" diff --git a/driver/src/database/postgres/auth_host.rs b/driver/src/database/postgres/auth_host.rs index 12371c5..e5b5077 100644 --- a/driver/src/database/postgres/auth_host.rs +++ b/driver/src/database/postgres/auth_host.rs @@ -22,13 +22,13 @@ impl From for AuthHost { pub struct PostgresAuthHostRepository; impl AuthHostQuery for PostgresAuthHostRepository { - type Transaction = PostgresConnection; + type Executor = PostgresConnection; async fn find_by_id( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, id: &AuthHostId, ) -> error_stack::Result, KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; sqlx::query_as::<_, AuthHostRow>( // language=postgresql r#" @@ -46,10 +46,10 @@ impl AuthHostQuery for PostgresAuthHostRepository { async fn find_by_url( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, domain: &AuthHostUrl, ) -> error_stack::Result, KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; sqlx::query_as::<_, AuthHostRow>( // language=postgresql r#" @@ -75,13 +75,13 @@ impl DependOnAuthHostQuery for PostgresDatabase { } impl AuthHostModifier for PostgresAuthHostRepository { - type Transaction = PostgresConnection; + type Executor = PostgresConnection; async fn create( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, auth_host: &AuthHost, ) -> error_stack::Result<(), KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; sqlx::query( // language=postgresql r#" @@ -99,10 +99,10 @@ impl AuthHostModifier for PostgresAuthHostRepository { async fn update( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, auth_host: &AuthHost, ) -> error_stack::Result<(), KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; sqlx::query( // language=postgresql r#" diff --git a/driver/src/database/postgres/event.rs b/driver/src/database/postgres/event.rs index 4c4176d..c90b47a 100644 --- a/driver/src/database/postgres/event.rs +++ b/driver/src/database/postgres/event.rs @@ -35,15 +35,15 @@ impl Deserialize<'a>, Entity> TryFrom for EventEnvelope pub struct PostgresEventRepository; impl EventQuery for PostgresEventRepository { - type Transaction = PostgresConnection; + type Executor = PostgresConnection; async fn find_by_id Deserialize<'de>, Entity>( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, id: &EventId, since: Option<&EventVersion>, ) -> error_stack::Result>, KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; if let Some(version) = since { sqlx::query_as::<_, EventRow>( //language=postgresql @@ -88,27 +88,26 @@ impl DependOnEventQuery for PostgresDatabase { } impl EventModifier for PostgresEventRepository { - type Transaction = PostgresConnection; + type Executor = PostgresConnection; async fn persist( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, command: &CommandEnvelope, ) -> error_stack::Result<(), KernelError> { - self.persist_internal(transaction, command, Uuid::now_v7()) + self.persist_internal(executor, command, Uuid::now_v7()) .await?; Ok(()) } async fn persist_and_transform( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, command: CommandEnvelope, ) -> error_stack::Result, KernelError> { let version = Uuid::now_v7(); - self.persist_internal(transaction, &command, version) - .await?; + self.persist_internal(executor, &command, version).await?; let command = command.into_destruct(); Ok(EventEnvelope::new( @@ -122,11 +121,11 @@ impl EventModifier for PostgresEventRepository { impl PostgresEventRepository { async fn persist_internal( &self, - transaction: &mut PostgresConnection, + executor: &mut PostgresConnection, command: &CommandEnvelope, version: Uuid, ) -> error_stack::Result<(), KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; // Validation logic let event_name = command.event_name(); diff --git a/driver/src/database/postgres/follow.rs b/driver/src/database/postgres/follow.rs index 7a54402..359bfd8 100644 --- a/driver/src/database/postgres/follow.rs +++ b/driver/src/database/postgres/follow.rs @@ -63,14 +63,14 @@ impl TryFrom for Follow { pub struct PostgresFollowRepository; impl FollowQuery for PostgresFollowRepository { - type Transaction = PostgresConnection; + type Executor = PostgresConnection; async fn find_followings( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, source_id: &FollowTargetId, ) -> error_stack::Result, KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; match source_id { FollowTargetId::Local(account_id) => { sqlx::query_as::<_, FollowRow>( @@ -100,10 +100,10 @@ impl FollowQuery for PostgresFollowRepository { async fn find_followers( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, destination_id: &FollowTargetId, ) -> error_stack::Result, KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; match destination_id { FollowTargetId::Local(account_id) => { sqlx::query_as::<_, FollowRow>( @@ -148,14 +148,14 @@ fn split_follow_target_id(target_id: &FollowTargetId) -> (Option<&Uuid>, Option< } impl FollowModifier for PostgresFollowRepository { - type Transaction = PostgresConnection; + type Executor = PostgresConnection; async fn create( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, follow: &Follow, ) -> error_stack::Result<(), KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; let (follower_local_id, follower_remote_id) = split_follow_target_id(follow.source()); let (followee_local_id, followee_remote_id) = split_follow_target_id(follow.destination()); sqlx::query( @@ -178,10 +178,10 @@ impl FollowModifier for PostgresFollowRepository { async fn update( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, follow: &Follow, ) -> error_stack::Result<(), KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; let (follower_local_id, follower_remote_id) = split_follow_target_id(follow.source()); let (followee_local_id, followee_remote_id) = split_follow_target_id(follow.destination()); sqlx::query( @@ -205,10 +205,10 @@ impl FollowModifier for PostgresFollowRepository { async fn delete( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, follow_id: &FollowId, ) -> error_stack::Result<(), KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; sqlx::query( //language=postgresql r#" diff --git a/driver/src/database/postgres/image.rs b/driver/src/database/postgres/image.rs index a0b5724..9304eeb 100644 --- a/driver/src/database/postgres/image.rs +++ b/driver/src/database/postgres/image.rs @@ -29,14 +29,14 @@ impl From for Image { pub struct PostgresImageRepository; impl ImageQuery for PostgresImageRepository { - type Transaction = PostgresConnection; + type Executor = PostgresConnection; async fn find_by_id( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, id: &ImageId, ) -> error_stack::Result, KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; sqlx::query_as::<_, ImageRow>( // language=postgresql r#" @@ -52,10 +52,10 @@ impl ImageQuery for PostgresImageRepository { async fn find_by_url( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, url: &ImageUrl, ) -> error_stack::Result, KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; sqlx::query_as::<_, ImageRow>( // language=postgresql r#" @@ -79,14 +79,14 @@ impl DependOnImageQuery for PostgresDatabase { } impl ImageModifier for PostgresImageRepository { - type Transaction = PostgresConnection; + type Executor = PostgresConnection; async fn create( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, image: &Image, ) -> error_stack::Result<(), KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; sqlx::query( // language=postgresql r#" @@ -105,10 +105,10 @@ impl ImageModifier for PostgresImageRepository { async fn delete( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, image_id: &ImageId, ) -> error_stack::Result<(), KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; sqlx::query( // language=postgresql r#" diff --git a/driver/src/database/postgres/metadata.rs b/driver/src/database/postgres/metadata.rs index 1bdf934..4dfd7f4 100644 --- a/driver/src/database/postgres/metadata.rs +++ b/driver/src/database/postgres/metadata.rs @@ -35,14 +35,14 @@ impl From for Metadata { pub struct PostgresMetadataRepository; impl MetadataQuery for PostgresMetadataRepository { - type Transaction = PostgresConnection; + type Executor = PostgresConnection; async fn find_by_id( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, metadata_id: &MetadataId, ) -> error_stack::Result, KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; sqlx::query_as::<_, MetadataRow>( // language=postgresql r#" @@ -60,10 +60,10 @@ impl MetadataQuery for PostgresMetadataRepository { async fn find_by_account_id( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, account_id: &AccountId, ) -> error_stack::Result, KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; sqlx::query_as::<_, MetadataRow>( // language=postgresql r#" @@ -89,14 +89,14 @@ impl DependOnMetadataQuery for PostgresDatabase { } impl MetadataModifier for PostgresMetadataRepository { - type Transaction = PostgresConnection; + type Executor = PostgresConnection; async fn create( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, metadata: &Metadata, ) -> error_stack::Result<(), KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; sqlx::query( // language=postgresql r#" @@ -118,10 +118,10 @@ impl MetadataModifier for PostgresMetadataRepository { async fn update( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, metadata: &Metadata, ) -> error_stack::Result<(), KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; sqlx::query( // language=postgresql r#" @@ -142,10 +142,10 @@ impl MetadataModifier for PostgresMetadataRepository { async fn delete( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, metadata_id: &MetadataId, ) -> error_stack::Result<(), KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; sqlx::query( // language=postgresql r#" diff --git a/driver/src/database/postgres/profile.rs b/driver/src/database/postgres/profile.rs index 58b5c7a..8c2c737 100644 --- a/driver/src/database/postgres/profile.rs +++ b/driver/src/database/postgres/profile.rs @@ -42,14 +42,14 @@ impl From for Profile { pub struct PostgresProfileRepository; impl ProfileQuery for PostgresProfileRepository { - type Transaction = PostgresConnection; + type Executor = PostgresConnection; async fn find_by_id( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, id: &ProfileId, ) -> error_stack::Result, KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; sqlx::query_as::<_, ProfileRow>( //language=postgresql r#" @@ -74,14 +74,14 @@ impl DependOnProfileQuery for PostgresDatabase { } impl ProfileModifier for PostgresProfileRepository { - type Transaction = PostgresConnection; + type Executor = PostgresConnection; async fn create( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, profile: &Profile, ) -> error_stack::Result<(), KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; sqlx::query( //language=postgresql r#" @@ -110,10 +110,10 @@ impl ProfileModifier for PostgresProfileRepository { async fn update( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, profile: &Profile, ) -> error_stack::Result<(), KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; sqlx::query( //language=postgresql r#" diff --git a/driver/src/database/postgres/remote_account.rs b/driver/src/database/postgres/remote_account.rs index 407d60c..819df1c 100644 --- a/driver/src/database/postgres/remote_account.rs +++ b/driver/src/database/postgres/remote_account.rs @@ -31,14 +31,14 @@ impl From for RemoteAccount { pub struct PostgresRemoteAccountRepository; impl RemoteAccountQuery for PostgresRemoteAccountRepository { - type Transaction = PostgresConnection; + type Executor = PostgresConnection; async fn find_by_id( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, id: &RemoteAccountId, ) -> error_stack::Result, KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; sqlx::query_as::<_, RemoteAccountRow>( // language=postgresql r#" @@ -56,10 +56,10 @@ impl RemoteAccountQuery for PostgresRemoteAccountRepository { async fn find_by_acct( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, acct: &RemoteAccountAcct, ) -> error_stack::Result, KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; sqlx::query_as::<_, RemoteAccountRow>( // language=postgresql r#" @@ -77,10 +77,10 @@ impl RemoteAccountQuery for PostgresRemoteAccountRepository { async fn find_by_url( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, url: &RemoteAccountUrl, ) -> error_stack::Result, KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; sqlx::query_as::<_, RemoteAccountRow>( // language=postgresql r#" @@ -106,14 +106,14 @@ impl DependOnRemoteAccountQuery for PostgresDatabase { } impl RemoteAccountModifier for PostgresRemoteAccountRepository { - type Transaction = PostgresConnection; + type Executor = PostgresConnection; async fn create( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, account: &RemoteAccount, ) -> error_stack::Result<(), KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; sqlx::query( // language=postgresql r#" @@ -133,10 +133,10 @@ impl RemoteAccountModifier for PostgresRemoteAccountRepository { async fn update( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, account: &RemoteAccount, ) -> error_stack::Result<(), KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; sqlx::query( // language=postgresql r#" @@ -157,10 +157,10 @@ impl RemoteAccountModifier for PostgresRemoteAccountRepository { async fn delete( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, account_id: &RemoteAccountId, ) -> error_stack::Result<(), KernelError> { - let con: &mut PgConnection = transaction; + let con: &mut PgConnection = executor; sqlx::query( // language=postgresql r#" diff --git a/driver/src/database/redis.rs b/driver/src/database/redis.rs index 36ffe37..c1a24e9 100644 --- a/driver/src/database/redis.rs +++ b/driver/src/database/redis.rs @@ -1,7 +1,7 @@ use crate::database::env; use deadpool_redis::{Config, Pool, Runtime}; use error_stack::{Report, ResultExt}; -use kernel::interfaces::database::{DatabaseConnection, Transaction}; +use kernel::interfaces::database::{DatabaseConnection, Executor}; use kernel::KernelError; use std::ops::Deref; use vodca::References; @@ -38,7 +38,7 @@ impl RedisDatabase { pub struct RedisConnection(deadpool_redis::Connection); -impl Transaction for RedisConnection {} +impl Executor for RedisConnection {} impl Deref for RedisConnection { type Target = deadpool_redis::Connection; @@ -49,9 +49,9 @@ impl Deref for RedisConnection { } impl DatabaseConnection for RedisDatabase { - type Transaction = RedisConnection; + type Executor = RedisConnection; - async fn begin_transaction(&self) -> error_stack::Result { + async fn begin_transaction(&self) -> error_stack::Result { let pool = self .pool .get() diff --git a/kernel/src/database.rs b/kernel/src/database.rs index c2d5307..e9dc45c 100644 --- a/kernel/src/database.rs +++ b/kernel/src/database.rs @@ -4,13 +4,13 @@ use std::future::Future; /// Databaseのトランザクション処理を示すトレイト /// /// 現状は何もないが、将来的にトランザクション時に使える機能を示す可能性を考えて用意している -pub trait Transaction {} +pub trait Executor {} pub trait DatabaseConnection: Sync + Send + 'static { - type Transaction: Transaction; + type Executor: Executor; fn begin_transaction( &self, - ) -> impl Future> + Send; + ) -> impl Future> + Send; } pub trait DependOnDatabaseConnection: Sync + Send { diff --git a/kernel/src/event_store/account.rs b/kernel/src/event_store/account.rs index 61c097d..2e67d4e 100644 --- a/kernel/src/event_store/account.rs +++ b/kernel/src/event_store/account.rs @@ -1,27 +1,27 @@ -use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Transaction}; +use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Executor}; use crate::entity::{Account, AccountEvent, CommandEnvelope, EventEnvelope, EventId, EventVersion}; use crate::KernelError; use std::future::Future; pub trait AccountEventStore: Sync + Send + 'static { - type Transaction: Transaction; + type Executor: Executor; fn persist( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, command: &CommandEnvelope, ) -> impl Future> + Send; fn persist_and_transform( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, command: CommandEnvelope, ) -> impl Future, KernelError>> + Send; fn find_by_id( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, id: &EventId, since: Option<&EventVersion>, ) -> impl Future< @@ -31,7 +31,7 @@ pub trait AccountEventStore: Sync + Send + 'static { pub trait DependOnAccountEventStore: Sync + Send + DependOnDatabaseConnection { type AccountEventStore: AccountEventStore< - Transaction = ::Transaction, + Executor = ::Executor, >; fn account_event_store(&self) -> &Self::AccountEventStore; diff --git a/kernel/src/modify/auth_account.rs b/kernel/src/modify/auth_account.rs index 7242853..e933f27 100644 --- a/kernel/src/modify/auth_account.rs +++ b/kernel/src/modify/auth_account.rs @@ -1,33 +1,33 @@ -use crate::database::{DependOnDatabaseConnection, Transaction}; +use crate::database::{DependOnDatabaseConnection, Executor}; use crate::entity::{AuthAccount, AuthAccountId}; use crate::KernelError; use std::future::Future; pub trait AuthAccountModifier: Sync + Send + 'static { - type Transaction: Transaction; + type Executor: Executor; fn create( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, auth_account: &AuthAccount, ) -> impl Future> + Send; fn update( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, auth_account: &AuthAccount, ) -> impl Future> + Send; fn delete( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, account_id: &AuthAccountId, ) -> impl Future> + Send; } pub trait DependOnAuthAccountModifier: Sync + Send + DependOnDatabaseConnection { type AuthAccountModifier: AuthAccountModifier< - Transaction = ::Transaction, + Executor = ::Executor, >; fn auth_account_modifier(&self) -> &Self::AuthAccountModifier; diff --git a/kernel/src/modify/auth_host.rs b/kernel/src/modify/auth_host.rs index 0d7fa99..8aeb656 100644 --- a/kernel/src/modify/auth_host.rs +++ b/kernel/src/modify/auth_host.rs @@ -1,27 +1,27 @@ -use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Transaction}; +use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Executor}; use crate::entity::AuthHost; use crate::KernelError; use std::future::Future; pub trait AuthHostModifier: Sync + Send + 'static { - type Transaction: Transaction; + type Executor: Executor; fn create( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, auth_host: &AuthHost, ) -> impl Future> + Send; fn update( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, auth_host: &AuthHost, ) -> impl Future> + Send; } pub trait DependOnAuthHostModifier: Sync + Send + DependOnDatabaseConnection { type AuthHostModifier: AuthHostModifier< - Transaction = ::Transaction, + Executor = ::Executor, >; fn auth_host_modifier(&self) -> &Self::AuthHostModifier; diff --git a/kernel/src/modify/event.rs b/kernel/src/modify/event.rs index 7c5a118..639dbd1 100644 --- a/kernel/src/modify/event.rs +++ b/kernel/src/modify/event.rs @@ -1,28 +1,28 @@ -use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Transaction}; +use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Executor}; use crate::entity::{CommandEnvelope, EventEnvelope}; use crate::KernelError; use serde::Serialize; use std::future::Future; pub trait EventModifier: 'static + Sync + Send { - type Transaction: Transaction; + type Executor: Executor; fn persist( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, command: &CommandEnvelope, ) -> impl Future> + Send; fn persist_and_transform( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, command: CommandEnvelope, ) -> impl Future, KernelError>> + Send; } pub trait DependOnEventModifier: Sync + Send + DependOnDatabaseConnection { type EventModifier: EventModifier< - Transaction = ::Transaction, + Executor = ::Executor, >; fn event_modifier(&self) -> &Self::EventModifier; diff --git a/kernel/src/modify/follow.rs b/kernel/src/modify/follow.rs index 3f0f7d6..b807aa8 100644 --- a/kernel/src/modify/follow.rs +++ b/kernel/src/modify/follow.rs @@ -1,33 +1,33 @@ -use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Transaction}; +use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Executor}; use crate::entity::{Follow, FollowId}; use crate::KernelError; use std::future::Future; pub trait FollowModifier: Sync + Send + 'static { - type Transaction: Transaction; + type Executor: Executor; fn create( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, follow: &Follow, ) -> impl Future> + Send; fn update( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, follow: &Follow, ) -> impl Future> + Send; fn delete( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, follow_id: &FollowId, ) -> impl Future> + Send; } pub trait DependOnFollowModifier: Sync + Send + DependOnDatabaseConnection { type FollowModifier: FollowModifier< - Transaction = ::Transaction, + Executor = ::Executor, >; fn follow_modifier(&self) -> &Self::FollowModifier; diff --git a/kernel/src/modify/image.rs b/kernel/src/modify/image.rs index d95d107..93b9593 100644 --- a/kernel/src/modify/image.rs +++ b/kernel/src/modify/image.rs @@ -1,27 +1,27 @@ -use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Transaction}; +use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Executor}; use crate::entity::{Image, ImageId}; use crate::KernelError; use std::future::Future; pub trait ImageModifier: Sync + Send + 'static { - type Transaction: Transaction; + type Executor: Executor; fn create( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, image: &Image, ) -> impl Future> + Send; fn delete( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, image_id: &ImageId, ) -> impl Future> + Send; } pub trait DependOnImageModifier: Sync + Send + DependOnDatabaseConnection { type ImageModifier: ImageModifier< - Transaction = ::Transaction, + Executor = ::Executor, >; fn image_modifier(&self) -> &Self::ImageModifier; diff --git a/kernel/src/modify/metadata.rs b/kernel/src/modify/metadata.rs index 80555ac..f31a748 100644 --- a/kernel/src/modify/metadata.rs +++ b/kernel/src/modify/metadata.rs @@ -1,33 +1,33 @@ -use crate::database::{DependOnDatabaseConnection, Transaction}; +use crate::database::{DependOnDatabaseConnection, Executor}; use crate::entity::{Metadata, MetadataId}; use crate::KernelError; use std::future::Future; pub trait MetadataModifier: Sync + Send + 'static { - type Transaction: Transaction; + type Executor: Executor; fn create( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, metadata: &Metadata, ) -> impl Future> + Send; fn update( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, metadata: &Metadata, ) -> impl Future> + Send; fn delete( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, metadata_id: &MetadataId, ) -> impl Future> + Send; } pub trait DependOnMetadataModifier: Sync + Send + DependOnDatabaseConnection { type MetadataModifier: MetadataModifier< - Transaction = ::Transaction, + Executor = ::Executor, >; fn metadata_modifier(&self) -> &Self::MetadataModifier; diff --git a/kernel/src/modify/profile.rs b/kernel/src/modify/profile.rs index 146217e..9435133 100644 --- a/kernel/src/modify/profile.rs +++ b/kernel/src/modify/profile.rs @@ -1,27 +1,27 @@ -use crate::database::{DependOnDatabaseConnection, Transaction}; +use crate::database::{DependOnDatabaseConnection, Executor}; use crate::entity::Profile; use crate::KernelError; use std::future::Future; pub trait ProfileModifier: Sync + Send + 'static { - type Transaction: Transaction; + type Executor: Executor; fn create( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, profile: &Profile, ) -> impl Future> + Send; fn update( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, profile: &Profile, ) -> impl Future> + Send; } pub trait DependOnProfileModifier: Sync + Send + DependOnDatabaseConnection { type ProfileModifier: ProfileModifier< - Transaction = ::Transaction, + Executor = ::Executor, >; fn profile_modifier(&self) -> &Self::ProfileModifier; diff --git a/kernel/src/modify/remote_account.rs b/kernel/src/modify/remote_account.rs index b8a7c1c..9639826 100644 --- a/kernel/src/modify/remote_account.rs +++ b/kernel/src/modify/remote_account.rs @@ -1,33 +1,33 @@ -use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Transaction}; +use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Executor}; use crate::entity::{RemoteAccount, RemoteAccountId}; use crate::KernelError; use std::future::Future; pub trait RemoteAccountModifier: Sync + Send + 'static { - type Transaction: Transaction; + type Executor: Executor; fn create( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, account: &RemoteAccount, ) -> impl Future> + Send; fn update( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, account: &RemoteAccount, ) -> impl Future> + Send; fn delete( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, account_id: &RemoteAccountId, ) -> impl Future> + Send; } pub trait DependOnRemoteAccountModifier: Sync + Send + DependOnDatabaseConnection { type RemoteAccountModifier: RemoteAccountModifier< - Transaction = ::Transaction, + Executor = ::Executor, >; fn remote_account_modifier(&self) -> &Self::RemoteAccountModifier; diff --git a/kernel/src/query/auth_account.rs b/kernel/src/query/auth_account.rs index 9cc594d..092994e 100644 --- a/kernel/src/query/auth_account.rs +++ b/kernel/src/query/auth_account.rs @@ -1,27 +1,27 @@ -use crate::database::{DependOnDatabaseConnection, Transaction}; +use crate::database::{DependOnDatabaseConnection, Executor}; use crate::entity::{AuthAccount, AuthAccountClientId, AuthAccountId}; use crate::KernelError; use std::future::Future; pub trait AuthAccountQuery: Sync + Send + 'static { - type Transaction: Transaction; + type Executor: Executor; fn find_by_id( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, account_id: &AuthAccountId, ) -> impl Future, KernelError>> + Send; fn find_by_client_id( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, client_id: &AuthAccountClientId, ) -> impl Future, KernelError>> + Send; } pub trait DependOnAuthAccountQuery: Sync + Send + DependOnDatabaseConnection { type AuthAccountQuery: AuthAccountQuery< - Transaction = ::Transaction, + Executor = ::Executor, >; fn auth_account_query(&self) -> &Self::AuthAccountQuery; diff --git a/kernel/src/query/auth_host.rs b/kernel/src/query/auth_host.rs index 8f7ed19..8396075 100644 --- a/kernel/src/query/auth_host.rs +++ b/kernel/src/query/auth_host.rs @@ -1,27 +1,27 @@ -use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Transaction}; +use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Executor}; use crate::entity::{AuthHost, AuthHostId, AuthHostUrl}; use crate::KernelError; use std::future::Future; pub trait AuthHostQuery: Sync + Send + 'static { - type Transaction: Transaction; + type Executor: Executor; fn find_by_id( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, id: &AuthHostId, ) -> impl Future, KernelError>> + Send; fn find_by_url( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, domain: &AuthHostUrl, ) -> impl Future, KernelError>> + Send; } pub trait DependOnAuthHostQuery: Sync + Send + DependOnDatabaseConnection { type AuthHostQuery: AuthHostQuery< - Transaction = ::Transaction, + Executor = ::Executor, >; fn auth_host_query(&self) -> &Self::AuthHostQuery; diff --git a/kernel/src/query/event.rs b/kernel/src/query/event.rs index 994003a..e4701d3 100644 --- a/kernel/src/query/event.rs +++ b/kernel/src/query/event.rs @@ -1,15 +1,15 @@ -use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Transaction}; +use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Executor}; use crate::entity::{EventEnvelope, EventId, EventVersion}; use crate::KernelError; use serde::Deserialize; use std::future::Future; pub trait EventQuery: Sync + Send + 'static { - type Transaction: Transaction; + type Executor: Executor; fn find_by_id Deserialize<'de> + Sync, Entity: Sync>( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, id: &EventId, since: Option<&EventVersion>, ) -> impl Future>, KernelError>> + Send; @@ -17,7 +17,7 @@ pub trait EventQuery: Sync + Send + 'static { pub trait DependOnEventQuery: Sync + Send + DependOnDatabaseConnection { type EventQuery: EventQuery< - Transaction = ::Transaction, + Executor = ::Executor, >; fn event_query(&self) -> &Self::EventQuery; diff --git a/kernel/src/query/follow.rs b/kernel/src/query/follow.rs index b6730cd..21a6429 100644 --- a/kernel/src/query/follow.rs +++ b/kernel/src/query/follow.rs @@ -1,27 +1,27 @@ -use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Transaction}; +use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Executor}; use crate::entity::{Follow, FollowTargetId}; use crate::KernelError; use std::future::Future; pub trait FollowQuery: Sync + Send + 'static { - type Transaction: Transaction; + type Executor: Executor; fn find_followings( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, source: &FollowTargetId, ) -> impl Future, KernelError>> + Send; fn find_followers( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, destination: &FollowTargetId, ) -> impl Future, KernelError>> + Send; } pub trait DependOnFollowQuery: Sync + Send + DependOnDatabaseConnection { type FollowQuery: FollowQuery< - Transaction = ::Transaction, + Executor = ::Executor, >; fn follow_query(&self) -> &Self::FollowQuery; diff --git a/kernel/src/query/image.rs b/kernel/src/query/image.rs index ef34a40..71568fc 100644 --- a/kernel/src/query/image.rs +++ b/kernel/src/query/image.rs @@ -1,27 +1,27 @@ -use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Transaction}; +use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Executor}; use crate::entity::{Image, ImageId, ImageUrl}; use crate::KernelError; use std::future::Future; pub trait ImageQuery: Sync + Send + 'static { - type Transaction: Transaction; + type Executor: Executor; fn find_by_id( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, id: &ImageId, ) -> impl Future, KernelError>> + Send; fn find_by_url( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, url: &ImageUrl, ) -> impl Future, KernelError>> + Send; } pub trait DependOnImageQuery: Sync + Send + DependOnDatabaseConnection { type ImageQuery: ImageQuery< - Transaction = ::Transaction, + Executor = ::Executor, >; fn image_query(&self) -> &Self::ImageQuery; diff --git a/kernel/src/query/metadata.rs b/kernel/src/query/metadata.rs index 6d491fd..824b421 100644 --- a/kernel/src/query/metadata.rs +++ b/kernel/src/query/metadata.rs @@ -1,27 +1,27 @@ -use crate::database::{DependOnDatabaseConnection, Transaction}; +use crate::database::{DependOnDatabaseConnection, Executor}; use crate::entity::{AccountId, Metadata, MetadataId}; use crate::KernelError; use std::future::Future; pub trait MetadataQuery: Sync + Send + 'static { - type Transaction: Transaction; + type Executor: Executor; fn find_by_id( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, metadata_id: &MetadataId, ) -> impl Future, KernelError>> + Send; fn find_by_account_id( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, account_id: &AccountId, ) -> impl Future, KernelError>> + Send; } pub trait DependOnMetadataQuery: Sync + Send + DependOnDatabaseConnection { type MetadataQuery: MetadataQuery< - Transaction = ::Transaction, + Executor = ::Executor, >; fn metadata_query(&self) -> &Self::MetadataQuery; diff --git a/kernel/src/query/profile.rs b/kernel/src/query/profile.rs index d8d2cb9..0fa6130 100644 --- a/kernel/src/query/profile.rs +++ b/kernel/src/query/profile.rs @@ -1,21 +1,21 @@ -use crate::database::{DependOnDatabaseConnection, Transaction}; +use crate::database::{DependOnDatabaseConnection, Executor}; use crate::entity::{Profile, ProfileId}; use crate::KernelError; use std::future::Future; pub trait ProfileQuery: Sync + Send + 'static { - type Transaction: Transaction; + type Executor: Executor; fn find_by_id( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, id: &ProfileId, ) -> impl Future, KernelError>> + Send; } pub trait DependOnProfileQuery: Sync + Send + DependOnDatabaseConnection { type ProfileQuery: ProfileQuery< - Transaction = ::Transaction, + Executor = ::Executor, >; fn profile_query(&self) -> &Self::ProfileQuery; diff --git a/kernel/src/query/remote_account.rs b/kernel/src/query/remote_account.rs index 8538fab..ee911bf 100644 --- a/kernel/src/query/remote_account.rs +++ b/kernel/src/query/remote_account.rs @@ -1,33 +1,33 @@ -use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Transaction}; +use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Executor}; use crate::entity::{RemoteAccount, RemoteAccountAcct, RemoteAccountId, RemoteAccountUrl}; use crate::KernelError; use std::future::Future; pub trait RemoteAccountQuery: Sync + Send + 'static { - type Transaction: Transaction; + type Executor: Executor; fn find_by_id( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, id: &RemoteAccountId, ) -> impl Future, KernelError>> + Send; fn find_by_acct( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, acct: &RemoteAccountAcct, ) -> impl Future, KernelError>> + Send; fn find_by_url( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, url: &RemoteAccountUrl, ) -> impl Future, KernelError>> + Send; } pub trait DependOnRemoteAccountQuery: Sync + Send + DependOnDatabaseConnection { type RemoteAccountQuery: RemoteAccountQuery< - Transaction = ::Transaction, + Executor = ::Executor, >; fn remote_account_query(&self) -> &Self::RemoteAccountQuery; diff --git a/kernel/src/read_model/account.rs b/kernel/src/read_model/account.rs index 3c8bf09..edd82d0 100644 --- a/kernel/src/read_model/account.rs +++ b/kernel/src/read_model/account.rs @@ -1,58 +1,58 @@ -use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Transaction}; +use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Executor}; use crate::entity::{Account, AccountId, AccountName, AuthAccountId, Nanoid}; use crate::KernelError; use std::future::Future; pub trait AccountReadModel: Sync + Send + 'static { - type Transaction: Transaction; + type Executor: Executor; // Query operations (projection reads) fn find_by_id( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, id: &AccountId, ) -> impl Future, KernelError>> + Send; fn find_by_auth_id( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, auth_id: &AuthAccountId, ) -> impl Future, KernelError>> + Send; fn find_by_name( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, name: &AccountName, ) -> impl Future, KernelError>> + Send; fn find_by_nanoid( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, nanoid: &Nanoid, ) -> impl Future, KernelError>> + Send; // Projection update operations (called by EventApplier pipeline) fn create( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, account: &Account, ) -> impl Future> + Send; fn update( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, account: &Account, ) -> impl Future> + Send; fn delete( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, account_id: &AccountId, ) -> impl Future> + Send; fn link_auth_account( &self, - transaction: &mut Self::Transaction, + executor: &mut Self::Executor, account_id: &AccountId, auth_account_id: &AuthAccountId, ) -> impl Future> + Send; @@ -60,7 +60,7 @@ pub trait AccountReadModel: Sync + Send + 'static { pub trait DependOnAccountReadModel: Sync + Send + DependOnDatabaseConnection { type AccountReadModel: AccountReadModel< - Transaction = ::Transaction, + Executor = ::Executor, >; fn account_read_model(&self) -> &Self::AccountReadModel; From 343bf98ecbc57b01559b0e742c8e273ec94daf4a Mon Sep 17 00:00:00 2001 From: turtton Date: Mon, 9 Mar 2026 17:06:29 +0900 Subject: [PATCH 47/59] :recycle: Introduce adapter layer with AccountCommandProcessor/QueryProcessor for CQRS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AccountEvent::Created に auth_account_id を追加し Account::create() static メソッドを追加 - adapter層に AccountCommandProcessor (EventStore + Signal をラップ) と AccountQueryProcessor (ReadModel読み取り専用) を配置 - application層のサービスを UseCase トレイトに書き換え (Get/Create/Edit/Delete) signal パラメータと認証関連DI境界を削除 - 認証ロジック (resolve_auth_account_id) を server層に移動 - AppModule に DependOn* トレイトを手動実装し blanket impl 経由で全UseCase を自動実装 - AccountApplier を直接ロジックに置き換え (Created イベントから auth_account_id を 抽出して link_auth_account も処理) - AuthAccountInfo を削除 (server層の KeycloakAuthAccount で代替) - Signal::emit に + Send、Executor に : Send を追加 --- adapter/Cargo.toml | 1 + adapter/src/lib.rs | 1 + adapter/src/processor.rs | 1 + adapter/src/processor/account.rs | 236 +++++++++++++ application/src/service/account.rs | 314 ++++-------------- application/src/service/auth_account.rs | 82 +---- application/src/transfer.rs | 1 - application/src/transfer/auth_account.rs | 5 - .../database/postgres/account_event_store.rs | 6 +- driver/src/database/postgres/event.rs | 6 +- kernel/src/database.rs | 2 +- kernel/src/entity/account.rs | 35 +- kernel/src/signal.rs | 5 +- server/src/applier.rs | 2 +- server/src/applier/account_applier.rs | 91 ++++- server/src/handler.rs | 64 +++- server/src/keycloak.rs | 73 +++- server/src/route/account.rs | 80 +++-- 18 files changed, 612 insertions(+), 393 deletions(-) create mode 100644 adapter/src/processor.rs create mode 100644 adapter/src/processor/account.rs delete mode 100644 application/src/transfer/auth_account.rs diff --git a/adapter/Cargo.toml b/adapter/Cargo.toml index 2096e6e..7731026 100644 --- a/adapter/Cargo.toml +++ b/adapter/Cargo.toml @@ -7,4 +7,5 @@ authors.workspace = true [dependencies] kernel = { path = "../kernel" } error-stack = { workspace = true } +tracing = { workspace = true } zeroize = "1.7" diff --git a/adapter/src/lib.rs b/adapter/src/lib.rs index 274f0ed..131719b 100644 --- a/adapter/src/lib.rs +++ b/adapter/src/lib.rs @@ -1 +1,2 @@ pub mod crypto; +pub mod processor; diff --git a/adapter/src/processor.rs b/adapter/src/processor.rs new file mode 100644 index 0000000..b0edc6c --- /dev/null +++ b/adapter/src/processor.rs @@ -0,0 +1 @@ +pub mod account; diff --git a/adapter/src/processor/account.rs b/adapter/src/processor/account.rs new file mode 100644 index 0000000..cc6cbf8 --- /dev/null +++ b/adapter/src/processor/account.rs @@ -0,0 +1,236 @@ +use error_stack::Report; +use kernel::interfaces::database::{DatabaseConnection, DependOnDatabaseConnection, Executor}; +use kernel::interfaces::event::EventApplier; +use kernel::interfaces::event_store::{AccountEventStore, DependOnAccountEventStore}; +use kernel::interfaces::read_model::{AccountReadModel, DependOnAccountReadModel}; +use kernel::interfaces::signal::Signal; +use kernel::prelude::entity::{ + Account, AccountId, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, + AuthAccountId, Nanoid, +}; +use kernel::KernelError; +use std::future::Future; + +// --- Signal DI trait (adapter-specific) --- + +pub trait DependOnAccountSignal: Send + Sync { + type AccountSignal: Signal + Send + Sync + 'static; + fn account_signal(&self) -> &Self::AccountSignal; +} + +// --- AccountCommandProcessor --- + +pub trait AccountCommandProcessor: Send + Sync + 'static { + type Executor: Executor; + + fn create( + &self, + executor: &mut Self::Executor, + name: AccountName, + private_key: AccountPrivateKey, + public_key: AccountPublicKey, + is_bot: AccountIsBot, + auth_account_id: AuthAccountId, + ) -> impl Future> + Send; + + fn update( + &self, + executor: &mut Self::Executor, + account_id: AccountId, + is_bot: AccountIsBot, + current_version: kernel::prelude::entity::EventVersion, + ) -> impl Future> + Send; + + fn delete( + &self, + executor: &mut Self::Executor, + account_id: AccountId, + current_version: kernel::prelude::entity::EventVersion, + ) -> impl Future> + Send; +} + +impl AccountCommandProcessor for T +where + T: DependOnAccountEventStore + DependOnAccountSignal + Send + Sync + 'static, +{ + type Executor = + <::AccountEventStore as AccountEventStore>::Executor; + + async fn create( + &self, + executor: &mut Self::Executor, + name: AccountName, + private_key: AccountPrivateKey, + public_key: AccountPublicKey, + is_bot: AccountIsBot, + auth_account_id: AuthAccountId, + ) -> error_stack::Result { + let account_id = AccountId::default(); + let nanoid = Nanoid::::default(); + let command = Account::create( + account_id.clone(), + name, + private_key, + public_key, + is_bot, + nanoid, + auth_account_id, + ); + + let event_envelope = self + .account_event_store() + .persist_and_transform(executor, command) + .await?; + + let mut account = None; + Account::apply(&mut account, event_envelope)?; + let account = account.ok_or_else(|| { + Report::new(KernelError::Internal) + .attach_printable("Failed to construct account from created event") + })?; + + if let Err(e) = self.account_signal().emit(account_id).await { + tracing::warn!("Failed to emit account signal: {:?}", e); + } + + Ok(account) + } + + async fn update( + &self, + executor: &mut Self::Executor, + account_id: AccountId, + is_bot: AccountIsBot, + current_version: kernel::prelude::entity::EventVersion, + ) -> error_stack::Result<(), KernelError> { + let command = Account::update(account_id.clone(), is_bot, current_version); + + self.account_event_store() + .persist_and_transform(executor, command) + .await?; + + if let Err(e) = self.account_signal().emit(account_id).await { + tracing::warn!("Failed to emit account signal: {:?}", e); + } + + Ok(()) + } + + async fn delete( + &self, + executor: &mut Self::Executor, + account_id: AccountId, + current_version: kernel::prelude::entity::EventVersion, + ) -> error_stack::Result<(), KernelError> { + let command = Account::delete(account_id.clone(), current_version); + + self.account_event_store() + .persist_and_transform(executor, command) + .await?; + + if let Err(e) = self.account_signal().emit(account_id).await { + tracing::warn!("Failed to emit account signal: {:?}", e); + } + + Ok(()) + } +} + +pub trait DependOnAccountCommandProcessor: DependOnDatabaseConnection + Send + Sync { + type AccountCommandProcessor: AccountCommandProcessor< + Executor = <::DatabaseConnection as DatabaseConnection>::Executor, + >; + fn account_command_processor(&self) -> &Self::AccountCommandProcessor; +} + +impl DependOnAccountCommandProcessor for T +where + T: DependOnAccountEventStore + + DependOnAccountSignal + + DependOnDatabaseConnection + + Send + + Sync + + 'static, +{ + type AccountCommandProcessor = Self; + fn account_command_processor(&self) -> &Self::AccountCommandProcessor { + self + } +} + +// --- AccountQueryProcessor --- + +pub trait AccountQueryProcessor: Send + Sync + 'static { + type Executor: Executor; + + fn find_by_id( + &self, + executor: &mut Self::Executor, + id: &AccountId, + ) -> impl Future, KernelError>> + Send; + + fn find_by_auth_id( + &self, + executor: &mut Self::Executor, + auth_id: &AuthAccountId, + ) -> impl Future, KernelError>> + Send; + + fn find_by_nanoid( + &self, + executor: &mut Self::Executor, + nanoid: &Nanoid, + ) -> impl Future, KernelError>> + Send; +} + +impl AccountQueryProcessor for T +where + T: DependOnAccountReadModel + Send + Sync + 'static, +{ + type Executor = + <::AccountReadModel as AccountReadModel>::Executor; + + async fn find_by_id( + &self, + executor: &mut Self::Executor, + id: &AccountId, + ) -> error_stack::Result, KernelError> { + self.account_read_model().find_by_id(executor, id).await + } + + async fn find_by_auth_id( + &self, + executor: &mut Self::Executor, + auth_id: &AuthAccountId, + ) -> error_stack::Result, KernelError> { + self.account_read_model() + .find_by_auth_id(executor, auth_id) + .await + } + + async fn find_by_nanoid( + &self, + executor: &mut Self::Executor, + nanoid: &Nanoid, + ) -> error_stack::Result, KernelError> { + self.account_read_model() + .find_by_nanoid(executor, nanoid) + .await + } +} + +pub trait DependOnAccountQueryProcessor: DependOnDatabaseConnection + Send + Sync { + type AccountQueryProcessor: AccountQueryProcessor< + Executor = <::DatabaseConnection as DatabaseConnection>::Executor, + >; + fn account_query_processor(&self) -> &Self::AccountQueryProcessor; +} + +impl DependOnAccountQueryProcessor for T +where + T: DependOnAccountReadModel + DependOnDatabaseConnection + Send + Sync + 'static, +{ + type AccountQueryProcessor = Self; + fn account_query_processor(&self) -> &Self::AccountQueryProcessor { + self + } +} diff --git a/application/src/service/account.rs b/application/src/service/account.rs index efa6d38..e3a0f92 100644 --- a/application/src/service/account.rs +++ b/application/src/service/account.rs @@ -1,61 +1,40 @@ -use crate::service::auth_account::get_auth_account; use crate::transfer::account::AccountDto; -use crate::transfer::auth_account::AuthAccountInfo; use crate::transfer::pagination::{apply_pagination, Pagination}; use adapter::crypto::{DependOnSigningKeyGenerator, SigningKeyGenerator}; +use adapter::processor::account::{ + AccountCommandProcessor, AccountQueryProcessor, DependOnAccountCommandProcessor, + DependOnAccountQueryProcessor, +}; use error_stack::Report; use kernel::interfaces::crypto::{DependOnPasswordProvider, PasswordProvider}; use kernel::interfaces::database::DatabaseConnection; -use kernel::interfaces::event::EventApplier; -use kernel::interfaces::event_store::{AccountEventStore, DependOnAccountEventStore}; -use kernel::interfaces::modify::{ - DependOnAuthAccountModifier, DependOnAuthHostModifier, DependOnEventModifier, -}; -use kernel::interfaces::query::{ - DependOnAuthAccountQuery, DependOnAuthHostQuery, DependOnEventQuery, -}; -use kernel::interfaces::read_model::{AccountReadModel, DependOnAccountReadModel}; -use kernel::interfaces::signal::Signal; use kernel::prelude::entity::{ - Account, AccountEvent, AccountId, AccountIsBot, AccountName, AccountPrivateKey, - AccountPublicKey, AuthAccountId, CommandEnvelope, EventId, Nanoid, + Account, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, AuthAccountId, Nanoid, }; use kernel::KernelError; use serde_json; use std::future::Future; -pub trait GetAccountService: - 'static - + Sync - + Send - + DependOnAccountReadModel - + DependOnAuthAccountQuery - + DependOnAuthAccountModifier - + DependOnAuthHostQuery - + DependOnAuthHostModifier - + DependOnEventModifier - + DependOnEventQuery -{ +pub trait GetAccountUseCase: 'static + Sync + Send + DependOnAccountQueryProcessor { fn get_all_accounts( &self, - signal: &impl Signal, - account: AuthAccountInfo, + auth_account_id: &AuthAccountId, Pagination { direction, cursor, limit, }: Pagination, - ) -> impl Future>, KernelError>> { + ) -> impl Future>, KernelError>> + Send + { async move { - let auth_account = get_auth_account(self, signal, account).await?; let mut transaction = self.database_connection().begin_transaction().await?; let accounts = self - .account_read_model() - .find_by_auth_id(&mut transaction, auth_account.id()) + .account_query_processor() + .find_by_auth_id(&mut transaction, auth_account_id) .await?; let cursor = if let Some(cursor) = cursor { let id: Nanoid = Nanoid::new(cursor); - self.account_read_model() + self.account_query_processor() .find_by_nanoid(&mut transaction, &id) .await? } else { @@ -68,17 +47,15 @@ pub trait GetAccountService: fn get_account_by_id( &self, - signal: &impl Signal, - auth_info: AuthAccountInfo, + auth_account_id: &AuthAccountId, account_id: String, - ) -> impl Future> { + ) -> impl Future> + Send { async move { - let auth_account = get_auth_account(self, signal, auth_info).await?; let mut transaction = self.database_connection().begin_transaction().await?; let nanoid = Nanoid::::new(account_id); let account = self - .account_read_model() + .account_query_processor() .find_by_nanoid(&mut transaction, &nanoid) .await? .ok_or_else(|| { @@ -88,9 +65,8 @@ pub trait GetAccountService: )) })?; - let auth_account_id = auth_account.id(); let accounts = self - .account_read_model() + .account_query_processor() .find_by_auth_id(&mut transaction, auth_account_id) .await?; @@ -105,49 +81,25 @@ pub trait GetAccountService: } } -impl GetAccountService for T where - T: 'static - + DependOnAccountReadModel - + DependOnAuthAccountQuery - + DependOnAuthAccountModifier - + DependOnAuthHostQuery - + DependOnAuthHostModifier - + DependOnEventModifier - + DependOnEventQuery -{ -} +impl GetAccountUseCase for T where T: 'static + DependOnAccountQueryProcessor {} -pub trait CreateAccountService: +pub trait CreateAccountUseCase: 'static + Sync + Send - + DependOnAccountReadModel - + DependOnAccountEventStore - + DependOnAuthAccountQuery - + DependOnAuthAccountModifier - + DependOnAuthHostQuery - + DependOnAuthHostModifier - + DependOnEventModifier - + DependOnEventQuery + + DependOnAccountCommandProcessor + DependOnPasswordProvider + DependOnSigningKeyGenerator { - fn create_account( + fn create_account( &self, - signal: &S, - auth_info: AuthAccountInfo, + auth_account_id: AuthAccountId, name: String, is_bot: bool, - ) -> impl Future> - where - S: Signal + Signal + Send + Sync + 'static, - { + ) -> impl Future> + Send { async move { - let auth_account = get_auth_account(self, signal, auth_info).await?; let mut transaction = self.database_connection().begin_transaction().await?; - let account_id = AccountId::default(); - // Generate key pair let master_password = self.password_provider().get_password()?; let key_pair = self.signing_key_generator().generate(&master_password)?; @@ -162,44 +114,17 @@ pub trait CreateAccountService: let public_key = AccountPublicKey::new(key_pair.public_key_pem); let account_name = AccountName::new(name); let account_is_bot = AccountIsBot::new(is_bot); - let nanoid = Nanoid::::default(); - // Create command and persist event - let event = AccountEvent::Created { - name: account_name, - private_key, - public_key, - is_bot: account_is_bot, - nanoid, - }; - let command = CommandEnvelope::new( - EventId::from(account_id.clone()), - event.name(), - event, - Some(kernel::prelude::entity::KnownEventVersion::Nothing), - ); - - let event_envelope = self - .account_event_store() - .persist_and_transform(&mut transaction, command) - .await?; - - // Apply event to build entity - let mut account = None; - Account::apply(&mut account, event_envelope)?; - let account = account.ok_or_else(|| { - Report::new(KernelError::Internal) - .attach_printable("Failed to construct account from created event") - })?; - - // Update projection - self.account_read_model() - .create(&mut transaction, &account) - .await?; - - // Link auth account - self.account_read_model() - .link_auth_account(&mut transaction, &account_id, auth_account.id()) + let account = self + .account_command_processor() + .create( + &mut transaction, + account_name, + private_key, + public_key, + account_is_bot, + auth_account_id, + ) .await?; Ok(AccountDto::from(account)) @@ -207,99 +132,29 @@ pub trait CreateAccountService: } } -impl CreateAccountService for T where +impl CreateAccountUseCase for T where T: 'static - + DependOnAccountReadModel - + DependOnAccountEventStore - + DependOnAuthAccountQuery - + DependOnAuthAccountModifier - + DependOnAuthHostQuery - + DependOnAuthHostModifier - + DependOnEventModifier - + DependOnEventQuery + + DependOnAccountCommandProcessor + DependOnPasswordProvider + DependOnSigningKeyGenerator { } -pub trait UpdateAccountService: - 'static + Sync + Send + DependOnAccountReadModel + DependOnAccountEventStore +pub trait EditAccountUseCase: + 'static + Sync + Send + DependOnAccountCommandProcessor + DependOnAccountQueryProcessor { - fn update_account( + fn edit_account( &self, - account_id: AccountId, - ) -> impl Future> { - async move { - let mut transaction = self.database_connection().begin_transaction().await?; - let account = self - .account_read_model() - .find_by_id(&mut transaction, &account_id) - .await?; - if let Some(account) = account { - let event_id = EventId::from(account_id.clone()); - let events = self - .account_event_store() - .find_by_id(&mut transaction, &event_id, Some(account.version())) - .await?; - if events.is_empty() { - return Ok(()); - } - let mut account = Some(account); - for event in events { - Account::apply(&mut account, event)?; - } - if let Some(account) = account { - self.account_read_model() - .update(&mut transaction, &account) - .await?; - } else { - self.account_read_model() - .delete(&mut transaction, &account_id) - .await?; - } - Ok(()) - } else { - Err(Report::new(KernelError::Internal) - .attach_printable(format!("Failed to get target account: {account_id:?}"))) - } - } - } -} - -impl UpdateAccountService for T where - T: 'static + DependOnAccountReadModel + DependOnAccountEventStore -{ -} - -pub trait DeleteAccountService: - 'static - + Sync - + Send - + DependOnAccountReadModel - + DependOnAccountEventStore - + DependOnAuthAccountQuery - + DependOnAuthAccountModifier - + DependOnAuthHostQuery - + DependOnAuthHostModifier - + DependOnEventModifier - + DependOnEventQuery -{ - fn delete_account( - &self, - signal: &S, - auth_info: AuthAccountInfo, + auth_account_id: &AuthAccountId, account_id: String, - ) -> impl Future> - where - S: Signal + Signal + Send + Sync + 'static, - { + is_bot: bool, + ) -> impl Future> + Send { async move { - let auth_account = get_auth_account(self, signal, auth_info).await?; let mut transaction = self.database_connection().begin_transaction().await?; let nanoid = Nanoid::::new(account_id); let account = self - .account_read_model() + .account_query_processor() .find_by_nanoid(&mut transaction, &nanoid) .await? .ok_or_else(|| { @@ -309,10 +164,9 @@ pub trait DeleteAccountService: )) })?; - let auth_account_id = auth_account.id().clone(); let accounts = self - .account_read_model() - .find_by_auth_id(&mut transaction, &auth_account_id) + .account_query_processor() + .find_by_auth_id(&mut transaction, auth_account_id) .await?; let found = accounts.iter().any(|a| a.id() == account.id()); @@ -323,62 +177,39 @@ pub trait DeleteAccountService: let account_id = account.id().clone(); let current_version = account.version().clone(); - let delete_command = Account::delete(account_id.clone(), current_version); - - self.account_event_store() - .persist_and_transform(&mut transaction, delete_command) + self.account_command_processor() + .update( + &mut transaction, + account_id, + AccountIsBot::new(is_bot), + current_version, + ) .await?; - signal.emit(account_id).await?; Ok(()) } } } -impl DeleteAccountService for T where - T: 'static - + DependOnAccountReadModel - + DependOnAccountEventStore - + DependOnAuthAccountQuery - + DependOnAuthAccountModifier - + DependOnAuthHostQuery - + DependOnAuthHostModifier - + DependOnEventModifier - + DependOnEventQuery - + UpdateAccountService +impl EditAccountUseCase for T where + T: 'static + DependOnAccountCommandProcessor + DependOnAccountQueryProcessor { } -pub trait EditAccountService: - 'static - + Sync - + Send - + DependOnAccountReadModel - + DependOnAccountEventStore - + DependOnAuthAccountQuery - + DependOnAuthAccountModifier - + DependOnAuthHostQuery - + DependOnAuthHostModifier - + DependOnEventModifier - + DependOnEventQuery +pub trait DeleteAccountUseCase: + 'static + Sync + Send + DependOnAccountCommandProcessor + DependOnAccountQueryProcessor { - fn edit_account( + fn delete_account( &self, - signal: &S, - auth_info: AuthAccountInfo, + auth_account_id: &AuthAccountId, account_id: String, - is_bot: bool, - ) -> impl Future> - where - S: Signal + Signal + Send + Sync + 'static, - { + ) -> impl Future> + Send { async move { - let auth_account = get_auth_account(self, signal, auth_info).await?; let mut transaction = self.database_connection().begin_transaction().await?; let nanoid = Nanoid::::new(account_id); let account = self - .account_read_model() + .account_query_processor() .find_by_nanoid(&mut transaction, &nanoid) .await? .ok_or_else(|| { @@ -388,10 +219,9 @@ pub trait EditAccountService: )) })?; - let auth_account_id = auth_account.id().clone(); let accounts = self - .account_read_model() - .find_by_auth_id(&mut transaction, &auth_account_id) + .account_query_processor() + .find_by_auth_id(&mut transaction, auth_account_id) .await?; let found = accounts.iter().any(|a| a.id() == account.id()); @@ -402,32 +232,16 @@ pub trait EditAccountService: let account_id = account.id().clone(); let current_version = account.version().clone(); - let update_command = Account::update( - account_id.clone(), - AccountIsBot::new(is_bot), - current_version, - ); - - self.account_event_store() - .persist_and_transform(&mut transaction, update_command) + self.account_command_processor() + .delete(&mut transaction, account_id, current_version) .await?; - signal.emit(account_id).await?; Ok(()) } } } -impl EditAccountService for T where - T: 'static - + DependOnAccountReadModel - + DependOnAccountEventStore - + DependOnAuthAccountQuery - + DependOnAuthAccountModifier - + DependOnAuthHostQuery - + DependOnAuthHostModifier - + DependOnEventModifier - + DependOnEventQuery - + UpdateAccountService +impl DeleteAccountUseCase for T where + T: 'static + DependOnAccountCommandProcessor + DependOnAccountQueryProcessor { } diff --git a/application/src/service/auth_account.rs b/application/src/service/auth_account.rs index 03f7747..c0ee129 100644 --- a/application/src/service/auth_account.rs +++ b/application/src/service/auth_account.rs @@ -1,19 +1,14 @@ -use crate::transfer::auth_account::AuthAccountInfo; use error_stack::Report; use kernel::interfaces::database::{DatabaseConnection, DependOnDatabaseConnection}; use kernel::interfaces::event::EventApplier; use kernel::interfaces::modify::{ - AuthAccountModifier, AuthHostModifier, DependOnAuthAccountModifier, DependOnAuthHostModifier, - DependOnEventModifier, EventModifier, + AuthAccountModifier, DependOnAuthAccountModifier, DependOnAuthHostModifier, }; use kernel::interfaces::query::{ - AuthAccountQuery, AuthHostQuery, DependOnAuthAccountQuery, DependOnAuthHostQuery, - DependOnEventQuery, EventQuery, -}; -use kernel::interfaces::signal::Signal; -use kernel::prelude::entity::{ - AuthAccount, AuthAccountClientId, AuthAccountId, AuthHost, AuthHostId, AuthHostUrl, EventId, + AuthAccountQuery, DependOnAuthAccountQuery, DependOnAuthHostQuery, DependOnEventQuery, + EventQuery, }; +use kernel::prelude::entity::{AuthAccount, AuthAccountId, EventId}; use kernel::KernelError; use std::future::Future; @@ -26,7 +21,6 @@ pub trait UpdateAuthAccount: + DependOnAuthHostModifier + DependOnEventQuery { - /// Update the auth account from events fn update_auth_account( &self, auth_account_id: AuthAccountId, @@ -81,71 +75,3 @@ impl UpdateAuthAccount for T where + DependOnEventQuery { } - -/// Get an auth account by the host URL and client ID. -/// If the auth account does not exist, it will be created. -pub(crate) async fn get_auth_account< - S: 'static - + DependOnDatabaseConnection - + DependOnAuthAccountQuery - + DependOnAuthAccountModifier - + DependOnAuthHostQuery - + DependOnAuthHostModifier - + DependOnEventModifier - + DependOnEventQuery - + ?Sized, ->( - service: &S, - signal: &impl Signal, - AuthAccountInfo { - host_url: host, - client_id, - }: AuthAccountInfo, -) -> error_stack::Result { - let client_id = AuthAccountClientId::new(client_id); - let mut transaction = service.database_connection().begin_transaction().await?; - let auth_account = service - .auth_account_query() - .find_by_client_id(&mut transaction, &client_id) - .await?; - let auth_account = if let Some(auth_account) = auth_account { - auth_account - } else { - let url = AuthHostUrl::new(host); - let auth_host = service - .auth_host_query() - .find_by_url(&mut transaction, &url) - .await?; - let auth_host = if let Some(auth_host) = auth_host { - auth_host - } else { - let auth_host = AuthHost::new(AuthHostId::default(), url); - service - .auth_host_modifier() - .create(&mut transaction, &auth_host) - .await?; - auth_host - }; - let host_id = auth_host.into_destruct().id; - let auth_account_id = AuthAccountId::default(); - let create_command = AuthAccount::create(auth_account_id.clone(), host_id, client_id); - let create_event = service - .event_modifier() - .persist_and_transform(&mut transaction, create_command) - .await?; - signal.emit(auth_account_id.clone()).await?; - let mut auth_account = None; - AuthAccount::apply(&mut auth_account, create_event)?; - if let Some(auth_account) = auth_account { - service - .auth_account_modifier() - .create(&mut transaction, &auth_account) - .await?; - auth_account - } else { - return Err(Report::new(KernelError::Internal) - .attach_printable("Failed to create auth account")); - } - }; - Ok(auth_account) -} diff --git a/application/src/transfer.rs b/application/src/transfer.rs index bac1dc3..971413f 100644 --- a/application/src/transfer.rs +++ b/application/src/transfer.rs @@ -1,3 +1,2 @@ pub mod account; -pub mod auth_account; pub mod pagination; diff --git a/application/src/transfer/auth_account.rs b/application/src/transfer/auth_account.rs deleted file mode 100644 index 7fa9d9d..0000000 --- a/application/src/transfer/auth_account.rs +++ /dev/null @@ -1,5 +0,0 @@ -#[derive(Debug)] -pub struct AuthAccountInfo { - pub host_url: String, - pub client_id: String, -} diff --git a/driver/src/database/postgres/account_event_store.rs b/driver/src/database/postgres/account_event_store.rs index cf778ed..a8598b2 100644 --- a/driver/src/database/postgres/account_event_store.rs +++ b/driver/src/database/postgres/account_event_store.rs @@ -203,7 +203,7 @@ mod test { use kernel::interfaces::event_store::{AccountEventStore, DependOnAccountEventStore}; use kernel::prelude::entity::{ Account, AccountEvent, AccountId, AccountIsBot, AccountName, AccountPrivateKey, - AccountPublicKey, CommandEnvelope, EventId, KnownEventVersion, Nanoid, + AccountPublicKey, AuthAccountId, CommandEnvelope, EventId, KnownEventVersion, Nanoid, }; use uuid::Uuid; @@ -214,6 +214,7 @@ mod test { public_key: AccountPublicKey::new("test"), is_bot: AccountIsBot::new(false), nanoid: Nanoid::default(), + auth_account_id: AuthAccountId::new(uuid::Uuid::now_v7()), }; CommandEnvelope::new( EventId::from(account_id), @@ -350,7 +351,7 @@ mod test { use kernel::interfaces::event_store::{AccountEventStore, DependOnAccountEventStore}; use kernel::prelude::entity::{ Account, AccountEvent, AccountId, AccountIsBot, AccountName, AccountPrivateKey, - AccountPublicKey, CommandEnvelope, EventId, KnownEventVersion, Nanoid, + AccountPublicKey, AuthAccountId, CommandEnvelope, EventId, KnownEventVersion, Nanoid, }; use uuid::Uuid; @@ -361,6 +362,7 @@ mod test { public_key: AccountPublicKey::new("test"), is_bot: AccountIsBot::new(false), nanoid: Nanoid::default(), + auth_account_id: AuthAccountId::new(uuid::Uuid::now_v7()), }; CommandEnvelope::new( EventId::from(account_id), diff --git a/driver/src/database/postgres/event.rs b/driver/src/database/postgres/event.rs index c90b47a..3f57cad 100644 --- a/driver/src/database/postgres/event.rs +++ b/driver/src/database/postgres/event.rs @@ -221,7 +221,7 @@ mod test { use kernel::interfaces::query::{DependOnEventQuery, EventQuery}; use kernel::prelude::entity::{ Account, AccountEvent, AccountId, AccountIsBot, AccountName, AccountPrivateKey, - AccountPublicKey, CommandEnvelope, EventId, KnownEventVersion, Nanoid, + AccountPublicKey, AuthAccountId, CommandEnvelope, EventId, KnownEventVersion, Nanoid, }; fn create_account_command(account_id: AccountId) -> CommandEnvelope { @@ -231,6 +231,7 @@ mod test { public_key: AccountPublicKey::new("test"), is_bot: AccountIsBot::new(false), nanoid: Nanoid::default(), + auth_account_id: AuthAccountId::new(uuid::Uuid::now_v7()), }; CommandEnvelope::new( EventId::from(account_id), @@ -367,7 +368,7 @@ mod test { use kernel::interfaces::query::{DependOnEventQuery, EventQuery}; use kernel::prelude::entity::{ Account, AccountEvent, AccountId, AccountIsBot, AccountName, AccountPrivateKey, - AccountPublicKey, CommandEnvelope, EventId, KnownEventVersion, Nanoid, + AccountPublicKey, AuthAccountId, CommandEnvelope, EventId, KnownEventVersion, Nanoid, }; fn create_account_command(account_id: AccountId) -> CommandEnvelope { @@ -377,6 +378,7 @@ mod test { public_key: AccountPublicKey::new("test"), is_bot: AccountIsBot::new(false), nanoid: Nanoid::default(), + auth_account_id: AuthAccountId::new(uuid::Uuid::now_v7()), }; CommandEnvelope::new( EventId::from(account_id), diff --git a/kernel/src/database.rs b/kernel/src/database.rs index e9dc45c..af71727 100644 --- a/kernel/src/database.rs +++ b/kernel/src/database.rs @@ -4,7 +4,7 @@ use std::future::Future; /// Databaseのトランザクション処理を示すトレイト /// /// 現状は何もないが、将来的にトランザクション時に使える機能を示す可能性を考えて用意している -pub trait Executor {} +pub trait Executor: Send {} pub trait DatabaseConnection: Sync + Send + 'static { type Executor: Executor; diff --git a/kernel/src/entity/account.rs b/kernel/src/entity/account.rs index 1ffcf0a..9809947 100644 --- a/kernel/src/entity/account.rs +++ b/kernel/src/entity/account.rs @@ -5,8 +5,8 @@ use serde::Serialize; use vodca::{Nameln, Newln, References}; use crate::entity::{ - CommandEnvelope, CreatedAt, DeletedAt, EventEnvelope, EventId, EventVersion, KnownEventVersion, - Nanoid, + AuthAccountId, CommandEnvelope, CreatedAt, DeletedAt, EventEnvelope, EventId, EventVersion, + KnownEventVersion, Nanoid, }; use crate::event::EventApplier; use crate::KernelError; @@ -48,6 +48,7 @@ pub enum AccountEvent { public_key: AccountPublicKey, is_bot: AccountIsBot, nanoid: Nanoid, + auth_account_id: AuthAccountId, }, Updated { is_bot: AccountIsBot, @@ -56,6 +57,31 @@ pub enum AccountEvent { } impl Account { + pub fn create( + id: AccountId, + name: AccountName, + private_key: AccountPrivateKey, + public_key: AccountPublicKey, + is_bot: AccountIsBot, + nanoid: Nanoid, + auth_account_id: AuthAccountId, + ) -> CommandEnvelope { + let event = AccountEvent::Created { + name, + private_key, + public_key, + is_bot, + nanoid, + auth_account_id, + }; + CommandEnvelope::new( + EventId::from(id), + event.name(), + event, + Some(KnownEventVersion::Nothing), + ) + } + pub fn update( id: AccountId, is_bot: AccountIsBot, @@ -111,6 +137,7 @@ impl EventApplier for Account { public_key, is_bot, nanoid: nano_id, + auth_account_id: _, } => { if let Some(entity) = entity { return Err(Report::new(KernelError::Internal) @@ -159,7 +186,7 @@ impl EventApplier for Account { mod test { use crate::entity::{ Account, AccountEvent, AccountId, AccountIsBot, AccountName, AccountPrivateKey, - AccountPublicKey, CreatedAt, EventEnvelope, EventId, EventVersion, Nanoid, + AccountPublicKey, AuthAccountId, CreatedAt, EventEnvelope, EventId, EventVersion, Nanoid, }; use crate::event::EventApplier; use crate::KernelError; @@ -179,6 +206,7 @@ mod test { public_key: public_key.clone(), is_bot: is_bot.clone(), nanoid: nano_id.clone(), + auth_account_id: AuthAccountId::new(Uuid::now_v7()), }; let envelope = EventEnvelope::new( EventId::from(id.clone()), @@ -222,6 +250,7 @@ mod test { public_key: public_key.clone(), is_bot: is_bot.clone(), nanoid: nano_id.clone(), + auth_account_id: AuthAccountId::new(Uuid::now_v7()), }; let envelope = EventEnvelope::new( EventId::from(id.clone()), diff --git a/kernel/src/signal.rs b/kernel/src/signal.rs index 0c9294f..718f794 100644 --- a/kernel/src/signal.rs +++ b/kernel/src/signal.rs @@ -2,5 +2,8 @@ use crate::KernelError; use std::future::Future; pub trait Signal { - fn emit(&self, signal_id: ID) -> impl Future>; + fn emit( + &self, + signal_id: ID, + ) -> impl Future> + Send; } diff --git a/server/src/applier.rs b/server/src/applier.rs index 79292f1..a585fe0 100644 --- a/server/src/applier.rs +++ b/server/src/applier.rs @@ -8,7 +8,7 @@ use std::sync::Arc; mod account_applier; mod auth_account_applier; -pub(crate) struct ApplierContainer { +pub struct ApplierContainer { account_applier: AccountApplier, auth_account_applier: AuthAccountApplier, } diff --git a/server/src/applier/account_applier.rs b/server/src/applier/account_applier.rs index a4af345..c249248 100644 --- a/server/src/applier/account_applier.rs +++ b/server/src/applier/account_applier.rs @@ -1,8 +1,11 @@ use crate::handler::Handler; -use application::service::account::UpdateAccountService; use error_stack::ResultExt; +use kernel::interfaces::database::{DatabaseConnection, DependOnDatabaseConnection}; +use kernel::interfaces::event::EventApplier; +use kernel::interfaces::event_store::{AccountEventStore, DependOnAccountEventStore}; +use kernel::interfaces::read_model::{AccountReadModel, DependOnAccountReadModel}; use kernel::interfaces::signal::Signal; -use kernel::prelude::entity::AccountId; +use kernel::prelude::entity::{Account, AccountEvent, AccountId, EventId}; use kernel::KernelError; use rikka_mq::config::MQConfig; use rikka_mq::define::redis::mq::RedisMessageQueue; @@ -23,11 +26,87 @@ impl AccountApplier { MQConfig::default(), Uuid::new_v4, |handler: Arc, id: AccountId| async move { - handler - .as_ref() - .update_account(id) + let mut tx = handler + .database_connection() + .begin_transaction() .await - .map_err(|e| ErrorOperation::Delay(format!("{:?}", e))) + .map_err(|e| ErrorOperation::Delay(format!("{:?}", e)))?; + let event_id = EventId::from(id.clone()); + + // 既存Projection取得 + let existing = handler + .account_read_model() + .find_by_id(&mut tx, &id) + .await + .map_err(|e| ErrorOperation::Delay(format!("{:?}", e)))?; + let since_version = existing.as_ref().map(|a| a.version().clone()); + + // 新規イベント取得 + let events = handler + .account_event_store() + .find_by_id(&mut tx, &event_id, since_version.as_ref()) + .await + .map_err(|e| ErrorOperation::Delay(format!("{:?}", e)))?; + if events.is_empty() { + return Ok(()); + } + + // Created イベントから auth_account_id 抽出 + let mut auth_account_id_for_link = None; + for event in &events { + if let AccountEvent::Created { + auth_account_id, .. + } = &event.event + { + auth_account_id_for_link = Some(auth_account_id.clone()); + } + } + + // イベント適用 + let mut entity = existing; + for event in events { + Account::apply(&mut entity, event) + .map_err(|e| ErrorOperation::Delay(format!("{:?}", e)))?; + } + + // Projection更新 + match (&entity, &since_version) { + (Some(account), None) => { + handler + .account_read_model() + .create(&mut tx, account) + .await + .map_err(|e| ErrorOperation::Delay(format!("{:?}", e)))?; + if let Some(auth_id) = auth_account_id_for_link { + handler + .account_read_model() + .link_auth_account(&mut tx, &id, &auth_id) + .await + .map_err(|e| ErrorOperation::Delay(format!("{:?}", e)))?; + } + } + (Some(account), Some(_)) => { + handler + .account_read_model() + .update(&mut tx, account) + .await + .map_err(|e| ErrorOperation::Delay(format!("{:?}", e)))?; + } + (None, Some(_)) => { + handler + .account_read_model() + .delete(&mut tx, &id) + .await + .map_err(|e| ErrorOperation::Delay(format!("{:?}", e)))?; + } + (None, None) => { + tracing::warn!( + "Account applier: entity is None with no prior projection for id {:?}", + id + ); + } + } + Ok(()) }, ); AccountApplier(queue) diff --git a/server/src/handler.rs b/server/src/handler.rs index 7799bc1..035ae73 100644 --- a/server/src/handler.rs +++ b/server/src/handler.rs @@ -1,4 +1,5 @@ use crate::applier::ApplierContainer; +use adapter::processor::account::DependOnAccountSignal; use driver::crypto::{ Argon2Encryptor, FilePasswordProvider, Rsa2048RawGenerator, Rsa2048Signer, Rsa2048Verifier, }; @@ -7,6 +8,7 @@ use kernel::interfaces::crypto::{ DependOnKeyEncryptor, DependOnPasswordProvider, DependOnRawKeyGenerator, DependOnSignatureVerifier, DependOnSigner, }; +use kernel::interfaces::database::DependOnDatabaseConnection; use kernel::KernelError; use std::sync::Arc; use vodca::References; @@ -28,6 +30,65 @@ impl AppModule { } } +// --- DependOn* implementations for AppModule (delegate to handler/applier_container) --- + +impl kernel::interfaces::database::DependOnDatabaseConnection for AppModule { + type DatabaseConnection = PostgresDatabase; + fn database_connection(&self) -> &Self::DatabaseConnection { + self.handler.as_ref().database_connection() + } +} + +impl kernel::interfaces::read_model::DependOnAccountReadModel for AppModule { + type AccountReadModel = ::AccountReadModel; + fn account_read_model(&self) -> &Self::AccountReadModel { + kernel::interfaces::read_model::DependOnAccountReadModel::account_read_model( + self.handler.as_ref().database_connection(), + ) + } +} + +impl kernel::interfaces::event_store::DependOnAccountEventStore for AppModule { + type AccountEventStore = ::AccountEventStore; + fn account_event_store(&self) -> &Self::AccountEventStore { + kernel::interfaces::event_store::DependOnAccountEventStore::account_event_store( + self.handler.as_ref().database_connection(), + ) + } +} + +impl DependOnPasswordProvider for AppModule { + type PasswordProvider = FilePasswordProvider; + fn password_provider(&self) -> &Self::PasswordProvider { + self.handler.as_ref().password_provider() + } +} + +impl DependOnRawKeyGenerator for AppModule { + type RawKeyGenerator = Rsa2048RawGenerator; + fn raw_key_generator(&self) -> &Self::RawKeyGenerator { + self.handler.as_ref().raw_key_generator() + } +} + +impl DependOnKeyEncryptor for AppModule { + type KeyEncryptor = Argon2Encryptor; + fn key_encryptor(&self) -> &Self::KeyEncryptor { + self.handler.as_ref().key_encryptor() + } +} + +impl DependOnAccountSignal for AppModule { + type AccountSignal = ApplierContainer; + fn account_signal(&self) -> &Self::AccountSignal { + &self.applier_container + } +} + +// Note: DependOnSigningKeyGenerator, DependOnAccountCommandProcessor, +// DependOnAccountQueryProcessor, and all UseCase traits are provided +// automatically via blanket impls in adapter. + #[derive(References)] pub struct Handler { pgpool: PostgresDatabase, @@ -84,9 +145,6 @@ impl DependOnKeyEncryptor for Handler { } } -// Note: DependOnSigningKeyGenerator is provided automatically via blanket impl in adapter -// when Handler implements DependOnRawKeyGenerator + DependOnKeyEncryptor - impl DependOnSigner for Handler { type Signer = Rsa2048Signer; fn signer(&self) -> &Self::Signer { diff --git a/server/src/keycloak.rs b/server/src/keycloak.rs index d080d67..11adad3 100644 --- a/server/src/keycloak.rs +++ b/server/src/keycloak.rs @@ -1,7 +1,22 @@ -use application::transfer::auth_account::AuthAccountInfo; +use crate::handler::Handler; use axum_keycloak_auth::decode::KeycloakToken; use axum_keycloak_auth::instance::{KeycloakAuthInstance, KeycloakConfig}; use axum_keycloak_auth::role::Role; +use error_stack::Report; +use kernel::interfaces::database::{DatabaseConnection, DependOnDatabaseConnection}; +use kernel::interfaces::event::EventApplier; +use kernel::interfaces::modify::{ + AuthAccountModifier, AuthHostModifier, DependOnAuthAccountModifier, DependOnAuthHostModifier, + DependOnEventModifier, EventModifier, +}; +use kernel::interfaces::query::{ + AuthAccountQuery, AuthHostQuery, DependOnAuthAccountQuery, DependOnAuthHostQuery, +}; +use kernel::interfaces::signal::Signal; +use kernel::prelude::entity::{ + AuthAccount, AuthAccountClientId, AuthAccountId, AuthHost, AuthHostId, AuthHostUrl, +}; +use kernel::KernelError; use std::sync::LazyLock; const KEYCLOAK_SERVER_KEY: &str = "KEYCLOAK_SERVER"; @@ -38,13 +53,57 @@ impl From> for KeycloakAuthAccount { } } -impl From for AuthAccountInfo { - fn from(val: KeycloakAuthAccount) -> Self { - AuthAccountInfo { - host_url: val.host_url, - client_id: val.client_id, +pub async fn resolve_auth_account_id( + handler: &Handler, + signal: &impl Signal, + auth_info: KeycloakAuthAccount, +) -> error_stack::Result { + let client_id = AuthAccountClientId::new(auth_info.client_id); + let mut transaction = handler.database_connection().begin_transaction().await?; + let auth_account = handler + .auth_account_query() + .find_by_client_id(&mut transaction, &client_id) + .await?; + let auth_account = if let Some(auth_account) = auth_account { + auth_account + } else { + let url = AuthHostUrl::new(auth_info.host_url); + let auth_host = handler + .auth_host_query() + .find_by_url(&mut transaction, &url) + .await?; + let auth_host = if let Some(auth_host) = auth_host { + auth_host + } else { + let auth_host = AuthHost::new(AuthHostId::default(), url); + handler + .auth_host_modifier() + .create(&mut transaction, &auth_host) + .await?; + auth_host + }; + let host_id = auth_host.into_destruct().id; + let auth_account_id = AuthAccountId::default(); + let create_command = AuthAccount::create(auth_account_id.clone(), host_id, client_id); + let create_event = handler + .event_modifier() + .persist_and_transform(&mut transaction, create_command) + .await?; + signal.emit(auth_account_id.clone()).await?; + let mut auth_account = None; + AuthAccount::apply(&mut auth_account, create_event)?; + if let Some(auth_account) = auth_account { + handler + .auth_account_modifier() + .create(&mut transaction, &auth_account) + .await?; + auth_account + } else { + return Err(Report::new(KernelError::Internal) + .attach_printable("Failed to create auth account")); } - } + }; + Ok(auth_account.id().clone()) } #[macro_export] diff --git a/server/src/route/account.rs b/server/src/route/account.rs index 828f011..5536be1 100644 --- a/server/src/route/account.rs +++ b/server/src/route/account.rs @@ -1,10 +1,10 @@ use crate::error::ErrorStatus; use crate::expect_role; use crate::handler::AppModule; -use crate::keycloak::KeycloakAuthAccount; +use crate::keycloak::{resolve_auth_account_id, KeycloakAuthAccount}; use crate::route::DirectionConverter; use application::service::account::{ - CreateAccountService, DeleteAccountService, EditAccountService, GetAccountService, + CreateAccountUseCase, DeleteAccountUseCase, EditAccountUseCase, GetAccountUseCase, }; use application::transfer::pagination::Pagination; use axum::extract::{Path, Query, State}; @@ -71,15 +71,18 @@ async fn get_accounts( ) -> Result, ErrorStatus> { expect_role!(&token, uri, method); let auth_info = KeycloakAuthAccount::from(token); + let auth_account_id = resolve_auth_account_id( + module.handler(), + module.applier_container().deref(), + auth_info, + ) + .await + .map_err(ErrorStatus::from)?; + let direction = direction.convert_to_direction()?; let pagination = Pagination::new(limit, cursor, direction); let result = module - .handler() - .get_all_accounts( - module.applier_container().deref(), - auth_info.into(), - pagination, - ) + .get_all_accounts(&auth_account_id, pagination) .await .map_err(ErrorStatus::from)? .ok_or(ErrorStatus::from(StatusCode::NOT_FOUND))?; @@ -121,15 +124,16 @@ async fn create_account( ))); } - // サービス層でのアカウント作成処理の呼び出し + let auth_account_id = resolve_auth_account_id( + module.handler(), + module.applier_container().deref(), + auth_info, + ) + .await + .map_err(ErrorStatus::from)?; + let account = module - .handler() - .create_account( - module.applier_container().deref(), - auth_info.into(), - request.name, - request.is_bot, - ) + .create_account(auth_account_id, request.name, request.is_bot) .await .map_err(ErrorStatus::from)?; @@ -154,7 +158,6 @@ async fn get_account_by_id( expect_role!(&token, uri, method); let auth_info = KeycloakAuthAccount::from(token); - // IDの検証 if id.trim().is_empty() { return Err(ErrorStatus::from(( StatusCode::BAD_REQUEST, @@ -162,10 +165,16 @@ async fn get_account_by_id( ))); } - // サービス層での特定アカウント取得処理 + let auth_account_id = resolve_auth_account_id( + module.handler(), + module.applier_container().deref(), + auth_info, + ) + .await + .map_err(ErrorStatus::from)?; + let account = module - .handler() - .get_account_by_id(module.applier_container().deref(), auth_info.into(), id) + .get_account_by_id(&auth_account_id, id) .await .map_err(ErrorStatus::from)?; @@ -191,7 +200,6 @@ async fn update_account_by_id( expect_role!(&token, uri, method); let auth_info = KeycloakAuthAccount::from(token); - // IDの検証 if id.trim().is_empty() { return Err(ErrorStatus::from(( StatusCode::BAD_REQUEST, @@ -199,15 +207,16 @@ async fn update_account_by_id( ))); } - // サービス層でのアカウント更新処理 + let auth_account_id = resolve_auth_account_id( + module.handler(), + module.applier_container().deref(), + auth_info, + ) + .await + .map_err(ErrorStatus::from)?; + module - .handler() - .edit_account( - module.applier_container().deref(), - auth_info.into(), - id, - request.is_bot, - ) + .edit_account(&auth_account_id, id, request.is_bot) .await .map_err(ErrorStatus::from)?; @@ -224,7 +233,6 @@ async fn delete_account_by_id( expect_role!(&token, uri, method); let auth_info = KeycloakAuthAccount::from(token); - // IDの検証 if id.trim().is_empty() { return Err(ErrorStatus::from(( StatusCode::BAD_REQUEST, @@ -232,10 +240,16 @@ async fn delete_account_by_id( ))); } - // サービス層でのアカウント削除処理 + let auth_account_id = resolve_auth_account_id( + module.handler(), + module.applier_container().deref(), + auth_info, + ) + .await + .map_err(ErrorStatus::from)?; + module - .handler() - .delete_account(module.applier_container().deref(), auth_info.into(), id) + .delete_account(&auth_account_id, id) .await .map_err(ErrorStatus::from)?; From a90d265ed361a76a0e379aad7c5b6eb0fa8f64e8 Mon Sep 17 00:00:00 2001 From: turtton Date: Mon, 9 Mar 2026 17:45:11 +0900 Subject: [PATCH 48/59] :hammer: Update lock file --- Cargo.lock | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.lock b/Cargo.lock index 792bda3..495f314 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,6 +8,7 @@ version = "0.1.0" dependencies = [ "error-stack", "kernel", + "tracing", "zeroize", ] From 90d104084f3a985bedccd827fc7881606922c7d6 Mon Sep 17 00:00:00 2001 From: turtton Date: Mon, 9 Mar 2026 19:05:44 +0900 Subject: [PATCH 49/59] :recycle: Apply CQRS pattern to AuthAccount and remove generic Event interfaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit AuthAccountにAccount同様のCQRSパターンを適用: - AuthAccountEventStore / AuthAccountReadModel を kernel に追加 - AuthAccountCommandProcessor / QueryProcessor を adapter に追加 - CommandProcessor内でprojection書き込みも完結させ、serverからReadModelの直接参照を除去 - 汎用EventModifier/EventQuery/AuthAccountQuery/AuthAccountModifierを削除 - auth_account_eventsテーブルのマイグレーション追加 --- adapter/src/processor.rs | 1 + adapter/src/processor/auth_account.rs | 187 ++++++++ application/src/service/auth_account.rs | 30 +- driver/src/database/postgres.rs | 2 +- driver/src/database/postgres/auth_account.rs | 72 +-- .../postgres/auth_account_event_store.rs | 390 ++++++++++++++++ driver/src/database/postgres/event.rs | 439 ------------------ kernel/src/event_store.rs | 2 + kernel/src/event_store/auth_account.rs | 44 ++ kernel/src/lib.rs | 40 +- kernel/src/modify.rs | 7 +- kernel/src/modify/auth_account.rs | 34 -- kernel/src/modify/event.rs | 29 -- kernel/src/query.rs | 7 +- kernel/src/query/auth_account.rs | 28 -- kernel/src/query/event.rs | 24 - kernel/src/read_model.rs | 2 + kernel/src/read_model/auth_account.rs | 48 ++ .../20260309000000_auth_account_events.sql | 8 + server/src/handler.rs | 45 ++ server/src/keycloak.rs | 62 +-- server/src/route/account.rs | 51 +- 22 files changed, 811 insertions(+), 741 deletions(-) create mode 100644 adapter/src/processor/auth_account.rs create mode 100644 driver/src/database/postgres/auth_account_event_store.rs delete mode 100644 driver/src/database/postgres/event.rs create mode 100644 kernel/src/event_store/auth_account.rs delete mode 100644 kernel/src/modify/auth_account.rs delete mode 100644 kernel/src/modify/event.rs delete mode 100644 kernel/src/query/auth_account.rs delete mode 100644 kernel/src/query/event.rs create mode 100644 kernel/src/read_model/auth_account.rs create mode 100644 migrations/20260309000000_auth_account_events.sql diff --git a/adapter/src/processor.rs b/adapter/src/processor.rs index b0edc6c..a3f9ff7 100644 --- a/adapter/src/processor.rs +++ b/adapter/src/processor.rs @@ -1 +1,2 @@ pub mod account; +pub mod auth_account; diff --git a/adapter/src/processor/auth_account.rs b/adapter/src/processor/auth_account.rs new file mode 100644 index 0000000..a7bbb41 --- /dev/null +++ b/adapter/src/processor/auth_account.rs @@ -0,0 +1,187 @@ +use error_stack::Report; +use kernel::interfaces::database::{DatabaseConnection, DependOnDatabaseConnection, Executor}; +use kernel::interfaces::event::EventApplier; +use kernel::interfaces::event_store::{AuthAccountEventStore, DependOnAuthAccountEventStore}; +use kernel::interfaces::read_model::{AuthAccountReadModel, DependOnAuthAccountReadModel}; +use kernel::interfaces::signal::Signal; +use kernel::prelude::entity::{AuthAccount, AuthAccountClientId, AuthAccountId, AuthHostId}; +use kernel::KernelError; +use std::future::Future; + +// --- Signal DI trait (adapter-specific) --- + +pub trait DependOnAuthAccountSignal: Send + Sync { + type AuthAccountSignal: Signal + Send + Sync + 'static; + fn auth_account_signal(&self) -> &Self::AuthAccountSignal; +} + +// --- AuthAccountCommandProcessor --- + +pub trait AuthAccountCommandProcessor: Send + Sync + 'static { + type Executor: Executor; + + fn create( + &self, + executor: &mut Self::Executor, + host: AuthHostId, + client_id: AuthAccountClientId, + ) -> impl Future> + Send; + + fn delete( + &self, + executor: &mut Self::Executor, + auth_account_id: AuthAccountId, + ) -> impl Future> + Send; +} + +impl AuthAccountCommandProcessor for T +where + T: DependOnAuthAccountEventStore + + DependOnAuthAccountReadModel + + DependOnAuthAccountSignal + + Send + + Sync + + 'static, +{ + type Executor = <::AuthAccountEventStore as AuthAccountEventStore>::Executor; + + async fn create( + &self, + executor: &mut Self::Executor, + host: AuthHostId, + client_id: AuthAccountClientId, + ) -> error_stack::Result { + let auth_account_id = AuthAccountId::default(); + let command = AuthAccount::create(auth_account_id.clone(), host, client_id); + + let event_envelope = self + .auth_account_event_store() + .persist_and_transform(executor, command) + .await?; + + let mut auth_account = None; + AuthAccount::apply(&mut auth_account, event_envelope)?; + let auth_account = auth_account.ok_or_else(|| { + Report::new(KernelError::Internal) + .attach_printable("Failed to construct auth account from created event") + })?; + + self.auth_account_read_model() + .create(executor, &auth_account) + .await?; + + if let Err(e) = self.auth_account_signal().emit(auth_account_id).await { + tracing::warn!("Failed to emit auth account signal: {:?}", e); + } + + Ok(auth_account) + } + + async fn delete( + &self, + executor: &mut Self::Executor, + auth_account_id: AuthAccountId, + ) -> error_stack::Result<(), KernelError> { + let command = AuthAccount::delete(auth_account_id.clone()); + + self.auth_account_event_store() + .persist_and_transform(executor, command) + .await?; + + self.auth_account_read_model() + .delete(executor, &auth_account_id) + .await?; + + if let Err(e) = self.auth_account_signal().emit(auth_account_id).await { + tracing::warn!("Failed to emit auth account signal: {:?}", e); + } + + Ok(()) + } +} + +pub trait DependOnAuthAccountCommandProcessor: DependOnDatabaseConnection + Send + Sync { + type AuthAccountCommandProcessor: AuthAccountCommandProcessor< + Executor = <::DatabaseConnection as DatabaseConnection>::Executor, + >; + fn auth_account_command_processor(&self) -> &Self::AuthAccountCommandProcessor; +} + +impl DependOnAuthAccountCommandProcessor for T +where + T: DependOnAuthAccountEventStore + + DependOnAuthAccountReadModel + + DependOnAuthAccountSignal + + DependOnDatabaseConnection + + Send + + Sync + + 'static, +{ + type AuthAccountCommandProcessor = Self; + fn auth_account_command_processor(&self) -> &Self::AuthAccountCommandProcessor { + self + } +} + +// --- AuthAccountQueryProcessor --- + +pub trait AuthAccountQueryProcessor: Send + Sync + 'static { + type Executor: Executor; + + fn find_by_id( + &self, + executor: &mut Self::Executor, + id: &AuthAccountId, + ) -> impl Future, KernelError>> + Send; + + fn find_by_client_id( + &self, + executor: &mut Self::Executor, + client_id: &AuthAccountClientId, + ) -> impl Future, KernelError>> + Send; +} + +impl AuthAccountQueryProcessor for T +where + T: DependOnAuthAccountReadModel + Send + Sync + 'static, +{ + type Executor = + <::AuthAccountReadModel as AuthAccountReadModel>::Executor; + + async fn find_by_id( + &self, + executor: &mut Self::Executor, + id: &AuthAccountId, + ) -> error_stack::Result, KernelError> { + self.auth_account_read_model() + .find_by_id(executor, id) + .await + } + + async fn find_by_client_id( + &self, + executor: &mut Self::Executor, + client_id: &AuthAccountClientId, + ) -> error_stack::Result, KernelError> { + self.auth_account_read_model() + .find_by_client_id(executor, client_id) + .await + } +} + +pub trait DependOnAuthAccountQueryProcessor: DependOnDatabaseConnection + Send + Sync { + type AuthAccountQueryProcessor: AuthAccountQueryProcessor< + Executor = <::DatabaseConnection as DatabaseConnection>::Executor, + >; + fn auth_account_query_processor(&self) -> &Self::AuthAccountQueryProcessor; +} + +impl DependOnAuthAccountQueryProcessor for T +where + T: DependOnAuthAccountReadModel + DependOnDatabaseConnection + Send + Sync + 'static, +{ + type AuthAccountQueryProcessor = Self; + fn auth_account_query_processor(&self) -> &Self::AuthAccountQueryProcessor { + self + } +} diff --git a/application/src/service/auth_account.rs b/application/src/service/auth_account.rs index c0ee129..2df6ee9 100644 --- a/application/src/service/auth_account.rs +++ b/application/src/service/auth_account.rs @@ -1,25 +1,14 @@ use error_stack::Report; use kernel::interfaces::database::{DatabaseConnection, DependOnDatabaseConnection}; use kernel::interfaces::event::EventApplier; -use kernel::interfaces::modify::{ - AuthAccountModifier, DependOnAuthAccountModifier, DependOnAuthHostModifier, -}; -use kernel::interfaces::query::{ - AuthAccountQuery, DependOnAuthAccountQuery, DependOnAuthHostQuery, DependOnEventQuery, - EventQuery, -}; +use kernel::interfaces::event_store::{AuthAccountEventStore, DependOnAuthAccountEventStore}; +use kernel::interfaces::read_model::{AuthAccountReadModel, DependOnAuthAccountReadModel}; use kernel::prelude::entity::{AuthAccount, AuthAccountId, EventId}; use kernel::KernelError; use std::future::Future; pub trait UpdateAuthAccount: - 'static - + DependOnDatabaseConnection - + DependOnAuthAccountQuery - + DependOnAuthAccountModifier - + DependOnAuthHostQuery - + DependOnAuthHostModifier - + DependOnEventQuery + 'static + DependOnDatabaseConnection + DependOnAuthAccountReadModel + DependOnAuthAccountEventStore { fn update_auth_account( &self, @@ -28,13 +17,13 @@ pub trait UpdateAuthAccount: async move { let mut transaction = self.database_connection().begin_transaction().await?; let auth_account = self - .auth_account_query() + .auth_account_read_model() .find_by_id(&mut transaction, &auth_account_id) .await?; if let Some(auth_account) = auth_account { let event_id = EventId::from(auth_account.id().clone()); let events = self - .event_query() + .auth_account_event_store() .find_by_id(&mut transaction, &event_id, Some(auth_account.version())) .await?; if events @@ -47,7 +36,7 @@ pub trait UpdateAuthAccount: AuthAccount::apply(&mut auth_account, event)?; } if let Some(auth_account) = auth_account { - self.auth_account_modifier() + self.auth_account_read_model() .update(&mut transaction, &auth_account) .await?; } else { @@ -68,10 +57,7 @@ pub trait UpdateAuthAccount: impl UpdateAuthAccount for T where T: 'static + DependOnDatabaseConnection - + DependOnAuthAccountQuery - + DependOnAuthAccountModifier - + DependOnAuthHostQuery - + DependOnAuthHostModifier - + DependOnEventQuery + + DependOnAuthAccountReadModel + + DependOnAuthAccountEventStore { } diff --git a/driver/src/database/postgres.rs b/driver/src/database/postgres.rs index d7dda70..877ade1 100644 --- a/driver/src/database/postgres.rs +++ b/driver/src/database/postgres.rs @@ -1,8 +1,8 @@ mod account; mod account_event_store; mod auth_account; +mod auth_account_event_store; mod auth_host; -mod event; mod follow; mod image; mod metadata; diff --git a/driver/src/database/postgres/auth_account.rs b/driver/src/database/postgres/auth_account.rs index e6831b4..3ce0bc4 100644 --- a/driver/src/database/postgres/auth_account.rs +++ b/driver/src/database/postgres/auth_account.rs @@ -1,7 +1,6 @@ use crate::database::{PostgresConnection, PostgresDatabase}; use crate::ConvertError; -use kernel::interfaces::modify::{AuthAccountModifier, DependOnAuthAccountModifier}; -use kernel::interfaces::query::{AuthAccountQuery, DependOnAuthAccountQuery}; +use kernel::interfaces::read_model::{AuthAccountReadModel, DependOnAuthAccountReadModel}; use kernel::prelude::entity::{ AuthAccount, AuthAccountClientId, AuthAccountId, AuthHostId, EventVersion, }; @@ -28,9 +27,9 @@ impl From for AuthAccount { } } -pub struct PostgresAuthAccountRepository; +pub struct PostgresAuthAccountReadModel; -impl AuthAccountQuery for PostgresAuthAccountRepository { +impl AuthAccountReadModel for PostgresAuthAccountReadModel { type Executor = PostgresConnection; async fn find_by_id( @@ -74,18 +73,6 @@ impl AuthAccountQuery for PostgresAuthAccountRepository { .convert_error() .map(|option| option.map(|row| row.into())) } -} - -impl DependOnAuthAccountQuery for PostgresDatabase { - type AuthAccountQuery = PostgresAuthAccountRepository; - - fn auth_account_query(&self) -> &Self::AuthAccountQuery { - &PostgresAuthAccountRepository - } -} - -impl AuthAccountModifier for PostgresAuthAccountRepository { - type Executor = PostgresConnection; async fn create( &self, @@ -152,11 +139,11 @@ impl AuthAccountModifier for PostgresAuthAccountRepository { } } -impl DependOnAuthAccountModifier for PostgresDatabase { - type AuthAccountModifier = PostgresAuthAccountRepository; +impl DependOnAuthAccountReadModel for PostgresDatabase { + type AuthAccountReadModel = PostgresAuthAccountReadModel; - fn auth_account_modifier(&self) -> &Self::AuthAccountModifier { - &PostgresAuthAccountRepository + fn auth_account_read_model(&self) -> &Self::AuthAccountReadModel { + &PostgresAuthAccountReadModel } } @@ -165,11 +152,8 @@ mod test { mod query { use crate::database::PostgresDatabase; use kernel::interfaces::database::DatabaseConnection; - use kernel::interfaces::modify::{ - AuthAccountModifier, AuthHostModifier, DependOnAuthAccountModifier, - DependOnAuthHostModifier, - }; - use kernel::interfaces::query::{AuthAccountQuery, DependOnAuthAccountQuery}; + use kernel::interfaces::modify::{AuthHostModifier, DependOnAuthHostModifier}; + use kernel::interfaces::read_model::{AuthAccountReadModel, DependOnAuthAccountReadModel}; use kernel::prelude::entity::{ AuthAccount, AuthAccountClientId, AuthAccountId, AuthHost, AuthHostId, AuthHostUrl, EventVersion, @@ -198,18 +182,18 @@ mod test { ); database - .auth_account_modifier() + .auth_account_read_model() .create(&mut transaction, &auth_account) .await .unwrap(); let result = database - .auth_account_query() + .auth_account_read_model() .find_by_id(&mut transaction, &account_id) .await .unwrap(); assert_eq!(result, Some(auth_account.clone())); database - .auth_account_modifier() + .auth_account_read_model() .delete(&mut transaction, auth_account.id()) .await .unwrap(); @@ -219,11 +203,8 @@ mod test { mod modify { use crate::database::PostgresDatabase; use kernel::interfaces::database::DatabaseConnection; - use kernel::interfaces::modify::{ - AuthAccountModifier, AuthHostModifier, DependOnAuthAccountModifier, - DependOnAuthHostModifier, - }; - use kernel::interfaces::query::{AuthAccountQuery, DependOnAuthAccountQuery}; + use kernel::interfaces::modify::{AuthHostModifier, DependOnAuthHostModifier}; + use kernel::interfaces::read_model::{AuthAccountReadModel, DependOnAuthAccountReadModel}; use kernel::prelude::entity::{ AuthAccount, AuthAccountClientId, AuthAccountId, AuthHost, AuthHostId, AuthHostUrl, EventVersion, @@ -251,18 +232,18 @@ mod test { EventVersion::new(Uuid::now_v7()), ); database - .auth_account_modifier() + .auth_account_read_model() .create(&mut transaction, &auth_account) .await .unwrap(); let result = database - .auth_account_query() + .auth_account_read_model() .find_by_id(&mut transaction, &account_id) .await .unwrap(); assert_eq!(result, Some(auth_account.clone())); database - .auth_account_modifier() + .auth_account_read_model() .delete(&mut transaction, auth_account.id()) .await .unwrap(); @@ -289,7 +270,7 @@ mod test { EventVersion::new(Uuid::now_v7()), ); database - .auth_account_modifier() + .auth_account_read_model() .create(&mut transaction, &auth_account) .await .unwrap(); @@ -300,18 +281,18 @@ mod test { EventVersion::new(Uuid::now_v7()), ); database - .auth_account_modifier() + .auth_account_read_model() .update(&mut transaction, &updated_auth_account) .await .unwrap(); let result = database - .auth_account_query() + .auth_account_read_model() .find_by_id(&mut transaction, &account_id) .await .unwrap(); assert_eq!(result, Some(updated_auth_account)); database - .auth_account_modifier() + .auth_account_read_model() .delete(&mut transaction, auth_account.id()) .await .unwrap(); @@ -338,26 +319,21 @@ mod test { EventVersion::new(Uuid::now_v7()), ); database - .auth_account_modifier() + .auth_account_read_model() .create(&mut transaction, &auth_account) .await .unwrap(); database - .auth_account_modifier() + .auth_account_read_model() .delete(&mut transaction, &account_id) .await .unwrap(); let result = database - .auth_account_query() + .auth_account_read_model() .find_by_id(&mut transaction, &account_id) .await .unwrap(); assert_eq!(result, None); - database - .auth_account_modifier() - .delete(&mut transaction, auth_account.id()) - .await - .unwrap(); } } } diff --git a/driver/src/database/postgres/auth_account_event_store.rs b/driver/src/database/postgres/auth_account_event_store.rs new file mode 100644 index 0000000..3cbd8bb --- /dev/null +++ b/driver/src/database/postgres/auth_account_event_store.rs @@ -0,0 +1,390 @@ +use crate::database::postgres::{CountRow, VersionRow}; +use crate::database::{PostgresConnection, PostgresDatabase}; +use crate::ConvertError; +use error_stack::Report; +use kernel::interfaces::event_store::{AuthAccountEventStore, DependOnAuthAccountEventStore}; +use kernel::prelude::entity::{ + AuthAccount, AuthAccountEvent, CommandEnvelope, EventEnvelope, EventId, EventVersion, + KnownEventVersion, +}; +use kernel::KernelError; +use serde_json; +use sqlx::PgConnection; +use uuid::Uuid; + +#[derive(sqlx::FromRow)] +struct EventRow { + version: Uuid, + id: Uuid, + #[allow(dead_code)] + event_name: String, + data: serde_json::Value, +} + +impl TryFrom for EventEnvelope { + type Error = Report; + fn try_from(value: EventRow) -> Result { + let event: AuthAccountEvent = serde_json::from_value(value.data).convert_error()?; + Ok(EventEnvelope::new( + EventId::new(value.id), + event, + EventVersion::new(value.version), + )) + } +} + +pub struct PostgresAuthAccountEventStore; + +impl AuthAccountEventStore for PostgresAuthAccountEventStore { + type Executor = PostgresConnection; + + async fn find_by_id( + &self, + executor: &mut Self::Executor, + id: &EventId, + since: Option<&EventVersion>, + ) -> error_stack::Result>, KernelError> { + let con: &mut PgConnection = executor; + let rows = if let Some(version) = since { + sqlx::query_as::<_, EventRow>( + //language=postgresql + r#" + SELECT version, id, event_name, data + FROM auth_account_events + WHERE id = $1 AND version > $2 + ORDER BY version + "#, + ) + .bind(id.as_ref()) + .bind(version.as_ref()) + .fetch_all(con) + .await + .convert_error()? + } else { + sqlx::query_as::<_, EventRow>( + //language=postgresql + r#" + SELECT version, id, event_name, data + FROM auth_account_events + WHERE id = $1 + ORDER BY version + "#, + ) + .bind(id.as_ref()) + .fetch_all(con) + .await + .convert_error()? + }; + rows.into_iter() + .map(|row| row.try_into()) + .collect::, KernelError>>() + } + + async fn persist( + &self, + executor: &mut Self::Executor, + command: &CommandEnvelope, + ) -> error_stack::Result<(), KernelError> { + self.persist_internal(executor, command, Uuid::now_v7()) + .await + } + + async fn persist_and_transform( + &self, + executor: &mut Self::Executor, + command: CommandEnvelope, + ) -> error_stack::Result, KernelError> { + let version = Uuid::now_v7(); + self.persist_internal(executor, &command, version).await?; + + let command = command.into_destruct(); + Ok(EventEnvelope::new( + command.id, + command.event, + EventVersion::new(version), + )) + } +} + +impl PostgresAuthAccountEventStore { + async fn persist_internal( + &self, + executor: &mut PostgresConnection, + command: &CommandEnvelope, + version: Uuid, + ) -> error_stack::Result<(), KernelError> { + let con: &mut PgConnection = executor; + + let event_name = command.event_name(); + let prev_version = command.prev_version().as_ref(); + if let Some(prev_version) = prev_version { + match prev_version { + KnownEventVersion::Nothing => { + let amount = sqlx::query_as::<_, CountRow>( + //language=postgresql + r#" + SELECT COUNT(*) + FROM auth_account_events + WHERE id = $1 + "#, + ) + .bind(command.id().as_ref()) + .fetch_one(&mut *con) + .await + .convert_error()?; + if amount.count != 0 { + return Err(Report::new(KernelError::Concurrency).attach_printable( + format!("Event {} already exists", command.id().as_ref()), + )); + } + } + KnownEventVersion::Prev(prev_version) => { + let last_version = sqlx::query_as::<_, VersionRow>( + //language=postgresql + r#" + SELECT version + FROM auth_account_events + WHERE id = $1 + ORDER BY version DESC + LIMIT 1 + "#, + ) + .bind(command.id().as_ref()) + .fetch_optional(&mut *con) + .await + .convert_error()?; + if last_version + .map(|row: VersionRow| &row.version != prev_version.as_ref()) + .unwrap_or(true) + { + return Err(Report::new(KernelError::Concurrency).attach_printable( + format!( + "Event {} version {} already exists", + command.id().as_ref(), + prev_version.as_ref() + ), + )); + } + } + }; + } + + sqlx::query( + //language=postgresql + r#" + INSERT INTO auth_account_events (version, id, event_name, data) + VALUES ($1, $2, $3, $4) + "#, + ) + .bind(version) + .bind(command.id().as_ref()) + .bind(event_name) + .bind(serde_json::to_value(command.event()).convert_error()?) + .execute(con) + .await + .convert_error()?; + + Ok(()) + } +} + +impl DependOnAuthAccountEventStore for PostgresDatabase { + type AuthAccountEventStore = PostgresAuthAccountEventStore; + + fn auth_account_event_store(&self) -> &Self::AuthAccountEventStore { + &PostgresAuthAccountEventStore + } +} + +#[cfg(test)] +mod test { + mod query { + use crate::database::PostgresDatabase; + use kernel::interfaces::database::DatabaseConnection; + use kernel::interfaces::event_store::{ + AuthAccountEventStore, DependOnAuthAccountEventStore, + }; + use kernel::prelude::entity::{ + AuthAccount, AuthAccountClientId, AuthAccountEvent, AuthAccountId, AuthHostId, + CommandEnvelope, EventId, + }; + use uuid::Uuid; + + fn create_auth_account_command( + id: AuthAccountId, + ) -> CommandEnvelope { + AuthAccount::create( + id, + AuthHostId::new(Uuid::now_v7()), + AuthAccountClientId::new("test_client"), + ) + } + + #[test_with::env(DATABASE_URL)] + #[tokio::test] + async fn find_by_id() { + let db = PostgresDatabase::new().await.unwrap(); + let mut transaction = db.begin_transaction().await.unwrap(); + let id = AuthAccountId::new(Uuid::now_v7()); + let event_id = EventId::from(id.clone()); + let events = db + .auth_account_event_store() + .find_by_id(&mut transaction, &event_id, None) + .await + .unwrap(); + assert_eq!(events.len(), 0); + + let created = create_auth_account_command(id.clone()); + let deleted = AuthAccount::delete(id.clone()); + + db.auth_account_event_store() + .persist(&mut transaction, &created) + .await + .unwrap(); + db.auth_account_event_store() + .persist(&mut transaction, &deleted) + .await + .unwrap(); + + let events = db + .auth_account_event_store() + .find_by_id(&mut transaction, &event_id, None) + .await + .unwrap(); + assert_eq!(events.len(), 2); + assert_eq!(&events[0].event, created.event()); + assert_eq!(&events[1].event, deleted.event()); + } + + #[test_with::env(DATABASE_URL)] + #[tokio::test] + async fn find_by_id_since_version() { + let db = PostgresDatabase::new().await.unwrap(); + let mut transaction = db.begin_transaction().await.unwrap(); + let id = AuthAccountId::new(Uuid::now_v7()); + let event_id = EventId::from(id.clone()); + + let created = create_auth_account_command(id.clone()); + let deleted = AuthAccount::delete(id.clone()); + + db.auth_account_event_store() + .persist(&mut transaction, &created) + .await + .unwrap(); + db.auth_account_event_store() + .persist(&mut transaction, &deleted) + .await + .unwrap(); + + let all_events = db + .auth_account_event_store() + .find_by_id(&mut transaction, &event_id, None) + .await + .unwrap(); + assert_eq!(all_events.len(), 2); + + let since_events = db + .auth_account_event_store() + .find_by_id(&mut transaction, &event_id, Some(&all_events[0].version)) + .await + .unwrap(); + assert_eq!(since_events.len(), 1); + assert_eq!(&since_events[0].event, deleted.event()); + + let no_events = db + .auth_account_event_store() + .find_by_id(&mut transaction, &event_id, Some(&all_events[1].version)) + .await + .unwrap(); + assert_eq!(no_events.len(), 0); + } + } + + mod persist { + use crate::database::PostgresDatabase; + use kernel::interfaces::database::DatabaseConnection; + use kernel::interfaces::event_store::{ + AuthAccountEventStore, DependOnAuthAccountEventStore, + }; + use kernel::prelude::entity::{ + AuthAccount, AuthAccountClientId, AuthAccountEvent, AuthAccountId, AuthHostId, + CommandEnvelope, EventId, + }; + use uuid::Uuid; + + fn create_auth_account_command( + id: AuthAccountId, + ) -> CommandEnvelope { + AuthAccount::create( + id, + AuthHostId::new(Uuid::now_v7()), + AuthAccountClientId::new("test_client"), + ) + } + + #[test_with::env(DATABASE_URL)] + #[tokio::test] + async fn basic_creation() { + let db = PostgresDatabase::new().await.unwrap(); + let mut transaction = db.begin_transaction().await.unwrap(); + let id = AuthAccountId::new(Uuid::now_v7()); + let created = create_auth_account_command(id.clone()); + db.auth_account_event_store() + .persist(&mut transaction, &created) + .await + .unwrap(); + let events = db + .auth_account_event_store() + .find_by_id(&mut transaction, &EventId::from(id), None) + .await + .unwrap(); + assert_eq!(events.len(), 1); + } + + #[test_with::env(DATABASE_URL)] + #[tokio::test] + async fn persist_and_transform_test() { + let db = PostgresDatabase::new().await.unwrap(); + let mut transaction = db.begin_transaction().await.unwrap(); + let id = AuthAccountId::new(Uuid::now_v7()); + let created = create_auth_account_command(id.clone()); + + let event_envelope = db + .auth_account_event_store() + .persist_and_transform(&mut transaction, created.clone()) + .await + .unwrap(); + + assert_eq!(event_envelope.id, EventId::from(id.clone())); + assert_eq!(&event_envelope.event, created.event()); + + let events = db + .auth_account_event_store() + .find_by_id(&mut transaction, &EventId::from(id), None) + .await + .unwrap(); + assert_eq!(events.len(), 1); + assert_eq!(&events[0].event, created.event()); + } + + #[test_with::env(DATABASE_URL)] + #[tokio::test] + async fn optimistic_concurrency_nothing() { + let db = PostgresDatabase::new().await.unwrap(); + let mut transaction = db.begin_transaction().await.unwrap(); + let id = AuthAccountId::new(Uuid::now_v7()); + let created = create_auth_account_command(id.clone()); + db.auth_account_event_store() + .persist(&mut transaction, &created) + .await + .unwrap(); + + let duplicate = create_auth_account_command(id.clone()); + let result = db + .auth_account_event_store() + .persist(&mut transaction, &duplicate) + .await; + assert!(result.is_err()); + } + } +} diff --git a/driver/src/database/postgres/event.rs b/driver/src/database/postgres/event.rs deleted file mode 100644 index 3f57cad..0000000 --- a/driver/src/database/postgres/event.rs +++ /dev/null @@ -1,439 +0,0 @@ -use crate::database::postgres::{CountRow, VersionRow}; -use crate::database::{PostgresConnection, PostgresDatabase}; -use crate::ConvertError; -use error_stack::Report; -use kernel::interfaces::modify::{DependOnEventModifier, EventModifier}; -use kernel::interfaces::query::{DependOnEventQuery, EventQuery}; -use kernel::prelude::entity::{ - CommandEnvelope, EventEnvelope, EventId, EventVersion, KnownEventVersion, -}; -use kernel::KernelError; -use serde::{Deserialize, Serialize}; -use sqlx::PgConnection; -use uuid::Uuid; - -#[derive(sqlx::FromRow)] -struct EventRow { - version: Uuid, - id: Uuid, - event_name: String, - data: serde_json::Value, -} - -impl Deserialize<'a>, Entity> TryFrom for EventEnvelope { - type Error = Report; - fn try_from(value: EventRow) -> Result { - let event: Event = serde_json::from_value(value.data).convert_error()?; - Ok(EventEnvelope::new( - EventId::new(value.id), - event, - EventVersion::new(value.version), - )) - } -} - -pub struct PostgresEventRepository; - -impl EventQuery for PostgresEventRepository { - type Executor = PostgresConnection; - - async fn find_by_id Deserialize<'de>, Entity>( - &self, - executor: &mut Self::Executor, - id: &EventId, - since: Option<&EventVersion>, - ) -> error_stack::Result>, KernelError> { - let con: &mut PgConnection = executor; - if let Some(version) = since { - sqlx::query_as::<_, EventRow>( - //language=postgresql - r#" - SELECT version, id, event_name, data - FROM event_streams - WHERE id = $2 AND version > $1 - ORDER BY version - "#, - ) - .bind(version.as_ref()) - } else { - sqlx::query_as::<_, EventRow>( - //language=postgresql - r#" - SELECT version, id, event_name, data - FROM event_streams - WHERE id = $1 - ORDER BY version - "#, - ) - } - .bind(id.as_ref()) - .fetch_all(con) - .await - .convert_error() - .and_then(|versions| { - versions - .into_iter() - .map(|row| row.try_into()) - .collect::>, KernelError>>() - }) - } -} - -impl DependOnEventQuery for PostgresDatabase { - type EventQuery = PostgresEventRepository; - - fn event_query(&self) -> &Self::EventQuery { - &PostgresEventRepository - } -} - -impl EventModifier for PostgresEventRepository { - type Executor = PostgresConnection; - - async fn persist( - &self, - executor: &mut Self::Executor, - command: &CommandEnvelope, - ) -> error_stack::Result<(), KernelError> { - self.persist_internal(executor, command, Uuid::now_v7()) - .await?; - Ok(()) - } - - async fn persist_and_transform( - &self, - executor: &mut Self::Executor, - command: CommandEnvelope, - ) -> error_stack::Result, KernelError> { - let version = Uuid::now_v7(); - - self.persist_internal(executor, &command, version).await?; - - let command = command.into_destruct(); - Ok(EventEnvelope::new( - command.id, - command.event, - EventVersion::new(version), - )) - } -} - -impl PostgresEventRepository { - async fn persist_internal( - &self, - executor: &mut PostgresConnection, - command: &CommandEnvelope, - version: Uuid, - ) -> error_stack::Result<(), KernelError> { - let con: &mut PgConnection = executor; - - // Validation logic - let event_name = command.event_name(); - let prev_version = command.prev_version().as_ref(); - if let Some(prev_version) = prev_version { - match prev_version { - KnownEventVersion::Nothing => { - let amount = sqlx::query_as::<_, CountRow>( - //language=postgresql - r#" - SELECT COUNT(*) - FROM event_streams - WHERE id = $1 - "#, - ) - .bind(command.id().as_ref()) - .fetch_one(&mut *con) - .await - .convert_error()?; - if amount.count != 0 { - return Err(Report::new(KernelError::Concurrency).attach_printable( - format!("Event {} already exists", command.id().as_ref()), - )); - } - } - KnownEventVersion::Prev(prev_version) => { - let last_version = sqlx::query_as::<_, VersionRow>( - //language=postgresql - r#" - SELECT version - FROM event_streams - WHERE id = $1 - ORDER BY version DESC - LIMIT 1 - "#, - ) - .bind(command.id().as_ref()) - .fetch_optional(&mut *con) - .await - .convert_error()?; - if last_version - .map(|row: VersionRow| &row.version != prev_version.as_ref()) - .unwrap_or(true) - { - return Err(Report::new(KernelError::Concurrency).attach_printable( - format!( - "Event {} version {} already exists", - command.id().as_ref(), - prev_version.as_ref() - ), - )); - } - } - }; - } - - // Insertion logic - sqlx::query( - //language=postgresql - r#" - INSERT INTO event_streams (version, id, event_name, data) - VALUES ($1, $2, $3, $4) - "#, - ) - .bind(version) - .bind(command.id().as_ref()) - .bind(event_name) - .bind(serde_json::to_value(command.event()).convert_error()?) - .execute(con) - .await - .convert_error()?; - - Ok(()) - } -} - -impl DependOnEventModifier for PostgresDatabase { - type EventModifier = PostgresEventRepository; - - fn event_modifier(&self) -> &Self::EventModifier { - &PostgresEventRepository - } -} - -#[cfg(test)] -mod test { - mod query { - use uuid::Uuid; - - use crate::database::PostgresDatabase; - use kernel::interfaces::database::DatabaseConnection; - use kernel::interfaces::modify::{DependOnEventModifier, EventModifier}; - use kernel::interfaces::query::{DependOnEventQuery, EventQuery}; - use kernel::prelude::entity::{ - Account, AccountEvent, AccountId, AccountIsBot, AccountName, AccountPrivateKey, - AccountPublicKey, AuthAccountId, CommandEnvelope, EventId, KnownEventVersion, Nanoid, - }; - - fn create_account_command(account_id: AccountId) -> CommandEnvelope { - let event = AccountEvent::Created { - name: AccountName::new("test"), - private_key: AccountPrivateKey::new("test"), - public_key: AccountPublicKey::new("test"), - is_bot: AccountIsBot::new(false), - nanoid: Nanoid::default(), - auth_account_id: AuthAccountId::new(uuid::Uuid::now_v7()), - }; - CommandEnvelope::new( - EventId::from(account_id), - event.name(), - event, - Some(KnownEventVersion::Nothing), - ) - } - - #[test_with::env(DATABASE_URL)] - #[tokio::test] - async fn find_by_id() { - let db = PostgresDatabase::new().await.unwrap(); - let mut transaction = db.begin_transaction().await.unwrap(); - let account_id = AccountId::new(Uuid::now_v7()); - let event_id = EventId::from(account_id.clone()); - let events = db - .event_query() - .find_by_id(&mut transaction, &event_id, None) - .await - .unwrap(); - assert_eq!(events.len(), 0); - let created_account = create_account_command(account_id.clone()); - let update_event = AccountEvent::Updated { - is_bot: AccountIsBot::new(true), - }; - let updated_account = CommandEnvelope::new( - EventId::from(account_id.clone()), - update_event.name(), - update_event, - None, - ); - let delete_event = AccountEvent::Deleted; - let deleted_account = CommandEnvelope::new( - EventId::from(account_id.clone()), - delete_event.name(), - delete_event, - None, - ); - - db.event_modifier() - .persist(&mut transaction, &created_account) - .await - .unwrap(); - db.event_modifier() - .persist(&mut transaction, &updated_account) - .await - .unwrap(); - db.event_modifier() - .persist(&mut transaction, &deleted_account) - .await - .unwrap(); - let events = db - .event_query() - .find_by_id(&mut transaction, &event_id, None) - .await - .unwrap(); - assert_eq!(events.len(), 3); - assert_eq!(&events[0].event, created_account.event()); - assert_eq!(&events[1].event, updated_account.event()); - assert_eq!(&events[2].event, deleted_account.event()); - } - - #[test_with::env(DATABASE_URL)] - #[tokio::test] - async fn find_by_id_since_version() { - let db = PostgresDatabase::new().await.unwrap(); - let mut transaction = db.begin_transaction().await.unwrap(); - let account_id = AccountId::new(Uuid::now_v7()); - let event_id = EventId::from(account_id.clone()); - let created_account = create_account_command(account_id.clone()); - let update_event = AccountEvent::Updated { - is_bot: AccountIsBot::new(true), - }; - let updated_account = CommandEnvelope::new( - EventId::from(account_id.clone()), - update_event.name(), - update_event, - None, - ); - let delete_event = AccountEvent::Deleted; - let deleted_account = CommandEnvelope::new( - EventId::from(account_id.clone()), - delete_event.name(), - delete_event, - None, - ); - db.event_modifier() - .persist(&mut transaction, &created_account) - .await - .unwrap(); - db.event_modifier() - .persist(&mut transaction, &updated_account) - .await - .unwrap(); - db.event_modifier() - .persist(&mut transaction, &deleted_account) - .await - .unwrap(); - - let all_events = db - .event_query() - .find_by_id(&mut transaction, &event_id, None) - .await - .unwrap(); - assert_eq!(all_events.len(), 3); - - // Query since the first event's version — should return the 2nd and 3rd events - let since_events = db - .event_query() - .find_by_id(&mut transaction, &event_id, Some(&all_events[0].version)) - .await - .unwrap(); - assert_eq!(since_events.len(), 2); - assert_eq!(&since_events[0].event, updated_account.event()); - assert_eq!(&since_events[1].event, deleted_account.event()); - - // Query since the last event's version — should return no events - let no_events = db - .event_query() - .find_by_id(&mut transaction, &event_id, Some(&all_events[2].version)) - .await - .unwrap(); - assert_eq!(no_events.len(), 0); - } - } - - mod modify { - use uuid::Uuid; - - use crate::database::PostgresDatabase; - use kernel::interfaces::database::DatabaseConnection; - use kernel::interfaces::modify::{DependOnEventModifier, EventModifier}; - use kernel::interfaces::query::{DependOnEventQuery, EventQuery}; - use kernel::prelude::entity::{ - Account, AccountEvent, AccountId, AccountIsBot, AccountName, AccountPrivateKey, - AccountPublicKey, AuthAccountId, CommandEnvelope, EventId, KnownEventVersion, Nanoid, - }; - - fn create_account_command(account_id: AccountId) -> CommandEnvelope { - let event = AccountEvent::Created { - name: AccountName::new("test"), - private_key: AccountPrivateKey::new("test"), - public_key: AccountPublicKey::new("test"), - is_bot: AccountIsBot::new(false), - nanoid: Nanoid::default(), - auth_account_id: AuthAccountId::new(uuid::Uuid::now_v7()), - }; - CommandEnvelope::new( - EventId::from(account_id), - event.name(), - event, - Some(KnownEventVersion::Nothing), - ) - } - - #[test_with::env(DATABASE_URL)] - #[tokio::test] - async fn basic_creation() { - let db = PostgresDatabase::new().await.unwrap(); - let mut transaction = db.begin_transaction().await.unwrap(); - let account_id = AccountId::new(Uuid::now_v7()); - let created_account = create_account_command(account_id.clone()); - db.event_modifier() - .persist(&mut transaction, &created_account) - .await - .unwrap(); - let events = db - .event_query() - .find_by_id(&mut transaction, &EventId::from(account_id), None) - .await - .unwrap(); - assert_eq!(events.len(), 1); - } - - #[test_with::env(DATABASE_URL)] - #[tokio::test] - async fn persist_and_transform_test() { - let db = PostgresDatabase::new().await.unwrap(); - let mut transaction = db.begin_transaction().await.unwrap(); - let account_id = AccountId::new(Uuid::now_v7()); - let created_account = create_account_command(account_id.clone()); - - // Test persist_and_transform - let event_envelope = db - .event_modifier() - .persist_and_transform(&mut transaction, created_account.clone()) - .await - .unwrap(); - - // Verify the returned envelope - assert_eq!(event_envelope.id, EventId::from(account_id.clone())); - assert_eq!(&event_envelope.event, created_account.event()); - - // Verify it was persisted - let events = db - .event_query() - .find_by_id(&mut transaction, &EventId::from(account_id), None) - .await - .unwrap(); - assert_eq!(events.len(), 1); - assert_eq!(&events[0].event, created_account.event()); - } - } -} diff --git a/kernel/src/event_store.rs b/kernel/src/event_store.rs index a727161..bd9ae1a 100644 --- a/kernel/src/event_store.rs +++ b/kernel/src/event_store.rs @@ -1,3 +1,5 @@ mod account; +mod auth_account; pub use self::account::*; +pub use self::auth_account::*; diff --git a/kernel/src/event_store/auth_account.rs b/kernel/src/event_store/auth_account.rs new file mode 100644 index 0000000..089d89f --- /dev/null +++ b/kernel/src/event_store/auth_account.rs @@ -0,0 +1,44 @@ +use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Executor}; +use crate::entity::{ + AuthAccount, AuthAccountEvent, CommandEnvelope, EventEnvelope, EventId, EventVersion, +}; +use crate::KernelError; +use std::future::Future; + +pub trait AuthAccountEventStore: Sync + Send + 'static { + type Executor: Executor; + + fn persist( + &self, + executor: &mut Self::Executor, + command: &CommandEnvelope, + ) -> impl Future> + Send; + + fn persist_and_transform( + &self, + executor: &mut Self::Executor, + command: CommandEnvelope, + ) -> impl Future< + Output = error_stack::Result, KernelError>, + > + Send; + + fn find_by_id( + &self, + executor: &mut Self::Executor, + id: &EventId, + since: Option<&EventVersion>, + ) -> impl Future< + Output = error_stack::Result< + Vec>, + KernelError, + >, + > + Send; +} + +pub trait DependOnAuthAccountEventStore: Sync + Send + DependOnDatabaseConnection { + type AuthAccountEventStore: AuthAccountEventStore< + Executor = ::Executor, + >; + + fn auth_account_event_store(&self) -> &Self::AuthAccountEventStore; +} diff --git a/kernel/src/lib.rs b/kernel/src/lib.rs index 4139c15..68159b7 100644 --- a/kernel/src/lib.rs +++ b/kernel/src/lib.rs @@ -51,8 +51,9 @@ pub mod interfaces { /// This macro generates implementations for: /// - DependOnDatabaseConnection /// - DependOnAccountReadModel, DependOnAccountEventStore -/// - DependOnAuthAccountQuery, DependOnAuthHostQuery, DependOnEventQuery -/// - DependOnAuthAccountModifier, DependOnAuthHostModifier, DependOnEventModifier +/// - DependOnAuthAccountReadModel, DependOnAuthAccountEventStore +/// - DependOnAuthHostQuery +/// - DependOnAuthHostModifier /// /// # Usage /// ```ignore @@ -87,10 +88,17 @@ macro_rules! impl_database_delegation { } } - impl $crate::interfaces::query::DependOnAuthAccountQuery for $impl_type { - type AuthAccountQuery = <$db_type as $crate::interfaces::query::DependOnAuthAccountQuery>::AuthAccountQuery; - fn auth_account_query(&self) -> &Self::AuthAccountQuery { - $crate::interfaces::query::DependOnAuthAccountQuery::auth_account_query(&self.$field) + impl $crate::interfaces::read_model::DependOnAuthAccountReadModel for $impl_type { + type AuthAccountReadModel = <$db_type as $crate::interfaces::read_model::DependOnAuthAccountReadModel>::AuthAccountReadModel; + fn auth_account_read_model(&self) -> &Self::AuthAccountReadModel { + $crate::interfaces::read_model::DependOnAuthAccountReadModel::auth_account_read_model(&self.$field) + } + } + + impl $crate::interfaces::event_store::DependOnAuthAccountEventStore for $impl_type { + type AuthAccountEventStore = <$db_type as $crate::interfaces::event_store::DependOnAuthAccountEventStore>::AuthAccountEventStore; + fn auth_account_event_store(&self) -> &Self::AuthAccountEventStore { + $crate::interfaces::event_store::DependOnAuthAccountEventStore::auth_account_event_store(&self.$field) } } @@ -101,20 +109,6 @@ macro_rules! impl_database_delegation { } } - impl $crate::interfaces::query::DependOnEventQuery for $impl_type { - type EventQuery = <$db_type as $crate::interfaces::query::DependOnEventQuery>::EventQuery; - fn event_query(&self) -> &Self::EventQuery { - $crate::interfaces::query::DependOnEventQuery::event_query(&self.$field) - } - } - - impl $crate::interfaces::modify::DependOnAuthAccountModifier for $impl_type { - type AuthAccountModifier = <$db_type as $crate::interfaces::modify::DependOnAuthAccountModifier>::AuthAccountModifier; - fn auth_account_modifier(&self) -> &Self::AuthAccountModifier { - $crate::interfaces::modify::DependOnAuthAccountModifier::auth_account_modifier(&self.$field) - } - } - impl $crate::interfaces::modify::DependOnAuthHostModifier for $impl_type { type AuthHostModifier = <$db_type as $crate::interfaces::modify::DependOnAuthHostModifier>::AuthHostModifier; fn auth_host_modifier(&self) -> &Self::AuthHostModifier { @@ -122,11 +116,5 @@ macro_rules! impl_database_delegation { } } - impl $crate::interfaces::modify::DependOnEventModifier for $impl_type { - type EventModifier = <$db_type as $crate::interfaces::modify::DependOnEventModifier>::EventModifier; - fn event_modifier(&self) -> &Self::EventModifier { - $crate::interfaces::modify::DependOnEventModifier::event_modifier(&self.$field) - } - } }; } diff --git a/kernel/src/modify.rs b/kernel/src/modify.rs index dcc8a0d..c673868 100644 --- a/kernel/src/modify.rs +++ b/kernel/src/modify.rs @@ -1,13 +1,8 @@ -mod auth_account; mod auth_host; -mod event; mod follow; mod image; mod metadata; mod profile; mod remote_account; -pub use self::{ - auth_account::*, auth_host::*, event::*, follow::*, image::*, metadata::*, profile::*, - remote_account::*, -}; +pub use self::{auth_host::*, follow::*, image::*, metadata::*, profile::*, remote_account::*}; diff --git a/kernel/src/modify/auth_account.rs b/kernel/src/modify/auth_account.rs deleted file mode 100644 index e933f27..0000000 --- a/kernel/src/modify/auth_account.rs +++ /dev/null @@ -1,34 +0,0 @@ -use crate::database::{DependOnDatabaseConnection, Executor}; -use crate::entity::{AuthAccount, AuthAccountId}; -use crate::KernelError; -use std::future::Future; - -pub trait AuthAccountModifier: Sync + Send + 'static { - type Executor: Executor; - - fn create( - &self, - executor: &mut Self::Executor, - auth_account: &AuthAccount, - ) -> impl Future> + Send; - - fn update( - &self, - executor: &mut Self::Executor, - auth_account: &AuthAccount, - ) -> impl Future> + Send; - - fn delete( - &self, - executor: &mut Self::Executor, - account_id: &AuthAccountId, - ) -> impl Future> + Send; -} - -pub trait DependOnAuthAccountModifier: Sync + Send + DependOnDatabaseConnection { - type AuthAccountModifier: AuthAccountModifier< - Executor = ::Executor, - >; - - fn auth_account_modifier(&self) -> &Self::AuthAccountModifier; -} diff --git a/kernel/src/modify/event.rs b/kernel/src/modify/event.rs deleted file mode 100644 index 639dbd1..0000000 --- a/kernel/src/modify/event.rs +++ /dev/null @@ -1,29 +0,0 @@ -use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Executor}; -use crate::entity::{CommandEnvelope, EventEnvelope}; -use crate::KernelError; -use serde::Serialize; -use std::future::Future; - -pub trait EventModifier: 'static + Sync + Send { - type Executor: Executor; - - fn persist( - &self, - executor: &mut Self::Executor, - command: &CommandEnvelope, - ) -> impl Future> + Send; - - fn persist_and_transform( - &self, - executor: &mut Self::Executor, - command: CommandEnvelope, - ) -> impl Future, KernelError>> + Send; -} - -pub trait DependOnEventModifier: Sync + Send + DependOnDatabaseConnection { - type EventModifier: EventModifier< - Executor = ::Executor, - >; - - fn event_modifier(&self) -> &Self::EventModifier; -} diff --git a/kernel/src/query.rs b/kernel/src/query.rs index dcc8a0d..c673868 100644 --- a/kernel/src/query.rs +++ b/kernel/src/query.rs @@ -1,13 +1,8 @@ -mod auth_account; mod auth_host; -mod event; mod follow; mod image; mod metadata; mod profile; mod remote_account; -pub use self::{ - auth_account::*, auth_host::*, event::*, follow::*, image::*, metadata::*, profile::*, - remote_account::*, -}; +pub use self::{auth_host::*, follow::*, image::*, metadata::*, profile::*, remote_account::*}; diff --git a/kernel/src/query/auth_account.rs b/kernel/src/query/auth_account.rs deleted file mode 100644 index 092994e..0000000 --- a/kernel/src/query/auth_account.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::database::{DependOnDatabaseConnection, Executor}; -use crate::entity::{AuthAccount, AuthAccountClientId, AuthAccountId}; -use crate::KernelError; -use std::future::Future; - -pub trait AuthAccountQuery: Sync + Send + 'static { - type Executor: Executor; - - fn find_by_id( - &self, - executor: &mut Self::Executor, - account_id: &AuthAccountId, - ) -> impl Future, KernelError>> + Send; - - fn find_by_client_id( - &self, - executor: &mut Self::Executor, - client_id: &AuthAccountClientId, - ) -> impl Future, KernelError>> + Send; -} - -pub trait DependOnAuthAccountQuery: Sync + Send + DependOnDatabaseConnection { - type AuthAccountQuery: AuthAccountQuery< - Executor = ::Executor, - >; - - fn auth_account_query(&self) -> &Self::AuthAccountQuery; -} diff --git a/kernel/src/query/event.rs b/kernel/src/query/event.rs deleted file mode 100644 index e4701d3..0000000 --- a/kernel/src/query/event.rs +++ /dev/null @@ -1,24 +0,0 @@ -use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Executor}; -use crate::entity::{EventEnvelope, EventId, EventVersion}; -use crate::KernelError; -use serde::Deserialize; -use std::future::Future; - -pub trait EventQuery: Sync + Send + 'static { - type Executor: Executor; - - fn find_by_id Deserialize<'de> + Sync, Entity: Sync>( - &self, - executor: &mut Self::Executor, - id: &EventId, - since: Option<&EventVersion>, - ) -> impl Future>, KernelError>> + Send; -} - -pub trait DependOnEventQuery: Sync + Send + DependOnDatabaseConnection { - type EventQuery: EventQuery< - Executor = ::Executor, - >; - - fn event_query(&self) -> &Self::EventQuery; -} diff --git a/kernel/src/read_model.rs b/kernel/src/read_model.rs index a727161..bd9ae1a 100644 --- a/kernel/src/read_model.rs +++ b/kernel/src/read_model.rs @@ -1,3 +1,5 @@ mod account; +mod auth_account; pub use self::account::*; +pub use self::auth_account::*; diff --git a/kernel/src/read_model/auth_account.rs b/kernel/src/read_model/auth_account.rs new file mode 100644 index 0000000..33e3fee --- /dev/null +++ b/kernel/src/read_model/auth_account.rs @@ -0,0 +1,48 @@ +use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Executor}; +use crate::entity::{AuthAccount, AuthAccountClientId, AuthAccountId}; +use crate::KernelError; +use std::future::Future; + +pub trait AuthAccountReadModel: Sync + Send + 'static { + type Executor: Executor; + + // Query operations (projection reads) + fn find_by_id( + &self, + executor: &mut Self::Executor, + id: &AuthAccountId, + ) -> impl Future, KernelError>> + Send; + + fn find_by_client_id( + &self, + executor: &mut Self::Executor, + client_id: &AuthAccountClientId, + ) -> impl Future, KernelError>> + Send; + + // Projection update operations (called by EventApplier pipeline) + fn create( + &self, + executor: &mut Self::Executor, + auth_account: &AuthAccount, + ) -> impl Future> + Send; + + fn update( + &self, + executor: &mut Self::Executor, + auth_account: &AuthAccount, + ) -> impl Future> + Send; + + fn delete( + &self, + executor: &mut Self::Executor, + account_id: &AuthAccountId, + ) -> impl Future> + Send; +} + +pub trait DependOnAuthAccountReadModel: Sync + Send + DependOnDatabaseConnection { + type AuthAccountReadModel: AuthAccountReadModel< + Executor = ::Executor, + >; + + fn auth_account_read_model(&self) -> &Self::AuthAccountReadModel; +} diff --git a/migrations/20260309000000_auth_account_events.sql b/migrations/20260309000000_auth_account_events.sql new file mode 100644 index 0000000..066f315 --- /dev/null +++ b/migrations/20260309000000_auth_account_events.sql @@ -0,0 +1,8 @@ +CREATE TABLE "auth_account_events" ( + "version" UUID NOT NULL, + "id" UUID NOT NULL, + "event_name" TEXT NOT NULL, + "data" JSONB NOT NULL, + "occurred_at" TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY ("id", "version") +); diff --git a/server/src/handler.rs b/server/src/handler.rs index 035ae73..ddc8e0b 100644 --- a/server/src/handler.rs +++ b/server/src/handler.rs @@ -1,5 +1,6 @@ use crate::applier::ApplierContainer; use adapter::processor::account::DependOnAccountSignal; +use adapter::processor::auth_account::DependOnAuthAccountSignal; use driver::crypto::{ Argon2Encryptor, FilePasswordProvider, Rsa2048RawGenerator, Rsa2048Signer, Rsa2048Verifier, }; @@ -85,6 +86,50 @@ impl DependOnAccountSignal for AppModule { } } +impl DependOnAuthAccountSignal for AppModule { + type AuthAccountSignal = ApplierContainer; + fn auth_account_signal(&self) -> &Self::AuthAccountSignal { + &self.applier_container + } +} + +impl kernel::interfaces::read_model::DependOnAuthAccountReadModel for AppModule { + type AuthAccountReadModel = ::AuthAccountReadModel; + fn auth_account_read_model(&self) -> &Self::AuthAccountReadModel { + kernel::interfaces::read_model::DependOnAuthAccountReadModel::auth_account_read_model( + self.handler.as_ref().database_connection(), + ) + } +} + +impl kernel::interfaces::event_store::DependOnAuthAccountEventStore for AppModule { + type AuthAccountEventStore = ::AuthAccountEventStore; + fn auth_account_event_store(&self) -> &Self::AuthAccountEventStore { + kernel::interfaces::event_store::DependOnAuthAccountEventStore::auth_account_event_store( + self.handler.as_ref().database_connection(), + ) + } +} + +impl kernel::interfaces::query::DependOnAuthHostQuery for AppModule { + type AuthHostQuery = + ::AuthHostQuery; + fn auth_host_query(&self) -> &Self::AuthHostQuery { + kernel::interfaces::query::DependOnAuthHostQuery::auth_host_query( + self.handler.as_ref().database_connection(), + ) + } +} + +impl kernel::interfaces::modify::DependOnAuthHostModifier for AppModule { + type AuthHostModifier = ::AuthHostModifier; + fn auth_host_modifier(&self) -> &Self::AuthHostModifier { + kernel::interfaces::modify::DependOnAuthHostModifier::auth_host_modifier( + self.handler.as_ref().database_connection(), + ) + } +} + // Note: DependOnSigningKeyGenerator, DependOnAccountCommandProcessor, // DependOnAccountQueryProcessor, and all UseCase traits are provided // automatically via blanket impls in adapter. diff --git a/server/src/keycloak.rs b/server/src/keycloak.rs index 11adad3..b9858e0 100644 --- a/server/src/keycloak.rs +++ b/server/src/keycloak.rs @@ -1,20 +1,16 @@ -use crate::handler::Handler; +use crate::handler::AppModule; +use adapter::processor::auth_account::{ + AuthAccountCommandProcessor, AuthAccountQueryProcessor, DependOnAuthAccountCommandProcessor, + DependOnAuthAccountQueryProcessor, +}; use axum_keycloak_auth::decode::KeycloakToken; use axum_keycloak_auth::instance::{KeycloakAuthInstance, KeycloakConfig}; use axum_keycloak_auth::role::Role; -use error_stack::Report; use kernel::interfaces::database::{DatabaseConnection, DependOnDatabaseConnection}; -use kernel::interfaces::event::EventApplier; -use kernel::interfaces::modify::{ - AuthAccountModifier, AuthHostModifier, DependOnAuthAccountModifier, DependOnAuthHostModifier, - DependOnEventModifier, EventModifier, -}; -use kernel::interfaces::query::{ - AuthAccountQuery, AuthHostQuery, DependOnAuthAccountQuery, DependOnAuthHostQuery, -}; -use kernel::interfaces::signal::Signal; +use kernel::interfaces::modify::{AuthHostModifier, DependOnAuthHostModifier}; +use kernel::interfaces::query::{AuthHostQuery, DependOnAuthHostQuery}; use kernel::prelude::entity::{ - AuthAccount, AuthAccountClientId, AuthAccountId, AuthHost, AuthHostId, AuthHostUrl, + AuthAccountClientId, AuthAccountId, AuthHost, AuthHostId, AuthHostUrl, }; use kernel::KernelError; use std::sync::LazyLock; @@ -54,54 +50,36 @@ impl From> for KeycloakAuthAccount { } pub async fn resolve_auth_account_id( - handler: &Handler, - signal: &impl Signal, + app: &AppModule, auth_info: KeycloakAuthAccount, ) -> error_stack::Result { let client_id = AuthAccountClientId::new(auth_info.client_id); - let mut transaction = handler.database_connection().begin_transaction().await?; - let auth_account = handler - .auth_account_query() - .find_by_client_id(&mut transaction, &client_id) + let mut executor = app.database_connection().begin_transaction().await?; + let auth_account = app + .auth_account_query_processor() + .find_by_client_id(&mut executor, &client_id) .await?; let auth_account = if let Some(auth_account) = auth_account { auth_account } else { let url = AuthHostUrl::new(auth_info.host_url); - let auth_host = handler + let auth_host = app .auth_host_query() - .find_by_url(&mut transaction, &url) + .find_by_url(&mut executor, &url) .await?; let auth_host = if let Some(auth_host) = auth_host { auth_host } else { let auth_host = AuthHost::new(AuthHostId::default(), url); - handler - .auth_host_modifier() - .create(&mut transaction, &auth_host) + app.auth_host_modifier() + .create(&mut executor, &auth_host) .await?; auth_host }; let host_id = auth_host.into_destruct().id; - let auth_account_id = AuthAccountId::default(); - let create_command = AuthAccount::create(auth_account_id.clone(), host_id, client_id); - let create_event = handler - .event_modifier() - .persist_and_transform(&mut transaction, create_command) - .await?; - signal.emit(auth_account_id.clone()).await?; - let mut auth_account = None; - AuthAccount::apply(&mut auth_account, create_event)?; - if let Some(auth_account) = auth_account { - handler - .auth_account_modifier() - .create(&mut transaction, &auth_account) - .await?; - auth_account - } else { - return Err(Report::new(KernelError::Internal) - .attach_printable("Failed to create auth account")); - } + app.auth_account_command_processor() + .create(&mut executor, host_id, client_id) + .await? }; Ok(auth_account.id().clone()) } diff --git a/server/src/route/account.rs b/server/src/route/account.rs index 5536be1..7a2c5da 100644 --- a/server/src/route/account.rs +++ b/server/src/route/account.rs @@ -17,7 +17,6 @@ use axum_keycloak_auth::instance::KeycloakAuthInstance; use axum_keycloak_auth::layer::KeycloakAuthLayer; use axum_keycloak_auth::PassthroughMode; use serde::{Deserialize, Serialize}; -use std::ops::Deref; use time::OffsetDateTime; #[derive(Debug, Deserialize)] @@ -71,13 +70,9 @@ async fn get_accounts( ) -> Result, ErrorStatus> { expect_role!(&token, uri, method); let auth_info = KeycloakAuthAccount::from(token); - let auth_account_id = resolve_auth_account_id( - module.handler(), - module.applier_container().deref(), - auth_info, - ) - .await - .map_err(ErrorStatus::from)?; + let auth_account_id = resolve_auth_account_id(&module, auth_info) + .await + .map_err(ErrorStatus::from)?; let direction = direction.convert_to_direction()?; let pagination = Pagination::new(limit, cursor, direction); @@ -124,13 +119,9 @@ async fn create_account( ))); } - let auth_account_id = resolve_auth_account_id( - module.handler(), - module.applier_container().deref(), - auth_info, - ) - .await - .map_err(ErrorStatus::from)?; + let auth_account_id = resolve_auth_account_id(&module, auth_info) + .await + .map_err(ErrorStatus::from)?; let account = module .create_account(auth_account_id, request.name, request.is_bot) @@ -165,13 +156,9 @@ async fn get_account_by_id( ))); } - let auth_account_id = resolve_auth_account_id( - module.handler(), - module.applier_container().deref(), - auth_info, - ) - .await - .map_err(ErrorStatus::from)?; + let auth_account_id = resolve_auth_account_id(&module, auth_info) + .await + .map_err(ErrorStatus::from)?; let account = module .get_account_by_id(&auth_account_id, id) @@ -207,13 +194,9 @@ async fn update_account_by_id( ))); } - let auth_account_id = resolve_auth_account_id( - module.handler(), - module.applier_container().deref(), - auth_info, - ) - .await - .map_err(ErrorStatus::from)?; + let auth_account_id = resolve_auth_account_id(&module, auth_info) + .await + .map_err(ErrorStatus::from)?; module .edit_account(&auth_account_id, id, request.is_bot) @@ -240,13 +223,9 @@ async fn delete_account_by_id( ))); } - let auth_account_id = resolve_auth_account_id( - module.handler(), - module.applier_container().deref(), - auth_info, - ) - .await - .map_err(ErrorStatus::from)?; + let auth_account_id = resolve_auth_account_id(&module, auth_info) + .await + .map_err(ErrorStatus::from)?; module .delete_account(&auth_account_id, id) From 364ceb8a07da3cbf43d4818f4f0045e789085d18 Mon Sep 17 00:00:00 2001 From: turtton Date: Mon, 9 Mar 2026 19:12:48 +0900 Subject: [PATCH 50/59] :bug: Fix AuthAccount CQRS issues found in code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - AuthAccount::deleteに楽観的排他制御(current_version)を追加 - CommandProcessor::deleteのシグネチャにcurrent_versionを追加 - UpdateAuthAccount applierでDeleted後のprojection削除を処理 - UpdateAuthAccount applierでReadModelに未登録のケース(create)を処理 - EventStoreテストのヘルパー関数重複を解消(親mod testに共通化) --- adapter/src/processor/auth_account.rs | 4 +- application/src/service/auth_account.rs | 35 ++++++--- .../postgres/auth_account_event_store.rs | 77 +++++++------------ kernel/src/entity/auth_account.rs | 14 +++- 4 files changed, 67 insertions(+), 63 deletions(-) diff --git a/adapter/src/processor/auth_account.rs b/adapter/src/processor/auth_account.rs index a7bbb41..4e3a937 100644 --- a/adapter/src/processor/auth_account.rs +++ b/adapter/src/processor/auth_account.rs @@ -31,6 +31,7 @@ pub trait AuthAccountCommandProcessor: Send + Sync + 'static { &self, executor: &mut Self::Executor, auth_account_id: AuthAccountId, + current_version: kernel::prelude::entity::EventVersion, ) -> impl Future> + Send; } @@ -81,8 +82,9 @@ where &self, executor: &mut Self::Executor, auth_account_id: AuthAccountId, + current_version: kernel::prelude::entity::EventVersion, ) -> error_stack::Result<(), KernelError> { - let command = AuthAccount::delete(auth_account_id.clone()); + let command = AuthAccount::delete(auth_account_id.clone(), current_version); self.auth_account_event_store() .persist_and_transform(executor, command) diff --git a/application/src/service/auth_account.rs b/application/src/service/auth_account.rs index 2df6ee9..666fea6 100644 --- a/application/src/service/auth_account.rs +++ b/application/src/service/auth_account.rs @@ -1,4 +1,3 @@ -use error_stack::Report; use kernel::interfaces::database::{DatabaseConnection, DependOnDatabaseConnection}; use kernel::interfaces::event::EventApplier; use kernel::interfaces::event_store::{AuthAccountEventStore, DependOnAuthAccountEventStore}; @@ -16,12 +15,13 @@ pub trait UpdateAuthAccount: ) -> impl Future> { async move { let mut transaction = self.database_connection().begin_transaction().await?; - let auth_account = self + let existing = self .auth_account_read_model() .find_by_id(&mut transaction, &auth_account_id) .await?; - if let Some(auth_account) = auth_account { - let event_id = EventId::from(auth_account.id().clone()); + let event_id = EventId::from(auth_account_id.clone()); + + if let Some(auth_account) = existing { let events = self .auth_account_event_store() .find_by_id(&mut transaction, &event_id, Some(auth_account.version())) @@ -29,7 +29,7 @@ pub trait UpdateAuthAccount: if events .last() .map(|event| &event.version != auth_account.version()) - .unwrap_or_else(|| false) + .unwrap_or(false) { let mut auth_account = Some(auth_account); for event in events { @@ -40,16 +40,29 @@ pub trait UpdateAuthAccount: .update(&mut transaction, &auth_account) .await?; } else { - return Err(Report::new(KernelError::Internal) - .attach_printable("Failed to get auth account")); + self.auth_account_read_model() + .delete(&mut transaction, &auth_account_id) + .await?; } } - Ok(()) } else { - Err(Report::new(KernelError::Internal).attach_printable(format!( - "Failed to get target auth account: {auth_account_id:?}" - ))) + let events = self + .auth_account_event_store() + .find_by_id(&mut transaction, &event_id, None) + .await?; + if !events.is_empty() { + let mut auth_account = None; + for event in events { + AuthAccount::apply(&mut auth_account, event)?; + } + if let Some(auth_account) = auth_account { + self.auth_account_read_model() + .create(&mut transaction, &auth_account) + .await?; + } + } } + Ok(()) } } } diff --git a/driver/src/database/postgres/auth_account_event_store.rs b/driver/src/database/postgres/auth_account_event_store.rs index 3cbd8bb..4a1a03a 100644 --- a/driver/src/database/postgres/auth_account_event_store.rs +++ b/driver/src/database/postgres/auth_account_event_store.rs @@ -198,27 +198,27 @@ impl DependOnAuthAccountEventStore for PostgresDatabase { #[cfg(test)] mod test { + use crate::database::PostgresDatabase; + use kernel::interfaces::database::DatabaseConnection; + use kernel::interfaces::event_store::{AuthAccountEventStore, DependOnAuthAccountEventStore}; + use kernel::prelude::entity::{ + AuthAccount, AuthAccountClientId, AuthAccountEvent, AuthAccountId, AuthHostId, + CommandEnvelope, EventId, + }; + use uuid::Uuid; + + fn create_auth_account_command( + id: AuthAccountId, + ) -> CommandEnvelope { + AuthAccount::create( + id, + AuthHostId::new(Uuid::now_v7()), + AuthAccountClientId::new("test_client"), + ) + } + mod query { - use crate::database::PostgresDatabase; - use kernel::interfaces::database::DatabaseConnection; - use kernel::interfaces::event_store::{ - AuthAccountEventStore, DependOnAuthAccountEventStore, - }; - use kernel::prelude::entity::{ - AuthAccount, AuthAccountClientId, AuthAccountEvent, AuthAccountId, AuthHostId, - CommandEnvelope, EventId, - }; - use uuid::Uuid; - - fn create_auth_account_command( - id: AuthAccountId, - ) -> CommandEnvelope { - AuthAccount::create( - id, - AuthHostId::new(Uuid::now_v7()), - AuthAccountClientId::new("test_client"), - ) - } + use super::*; #[test_with::env(DATABASE_URL)] #[tokio::test] @@ -235,12 +235,12 @@ mod test { assert_eq!(events.len(), 0); let created = create_auth_account_command(id.clone()); - let deleted = AuthAccount::delete(id.clone()); - - db.auth_account_event_store() - .persist(&mut transaction, &created) + let create_envelope = db + .auth_account_event_store() + .persist_and_transform(&mut transaction, created.clone()) .await .unwrap(); + let deleted = AuthAccount::delete(id.clone(), create_envelope.version); db.auth_account_event_store() .persist(&mut transaction, &deleted) .await @@ -265,12 +265,12 @@ mod test { let event_id = EventId::from(id.clone()); let created = create_auth_account_command(id.clone()); - let deleted = AuthAccount::delete(id.clone()); - - db.auth_account_event_store() - .persist(&mut transaction, &created) + let create_envelope = db + .auth_account_event_store() + .persist_and_transform(&mut transaction, created.clone()) .await .unwrap(); + let deleted = AuthAccount::delete(id.clone(), create_envelope.version); db.auth_account_event_store() .persist(&mut transaction, &deleted) .await @@ -301,26 +301,7 @@ mod test { } mod persist { - use crate::database::PostgresDatabase; - use kernel::interfaces::database::DatabaseConnection; - use kernel::interfaces::event_store::{ - AuthAccountEventStore, DependOnAuthAccountEventStore, - }; - use kernel::prelude::entity::{ - AuthAccount, AuthAccountClientId, AuthAccountEvent, AuthAccountId, AuthHostId, - CommandEnvelope, EventId, - }; - use uuid::Uuid; - - fn create_auth_account_command( - id: AuthAccountId, - ) -> CommandEnvelope { - AuthAccount::create( - id, - AuthHostId::new(Uuid::now_v7()), - AuthAccountClientId::new("test_client"), - ) - } + use super::*; #[test_with::env(DATABASE_URL)] #[tokio::test] diff --git a/kernel/src/entity/auth_account.rs b/kernel/src/entity/auth_account.rs index eb87c07..7ceca96 100644 --- a/kernel/src/entity/auth_account.rs +++ b/kernel/src/entity/auth_account.rs @@ -50,9 +50,17 @@ impl AuthAccount { ) } - pub fn delete(id: AuthAccountId) -> CommandEnvelope { + pub fn delete( + id: AuthAccountId, + current_version: EventVersion, + ) -> CommandEnvelope { let event = AuthAccountEvent::Deleted; - CommandEnvelope::new(EventId::from(id), event.name(), event, None) + CommandEnvelope::new( + EventId::from(id), + event.name(), + event, + Some(KnownEventVersion::Prev(current_version)), + ) } } @@ -131,7 +139,7 @@ mod test { client_id.clone(), EventVersion::new(Uuid::now_v7()), ); - let delete_account = AuthAccount::delete(id.clone()); + let delete_account = AuthAccount::delete(id.clone(), account.version().clone()); let envelope = EventEnvelope::new( delete_account.id().clone(), delete_account.event().clone(), From 3e9a5e4d8e19b87ba33bb1e7bce3e8eeaf018943 Mon Sep 17 00:00:00 2001 From: turtton Date: Mon, 9 Mar 2026 20:49:11 +0900 Subject: [PATCH 51/59] :memo: Fix CLAUDE.md discrepancies found by code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 依存グラフの修正、エンティティ分類の明確化、applicationサービスの記述追加 --- CLAUDE.md | 100 ++++++++++++++++++++++++++++++++++++++++++------------ 1 file changed, 78 insertions(+), 22 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 76596f1..4338aad 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -50,43 +50,99 @@ Copy `.env.example` to `.env`: ## Architecture -### Workspace Structure (4 crates with dependency flow) +### Workspace Structure (5 crates with dependency flow) ``` -kernel → application → driver → server +kernel → adapter → application → server +kernel → driver → server ``` -- **kernel**: Domain entities, traits for Query/Modify/Event interfaces, Event Sourcing core -- **application**: Business logic services, cryptography (RSA-2048, Argon2id, AES-GCM), DTOs +- **kernel**: Domain entities, interface traits (EventStore, ReadModel, Query, Modify), Event Sourcing core. Traits are exposed via logical `pub mod interfaces {}` block in `lib.rs` (not a physical directory). +- **adapter**: CQRS processors (CommandProcessor/QueryProcessor) that compose kernel traits, crypto trait composition (SigningKeyGenerator) +- **application**: Use case services (Account CRUD use cases), event appliers (projection update), DTOs - **driver**: PostgreSQL/Redis implementations of kernel interfaces -- **server**: Axum HTTP server, Keycloak auth, route handlers, event appliers +- **server**: Axum HTTP server, Keycloak auth, route handlers, DI wiring (Handler/AppModule) -### Event Sourcing Pattern +### CQRS + Event Sourcing Pattern -Commands create events via `CommandEnvelope` → persisted to `event_streams` table → applied via `EventApplier` trait → projections updated in entity tables. +Two entity types exist in the codebase: **CQRS-migrated** and **legacy (Query/Modifier)**. -Key components: -- `EventApplier` trait (kernel/src/event.rs): Defines how events reconstruct entity state -- `Signal` trait (kernel/src/signal.rs): Triggers async event application via Redis message queue -- `ApplierContainer` (server/src/applier.rs): Manages entity-specific appliers using rikka-mq +#### CQRS-migrated entities (Account, AuthAccount) -### Interface Trait Pattern +Each CQRS entity has these components across layers: -Kernel defines interface traits, driver implements them: -- `DatabaseConnection` / `Transaction`: Database abstraction -- `*Query` traits: Read operations (e.g., `AccountQuery`) -- `*Modifier` traits: Write operations (e.g., `AccountModifier`) -- `DependOn*` traits: Dependency injection pattern +``` +Command Flow: + REST handler → CommandProcessor (adapter) + → EventStore.persist_and_transform() (kernel trait, driver impl) + → EventApplier (kernel) → entity reconstruction + → [AuthAccount only: ReadModel.create() for immediate consistency] + → Signal → async applier → ReadModel projection update + +Query Flow: + REST handler → QueryProcessor (adapter) + → ReadModel.find_*() (kernel trait, driver impl) +``` + +**kernel** defines per-entity interface traits: +- `AccountEventStore` / `AuthAccountEventStore` — event persistence + retrieval per entity-specific table +- `AccountReadModel` / `AuthAccountReadModel` — projection reads + writes (replaces old Query + Modifier) + +**adapter** provides processors with blanket impls: +- `AccountCommandProcessor` — EventStore + EventApplier + Signal (projection via async applier) +- `AuthAccountCommandProcessor` — EventStore + EventApplier + ReadModel.create() + Signal (synchronous projection for find-or-create pattern) +- `*QueryProcessor` — ReadModel facade + +**driver** implements per-entity stores: +- `PostgresAccountEventStore` → `account_events` table +- `PostgresAuthAccountEventStore` → `auth_account_events` table +- `PostgresAccountReadModel` → `accounts` table +- `PostgresAuthAccountReadModel` → `auth_accounts` table + +**application** provides use case services and event appliers: +- `GetAccountUseCase` / `CreateAccountUseCase` / `EditAccountUseCase` / `DeleteAccountUseCase` — Account CRUD orchestration via CommandProcessor/QueryProcessor +- `UpdateAuthAccount` — event applier that replays events from EventStore, updates/creates/deletes ReadModel projection + +#### Legacy entities (Profile, Metadata, Follow, RemoteAccount — with EventApplier) and (AuthHost, Image — pure CRUD) + +These still use the older Query/Modifier pattern directly: +- `*Query` traits in `kernel/src/query/` — read operations +- `*Modifier` traits in `kernel/src/modify/` — write operations (direct INSERT/UPDATE/DELETE) +- Profile, Metadata, Follow, RemoteAccount have Event enums + EventApplier (ready for CQRS migration) +- AuthHost, Image have no Event enum or EventApplier (pure CRUD entities) +- No per-entity EventStore or ReadModel yet + +### Key Patterns + +**DependOn\* trait pattern**: Dependency injection via associated types. `DependOnFoo` provides `fn foo(&self) -> &Self::Foo`. Blanket impls auto-wire when dependencies are satisfied. + +**impl_database_delegation! macro** (kernel/src/lib.rs): Delegates all database `DependOn*` traits from a wrapper type to a database field. Used by `Handler` to wire `PostgresDatabase`. + +**EventApplier trait** (kernel/src/event.rs): Reconstructs entity state from events. `fn apply(entity: &mut Option, event: EventEnvelope) -> Result<()>`. Entity becomes `None` on Deleted events. + +**Optimistic concurrency control**: Commands carry `prev_version: Option`. `KnownEventVersion::Nothing` = must be first event, `KnownEventVersion::Prev(version)` = must match latest version. EventStore validates before persisting. + +**Signal → Applier pipeline**: `Signal` trait emits entity IDs via Redis (rikka-mq). `ApplierContainer` (server/src/applier.rs) receives and dispatches to entity-specific appliers that update ReadModel projections. ### Entity Structure -Entities use vodca macros (`References`, `Newln`, `Nameln`) and destructure for field access. Each entity has: -- ID type (UUID-based) -- Event enum with variants (Created, Updated, Deleted) +Entities use vodca macros (`References`, `Newln`, `Nameln`) and `destructure::Destructure` for field access. + +Event Sourcing対象エンティティ (Account, AuthAccount, Profile, Metadata, Follow, RemoteAccount): +- ID type (UUIDv7-based, provides temporal ordering) +- Event enum with variants (Created, Updated, Deleted) + `Nameln` for event name serialization - `EventApplier` implementation +- `CommandEnvelope` factory methods (e.g., `Account::create()`, `Account::delete()`) + +純粋CRUDエンティティ (AuthHost, Image): +- ID type のみ。Event enum / EventApplier なし + +### Server DI Architecture + +`Handler` — owns PostgresDatabase + RedisDatabase + crypto providers. `impl_database_delegation!` wires kernel traits. -Domain entities: Account, AuthAccount, AuthHost, Profile, Metadata, Follow, Image, RemoteAccount +`AppModule` — wraps `Arc` + `Arc`. Manually implements `DependOn*` for adapter-layer traits (Signal, ReadModel, EventStore). Blanket impls provide CommandProcessor/QueryProcessor automatically. ### Testing -Database tests use `#[test_with::env(DATABASE_URL)]` attribute to skip when database is unavailable. \ No newline at end of file +Database tests use `#[test_with::env(DATABASE_URL)]` attribute to skip when database is unavailable. From 149875af12b572ad731fa9821f77e0b84ccb0528 Mon Sep 17 00:00:00 2001 From: turtton Date: Tue, 10 Mar 2026 00:09:50 +0900 Subject: [PATCH 52/59] :recycle: Apply CQRS to Profile/Metadata and Repository pattern to Follow/RemoteAccount/Image/AuthHost MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Profile/MetadataにCQRS + Event Sourcingパターンを適用: - kernel: EventStore/ReadModelトレイト定義、ProfileEvent::Deleted追加、楽観的並行制御 - adapter: CommandProcessor/QueryProcessor(blanket impl) - application: CRUD UseCase + EventApplierサービス + DTO - driver: PostgreSQL EventStore/ReadModel実装 - server: REST API、Applier、DI配線 - migration: profile_events/metadata_eventsテーブル Follow/RemoteAccount/Image/AuthHostをRepositoryパターンに移行: - kernel: Repositoryトレイト定義(Query + Modifier統合) - Follow/RemoteAccount: Event enum, EventApplier, CommandEnvelope削除 - driver: Repository impl書き換え(SQL変更なし) - legacy Query/Modifierモジュール完全削除 - impl_database_delegation!マクロ + AppModule DI更新 --- CLAUDE.md | 38 +- Cargo.lock | 1 + adapter/Cargo.toml | 1 + adapter/src/processor.rs | 2 + adapter/src/processor/metadata.rs | 210 ++++++++ adapter/src/processor/profile.rs | 234 +++++++++ application/Cargo.toml | 4 +- application/src/service.rs | 2 + application/src/service/metadata.rs | 353 ++++++++++++++ application/src/service/profile.rs | 368 ++++++++++++++ application/src/transfer.rs | 2 + application/src/transfer/metadata.rs | 52 ++ application/src/transfer/profile.rs | 94 ++++ driver/src/database/postgres.rs | 2 + driver/src/database/postgres/auth_account.rs | 12 +- driver/src/database/postgres/auth_host.rs | 46 +- driver/src/database/postgres/follow.rs | 79 ++- driver/src/database/postgres/image.rs | 48 +- driver/src/database/postgres/metadata.rs | 315 ++++++------ .../database/postgres/metadata_event_store.rs | 445 +++++++++++++++++ driver/src/database/postgres/profile.rs | 310 +++++++----- .../database/postgres/profile_event_store.rs | 448 ++++++++++++++++++ .../src/database/postgres/remote_account.rs | 69 ++- kernel/src/entity/follow.rs | 179 +------ kernel/src/entity/follow/id.rs | 7 - kernel/src/entity/metadata.rs | 29 +- kernel/src/entity/profile.rs | 77 ++- kernel/src/entity/remote_account.rs | 162 +------ kernel/src/entity/remote_account/id.rs | 7 - kernel/src/event_store.rs | 4 + kernel/src/event_store/metadata.rs | 40 ++ kernel/src/event_store/profile.rs | 38 ++ kernel/src/lib.rs | 78 ++- kernel/src/modify.rs | 8 - kernel/src/modify/auth_host.rs | 28 -- kernel/src/modify/image.rs | 28 -- kernel/src/modify/metadata.rs | 34 -- kernel/src/modify/profile.rs | 28 -- kernel/src/modify/remote_account.rs | 34 -- kernel/src/query.rs | 8 - kernel/src/query/follow.rs | 28 -- kernel/src/query/metadata.rs | 28 -- kernel/src/query/profile.rs | 22 - kernel/src/read_model.rs | 4 + kernel/src/read_model/metadata.rs | 48 ++ kernel/src/read_model/profile.rs | 48 ++ kernel/src/repository.rs | 9 + kernel/src/{query => repository}/auth_host.rs | 22 +- kernel/src/{modify => repository}/follow.rs | 22 +- kernel/src/{query => repository}/image.rs | 20 +- .../{query => repository}/remote_account.rs | 26 +- migrations/20260310000000_profile_events.sql | 10 + migrations/20260311000000_metadata_events.sql | 8 + server/src/applier.rs | 12 +- server/src/applier/metadata_applier.rs | 41 ++ server/src/applier/profile_applier.rs | 41 ++ server/src/handler.rs | 91 +++- server/src/keycloak.rs | 7 +- server/src/main.rs | 9 +- server/src/route.rs | 2 + server/src/route/account.rs | 5 +- server/src/route/metadata.rs | 235 +++++++++ server/src/route/profile.rs | 262 ++++++++++ 63 files changed, 3805 insertions(+), 1119 deletions(-) create mode 100644 adapter/src/processor/metadata.rs create mode 100644 adapter/src/processor/profile.rs create mode 100644 application/src/service/metadata.rs create mode 100644 application/src/service/profile.rs create mode 100644 application/src/transfer/metadata.rs create mode 100644 application/src/transfer/profile.rs create mode 100644 driver/src/database/postgres/metadata_event_store.rs create mode 100644 driver/src/database/postgres/profile_event_store.rs create mode 100644 kernel/src/event_store/metadata.rs create mode 100644 kernel/src/event_store/profile.rs delete mode 100644 kernel/src/modify.rs delete mode 100644 kernel/src/modify/auth_host.rs delete mode 100644 kernel/src/modify/image.rs delete mode 100644 kernel/src/modify/metadata.rs delete mode 100644 kernel/src/modify/profile.rs delete mode 100644 kernel/src/modify/remote_account.rs delete mode 100644 kernel/src/query.rs delete mode 100644 kernel/src/query/follow.rs delete mode 100644 kernel/src/query/metadata.rs delete mode 100644 kernel/src/query/profile.rs create mode 100644 kernel/src/read_model/metadata.rs create mode 100644 kernel/src/read_model/profile.rs create mode 100644 kernel/src/repository.rs rename kernel/src/{query => repository}/auth_host.rs (51%) rename kernel/src/{modify => repository}/follow.rs (52%) rename kernel/src/{query => repository}/image.rs (53%) rename kernel/src/{query => repository}/remote_account.rs (53%) create mode 100644 migrations/20260310000000_profile_events.sql create mode 100644 migrations/20260311000000_metadata_events.sql create mode 100644 server/src/applier/metadata_applier.rs create mode 100644 server/src/applier/profile_applier.rs create mode 100644 server/src/route/metadata.rs create mode 100644 server/src/route/profile.rs diff --git a/CLAUDE.md b/CLAUDE.md index 4338aad..2d18853 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -57,7 +57,7 @@ kernel → adapter → application → server kernel → driver → server ``` -- **kernel**: Domain entities, interface traits (EventStore, ReadModel, Query, Modify), Event Sourcing core. Traits are exposed via logical `pub mod interfaces {}` block in `lib.rs` (not a physical directory). +- **kernel**: Domain entities, interface traits (EventStore, ReadModel, Repository), Event Sourcing core. Traits are exposed via logical `pub mod interfaces {}` block in `lib.rs` (not a physical directory). - **adapter**: CQRS processors (CommandProcessor/QueryProcessor) that compose kernel traits, crypto trait composition (SigningKeyGenerator) - **application**: Use case services (Account CRUD use cases), event appliers (projection update), DTOs - **driver**: PostgreSQL/Redis implementations of kernel interfaces @@ -67,7 +67,7 @@ kernel → driver → server Two entity types exist in the codebase: **CQRS-migrated** and **legacy (Query/Modifier)**. -#### CQRS-migrated entities (Account, AuthAccount) +#### CQRS-migrated entities (Account, AuthAccount, Profile, Metadata) Each CQRS entity has these components across layers: @@ -85,32 +85,37 @@ Query Flow: ``` **kernel** defines per-entity interface traits: -- `AccountEventStore` / `AuthAccountEventStore` — event persistence + retrieval per entity-specific table -- `AccountReadModel` / `AuthAccountReadModel` — projection reads + writes (replaces old Query + Modifier) +- `AccountEventStore` / `AuthAccountEventStore` / `ProfileEventStore` / `MetadataEventStore` — event persistence + retrieval per entity-specific table +- `AccountReadModel` / `AuthAccountReadModel` / `ProfileReadModel` / `MetadataReadModel` — projection reads + writes **adapter** provides processors with blanket impls: -- `AccountCommandProcessor` — EventStore + EventApplier + Signal (projection via async applier) +- `AccountCommandProcessor` / `ProfileCommandProcessor` / `MetadataCommandProcessor` — EventStore + EventApplier + Signal (projection via async applier) - `AuthAccountCommandProcessor` — EventStore + EventApplier + ReadModel.create() + Signal (synchronous projection for find-or-create pattern) - `*QueryProcessor` — ReadModel facade **driver** implements per-entity stores: - `PostgresAccountEventStore` → `account_events` table - `PostgresAuthAccountEventStore` → `auth_account_events` table +- `PostgresProfileEventStore` → `profile_events` table +- `PostgresMetadataEventStore` → `metadata_events` table - `PostgresAccountReadModel` → `accounts` table - `PostgresAuthAccountReadModel` → `auth_accounts` table +- `PostgresProfileReadModel` → `profiles` table +- `PostgresMetadataReadModel` → `metadatas` table **application** provides use case services and event appliers: - `GetAccountUseCase` / `CreateAccountUseCase` / `EditAccountUseCase` / `DeleteAccountUseCase` — Account CRUD orchestration via CommandProcessor/QueryProcessor -- `UpdateAuthAccount` — event applier that replays events from EventStore, updates/creates/deletes ReadModel projection +- `GetProfileUseCase` / `CreateProfileUseCase` / `EditProfileUseCase` / `DeleteProfileUseCase` — Profile CRUD +- `GetMetadataUseCase` / `CreateMetadataUseCase` / `EditMetadataUseCase` / `DeleteMetadataUseCase` — Metadata CRUD +- `UpdateAuthAccount` / `UpdateProfile` / `UpdateMetadata` — event appliers that replay events from EventStore, update ReadModel projections -#### Legacy entities (Profile, Metadata, Follow, RemoteAccount — with EventApplier) and (AuthHost, Image — pure CRUD) +#### Repository entities (Follow, RemoteAccount, Image, AuthHost) -These still use the older Query/Modifier pattern directly: -- `*Query` traits in `kernel/src/query/` — read operations -- `*Modifier` traits in `kernel/src/modify/` — write operations (direct INSERT/UPDATE/DELETE) -- Profile, Metadata, Follow, RemoteAccount have Event enums + EventApplier (ready for CQRS migration) -- AuthHost, Image have no Event enum or EventApplier (pure CRUD entities) -- No per-entity EventStore or ReadModel yet +These use the Repository pattern — a single trait combining read and write operations: +- `*Repository` traits in `kernel/src/repository/` — unified CRUD interface +- `Postgres*Repository` driver implementations in `driver/src/database/postgres/` +- Follow and RemoteAccount are pure CRUD (Event Sourcing removed) +- AuthHost and Image are pure CRUD (never had Event Sourcing) ### Key Patterns @@ -128,20 +133,21 @@ These still use the older Query/Modifier pattern directly: Entities use vodca macros (`References`, `Newln`, `Nameln`) and `destructure::Destructure` for field access. -Event Sourcing対象エンティティ (Account, AuthAccount, Profile, Metadata, Follow, RemoteAccount): +Event Sourcing対象エンティティ (Account, AuthAccount, Profile, Metadata): - ID type (UUIDv7-based, provides temporal ordering) - Event enum with variants (Created, Updated, Deleted) + `Nameln` for event name serialization - `EventApplier` implementation - `CommandEnvelope` factory methods (e.g., `Account::create()`, `Account::delete()`) -純粋CRUDエンティティ (AuthHost, Image): +純粋CRUDエンティティ (Follow, RemoteAccount, AuthHost, Image): - ID type のみ。Event enum / EventApplier なし +- Repository パターンで直接 CRUD 操作 ### Server DI Architecture `Handler` — owns PostgresDatabase + RedisDatabase + crypto providers. `impl_database_delegation!` wires kernel traits. -`AppModule` — wraps `Arc` + `Arc`. Manually implements `DependOn*` for adapter-layer traits (Signal, ReadModel, EventStore). Blanket impls provide CommandProcessor/QueryProcessor automatically. +`AppModule` — wraps `Arc` + `Arc`. Manually implements `DependOn*` for adapter-layer traits (Signal, ReadModel, EventStore, Repository). Blanket impls provide CommandProcessor/QueryProcessor automatically. ### Testing diff --git a/Cargo.lock b/Cargo.lock index 495f314..98a00d5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -9,6 +9,7 @@ dependencies = [ "error-stack", "kernel", "tracing", + "uuid", "zeroize", ] diff --git a/adapter/Cargo.toml b/adapter/Cargo.toml index 7731026..a8c619f 100644 --- a/adapter/Cargo.toml +++ b/adapter/Cargo.toml @@ -8,4 +8,5 @@ authors.workspace = true kernel = { path = "../kernel" } error-stack = { workspace = true } tracing = { workspace = true } +uuid = { workspace = true } zeroize = "1.7" diff --git a/adapter/src/processor.rs b/adapter/src/processor.rs index a3f9ff7..63247ca 100644 --- a/adapter/src/processor.rs +++ b/adapter/src/processor.rs @@ -1,2 +1,4 @@ pub mod account; pub mod auth_account; +pub mod metadata; +pub mod profile; diff --git a/adapter/src/processor/metadata.rs b/adapter/src/processor/metadata.rs new file mode 100644 index 0000000..ce0f944 --- /dev/null +++ b/adapter/src/processor/metadata.rs @@ -0,0 +1,210 @@ +use error_stack::Report; +use kernel::interfaces::database::{DatabaseConnection, DependOnDatabaseConnection, Executor}; +use kernel::interfaces::event::EventApplier; +use kernel::interfaces::event_store::{DependOnMetadataEventStore, MetadataEventStore}; +use kernel::interfaces::read_model::{DependOnMetadataReadModel, MetadataReadModel}; +use kernel::interfaces::signal::Signal; +use kernel::prelude::entity::{ + AccountId, EventVersion, Metadata, MetadataContent, MetadataId, MetadataLabel, Nanoid, +}; +use kernel::KernelError; +use std::future::Future; + +// --- Signal DI trait (adapter-specific) --- + +pub trait DependOnMetadataSignal: Send + Sync { + type MetadataSignal: Signal + Send + Sync + 'static; + fn metadata_signal(&self) -> &Self::MetadataSignal; +} + +// --- MetadataCommandProcessor --- + +pub trait MetadataCommandProcessor: Send + Sync + 'static { + type Executor: Executor; + + fn create( + &self, + executor: &mut Self::Executor, + account_id: AccountId, + label: MetadataLabel, + content: MetadataContent, + nano_id: Nanoid, + ) -> impl Future> + Send; + + fn update( + &self, + executor: &mut Self::Executor, + metadata_id: MetadataId, + label: MetadataLabel, + content: MetadataContent, + current_version: EventVersion, + ) -> impl Future> + Send; + + fn delete( + &self, + executor: &mut Self::Executor, + metadata_id: MetadataId, + current_version: EventVersion, + ) -> impl Future> + Send; +} + +impl MetadataCommandProcessor for T +where + T: DependOnMetadataEventStore + DependOnMetadataSignal + Send + Sync + 'static, +{ + type Executor = + <::MetadataEventStore as MetadataEventStore>::Executor; + + async fn create( + &self, + executor: &mut Self::Executor, + account_id: AccountId, + label: MetadataLabel, + content: MetadataContent, + nano_id: Nanoid, + ) -> error_stack::Result { + let metadata_id = MetadataId::new(uuid::Uuid::now_v7()); + let command = Metadata::create(metadata_id.clone(), account_id, label, content, nano_id); + + let event_envelope = self + .metadata_event_store() + .persist_and_transform(executor, command) + .await?; + + let mut metadata = None; + Metadata::apply(&mut metadata, event_envelope)?; + let metadata = metadata.ok_or_else(|| { + Report::new(KernelError::Internal) + .attach_printable("Failed to construct metadata from created event") + })?; + + if let Err(e) = self.metadata_signal().emit(metadata_id).await { + tracing::warn!("Failed to emit metadata signal: {:?}", e); + } + + Ok(metadata) + } + + async fn update( + &self, + executor: &mut Self::Executor, + metadata_id: MetadataId, + label: MetadataLabel, + content: MetadataContent, + current_version: EventVersion, + ) -> error_stack::Result<(), KernelError> { + let command = Metadata::update(metadata_id.clone(), label, content, current_version); + + self.metadata_event_store() + .persist_and_transform(executor, command) + .await?; + + if let Err(e) = self.metadata_signal().emit(metadata_id).await { + tracing::warn!("Failed to emit metadata signal: {:?}", e); + } + + Ok(()) + } + + async fn delete( + &self, + executor: &mut Self::Executor, + metadata_id: MetadataId, + current_version: EventVersion, + ) -> error_stack::Result<(), KernelError> { + let command = Metadata::delete(metadata_id.clone(), current_version); + + self.metadata_event_store() + .persist_and_transform(executor, command) + .await?; + + if let Err(e) = self.metadata_signal().emit(metadata_id).await { + tracing::warn!("Failed to emit metadata signal: {:?}", e); + } + + Ok(()) + } +} + +pub trait DependOnMetadataCommandProcessor: DependOnDatabaseConnection + Send + Sync { + type MetadataCommandProcessor: MetadataCommandProcessor< + Executor = <::DatabaseConnection as DatabaseConnection>::Executor, + >; + fn metadata_command_processor(&self) -> &Self::MetadataCommandProcessor; +} + +impl DependOnMetadataCommandProcessor for T +where + T: DependOnMetadataEventStore + + DependOnMetadataSignal + + DependOnDatabaseConnection + + Send + + Sync + + 'static, +{ + type MetadataCommandProcessor = Self; + fn metadata_command_processor(&self) -> &Self::MetadataCommandProcessor { + self + } +} + +// --- MetadataQueryProcessor --- + +pub trait MetadataQueryProcessor: Send + Sync + 'static { + type Executor: Executor; + + fn find_by_id( + &self, + executor: &mut Self::Executor, + id: &MetadataId, + ) -> impl Future, KernelError>> + Send; + + fn find_by_account_id( + &self, + executor: &mut Self::Executor, + account_id: &AccountId, + ) -> impl Future, KernelError>> + Send; +} + +impl MetadataQueryProcessor for T +where + T: DependOnMetadataReadModel + Send + Sync + 'static, +{ + type Executor = + <::MetadataReadModel as MetadataReadModel>::Executor; + + async fn find_by_id( + &self, + executor: &mut Self::Executor, + id: &MetadataId, + ) -> error_stack::Result, KernelError> { + self.metadata_read_model().find_by_id(executor, id).await + } + + async fn find_by_account_id( + &self, + executor: &mut Self::Executor, + account_id: &AccountId, + ) -> error_stack::Result, KernelError> { + self.metadata_read_model() + .find_by_account_id(executor, account_id) + .await + } +} + +pub trait DependOnMetadataQueryProcessor: DependOnDatabaseConnection + Send + Sync { + type MetadataQueryProcessor: MetadataQueryProcessor< + Executor = <::DatabaseConnection as DatabaseConnection>::Executor, + >; + fn metadata_query_processor(&self) -> &Self::MetadataQueryProcessor; +} + +impl DependOnMetadataQueryProcessor for T +where + T: DependOnMetadataReadModel + DependOnDatabaseConnection + Send + Sync + 'static, +{ + type MetadataQueryProcessor = Self; + fn metadata_query_processor(&self) -> &Self::MetadataQueryProcessor { + self + } +} diff --git a/adapter/src/processor/profile.rs b/adapter/src/processor/profile.rs new file mode 100644 index 0000000..2304850 --- /dev/null +++ b/adapter/src/processor/profile.rs @@ -0,0 +1,234 @@ +use error_stack::Report; +use kernel::interfaces::database::{DatabaseConnection, DependOnDatabaseConnection, Executor}; +use kernel::interfaces::event::EventApplier; +use kernel::interfaces::event_store::{DependOnProfileEventStore, ProfileEventStore}; +use kernel::interfaces::read_model::{DependOnProfileReadModel, ProfileReadModel}; +use kernel::interfaces::signal::Signal; +use kernel::prelude::entity::{ + AccountId, EventVersion, ImageId, Nanoid, Profile, ProfileDisplayName, ProfileId, + ProfileSummary, +}; +use kernel::KernelError; +use std::future::Future; + +// --- Signal DI trait (adapter-specific) --- + +pub trait DependOnProfileSignal: Send + Sync { + type ProfileSignal: Signal + Send + Sync + 'static; + fn profile_signal(&self) -> &Self::ProfileSignal; +} + +// --- ProfileCommandProcessor --- + +pub trait ProfileCommandProcessor: Send + Sync + 'static { + type Executor: Executor; + + fn create( + &self, + executor: &mut Self::Executor, + account_id: AccountId, + display_name: Option, + summary: Option, + icon: Option, + banner: Option, + nano_id: Nanoid, + ) -> impl Future> + Send; + + fn update( + &self, + executor: &mut Self::Executor, + profile_id: ProfileId, + display_name: Option, + summary: Option, + icon: Option, + banner: Option, + current_version: EventVersion, + ) -> impl Future> + Send; + + fn delete( + &self, + executor: &mut Self::Executor, + profile_id: ProfileId, + current_version: EventVersion, + ) -> impl Future> + Send; +} + +impl ProfileCommandProcessor for T +where + T: DependOnProfileEventStore + DependOnProfileSignal + Send + Sync + 'static, +{ + type Executor = + <::ProfileEventStore as ProfileEventStore>::Executor; + + async fn create( + &self, + executor: &mut Self::Executor, + account_id: AccountId, + display_name: Option, + summary: Option, + icon: Option, + banner: Option, + nano_id: Nanoid, + ) -> error_stack::Result { + let profile_id = ProfileId::new(uuid::Uuid::now_v7()); + let command = Profile::create( + profile_id.clone(), + account_id, + display_name, + summary, + icon, + banner, + nano_id, + ); + + let event_envelope = self + .profile_event_store() + .persist_and_transform(executor, command) + .await?; + + let mut profile = None; + Profile::apply(&mut profile, event_envelope)?; + let profile = profile.ok_or_else(|| { + Report::new(KernelError::Internal) + .attach_printable("Failed to construct profile from created event") + })?; + + if let Err(e) = self.profile_signal().emit(profile_id).await { + tracing::warn!("Failed to emit profile signal: {:?}", e); + } + + Ok(profile) + } + + async fn update( + &self, + executor: &mut Self::Executor, + profile_id: ProfileId, + display_name: Option, + summary: Option, + icon: Option, + banner: Option, + current_version: EventVersion, + ) -> error_stack::Result<(), KernelError> { + let command = Profile::update( + profile_id.clone(), + display_name, + summary, + icon, + banner, + current_version, + ); + + self.profile_event_store() + .persist_and_transform(executor, command) + .await?; + + if let Err(e) = self.profile_signal().emit(profile_id).await { + tracing::warn!("Failed to emit profile signal: {:?}", e); + } + + Ok(()) + } + + async fn delete( + &self, + executor: &mut Self::Executor, + profile_id: ProfileId, + current_version: EventVersion, + ) -> error_stack::Result<(), KernelError> { + let command = Profile::delete(profile_id.clone(), current_version); + + self.profile_event_store() + .persist_and_transform(executor, command) + .await?; + + if let Err(e) = self.profile_signal().emit(profile_id).await { + tracing::warn!("Failed to emit profile signal: {:?}", e); + } + + Ok(()) + } +} + +pub trait DependOnProfileCommandProcessor: DependOnDatabaseConnection + Send + Sync { + type ProfileCommandProcessor: ProfileCommandProcessor< + Executor = <::DatabaseConnection as DatabaseConnection>::Executor, + >; + fn profile_command_processor(&self) -> &Self::ProfileCommandProcessor; +} + +impl DependOnProfileCommandProcessor for T +where + T: DependOnProfileEventStore + + DependOnProfileSignal + + DependOnDatabaseConnection + + Send + + Sync + + 'static, +{ + type ProfileCommandProcessor = Self; + fn profile_command_processor(&self) -> &Self::ProfileCommandProcessor { + self + } +} + +// --- ProfileQueryProcessor --- + +pub trait ProfileQueryProcessor: Send + Sync + 'static { + type Executor: Executor; + + fn find_by_id( + &self, + executor: &mut Self::Executor, + id: &ProfileId, + ) -> impl Future, KernelError>> + Send; + + fn find_by_account_id( + &self, + executor: &mut Self::Executor, + account_id: &AccountId, + ) -> impl Future, KernelError>> + Send; +} + +impl ProfileQueryProcessor for T +where + T: DependOnProfileReadModel + Send + Sync + 'static, +{ + type Executor = + <::ProfileReadModel as ProfileReadModel>::Executor; + + async fn find_by_id( + &self, + executor: &mut Self::Executor, + id: &ProfileId, + ) -> error_stack::Result, KernelError> { + self.profile_read_model().find_by_id(executor, id).await + } + + async fn find_by_account_id( + &self, + executor: &mut Self::Executor, + account_id: &AccountId, + ) -> error_stack::Result, KernelError> { + self.profile_read_model() + .find_by_account_id(executor, account_id) + .await + } +} + +pub trait DependOnProfileQueryProcessor: DependOnDatabaseConnection + Send + Sync { + type ProfileQueryProcessor: ProfileQueryProcessor< + Executor = <::DatabaseConnection as DatabaseConnection>::Executor, + >; + fn profile_query_processor(&self) -> &Self::ProfileQueryProcessor; +} + +impl DependOnProfileQueryProcessor for T +where + T: DependOnProfileReadModel + DependOnDatabaseConnection + Send + Sync + 'static, +{ + type ProfileQueryProcessor = Self; + fn profile_query_processor(&self) -> &Self::ProfileQueryProcessor { + self + } +} diff --git a/application/Cargo.toml b/application/Cargo.toml index 375f067..858dc6b 100644 --- a/application/Cargo.toml +++ b/application/Cargo.toml @@ -18,10 +18,10 @@ serde_json = "1" adapter = { path = "../adapter" } kernel = { path = "../kernel" } +uuid = { workspace = true } + [dev-dependencies] tokio = { workspace = true, features = ["macros", "test-util"] } tempfile = "3" -uuid.workspace = true - driver.path = "../driver" \ No newline at end of file diff --git a/application/src/service.rs b/application/src/service.rs index a3f9ff7..63247ca 100644 --- a/application/src/service.rs +++ b/application/src/service.rs @@ -1,2 +1,4 @@ pub mod account; pub mod auth_account; +pub mod metadata; +pub mod profile; diff --git a/application/src/service/metadata.rs b/application/src/service/metadata.rs new file mode 100644 index 0000000..d9d718b --- /dev/null +++ b/application/src/service/metadata.rs @@ -0,0 +1,353 @@ +use crate::transfer::metadata::MetadataDto; +use adapter::processor::account::{AccountQueryProcessor, DependOnAccountQueryProcessor}; +use adapter::processor::metadata::{ + DependOnMetadataCommandProcessor, DependOnMetadataQueryProcessor, MetadataCommandProcessor, + MetadataQueryProcessor, +}; +use error_stack::Report; +use kernel::interfaces::database::{DatabaseConnection, DependOnDatabaseConnection}; +use kernel::interfaces::event::EventApplier; +use kernel::interfaces::event_store::{DependOnMetadataEventStore, MetadataEventStore}; +use kernel::interfaces::read_model::{DependOnMetadataReadModel, MetadataReadModel}; +use kernel::prelude::entity::{ + Account, AuthAccountId, EventId, Metadata, MetadataContent, MetadataId, MetadataLabel, Nanoid, +}; +use kernel::KernelError; +use std::future::Future; + +pub trait UpdateMetadata: + 'static + DependOnDatabaseConnection + DependOnMetadataReadModel + DependOnMetadataEventStore +{ + fn update_metadata( + &self, + metadata_id: MetadataId, + ) -> impl Future> { + async move { + let mut transaction = self.database_connection().begin_transaction().await?; + let existing = self + .metadata_read_model() + .find_by_id(&mut transaction, &metadata_id) + .await?; + let event_id = EventId::from(metadata_id.clone()); + + if let Some(metadata) = existing { + let events = self + .metadata_event_store() + .find_by_id(&mut transaction, &event_id, Some(metadata.version())) + .await?; + if events + .last() + .map(|event| &event.version != metadata.version()) + .unwrap_or(false) + { + let mut metadata = Some(metadata); + for event in events { + Metadata::apply(&mut metadata, event)?; + } + if let Some(metadata) = metadata { + self.metadata_read_model() + .update(&mut transaction, &metadata) + .await?; + } else { + self.metadata_read_model() + .delete(&mut transaction, &metadata_id) + .await?; + } + } + } else { + let events = self + .metadata_event_store() + .find_by_id(&mut transaction, &event_id, None) + .await?; + if !events.is_empty() { + let mut metadata = None; + for event in events { + Metadata::apply(&mut metadata, event)?; + } + if let Some(metadata) = metadata { + self.metadata_read_model() + .create(&mut transaction, &metadata) + .await?; + } + } + } + Ok(()) + } + } +} + +impl UpdateMetadata for T where + T: 'static + + DependOnDatabaseConnection + + DependOnMetadataReadModel + + DependOnMetadataEventStore +{ +} + +pub trait GetMetadataUseCase: + 'static + Sync + Send + DependOnMetadataQueryProcessor + DependOnAccountQueryProcessor +{ + fn get_metadata( + &self, + auth_account_id: &AuthAccountId, + account_nanoid: String, + ) -> impl Future, KernelError>> + Send { + async move { + let mut transaction = self.database_connection().begin_transaction().await?; + + let nanoid = kernel::prelude::entity::Nanoid::::new(account_nanoid); + let account = self + .account_query_processor() + .find_by_nanoid(&mut transaction, &nanoid) + .await? + .ok_or_else(|| { + Report::new(KernelError::NotFound).attach_printable(format!( + "Account not found with nanoid: {}", + nanoid.as_ref() + )) + })?; + + let accounts = self + .account_query_processor() + .find_by_auth_id(&mut transaction, auth_account_id) + .await?; + + let found = accounts.iter().any(|a| a.id() == account.id()); + if !found { + return Err(Report::new(KernelError::PermissionDenied) + .attach_printable("This account does not belong to the authenticated user")); + } + + let metadata_list = self + .metadata_query_processor() + .find_by_account_id(&mut transaction, account.id()) + .await?; + + Ok(metadata_list.into_iter().map(MetadataDto::from).collect()) + } + } +} + +impl GetMetadataUseCase for T where + T: 'static + Sync + Send + DependOnMetadataQueryProcessor + DependOnAccountQueryProcessor +{ +} + +pub trait CreateMetadataUseCase: + 'static + Sync + Send + DependOnMetadataCommandProcessor + DependOnAccountQueryProcessor +{ + fn create_metadata( + &self, + auth_account_id: &AuthAccountId, + account_nanoid: String, + label: String, + content: String, + ) -> impl Future> + Send { + async move { + let mut transaction = self.database_connection().begin_transaction().await?; + + let nanoid = kernel::prelude::entity::Nanoid::::new(account_nanoid); + let account = self + .account_query_processor() + .find_by_nanoid(&mut transaction, &nanoid) + .await? + .ok_or_else(|| { + Report::new(KernelError::NotFound).attach_printable(format!( + "Account not found with nanoid: {}", + nanoid.as_ref() + )) + })?; + + let accounts = self + .account_query_processor() + .find_by_auth_id(&mut transaction, auth_account_id) + .await?; + + let found = accounts.iter().any(|a| a.id() == account.id()); + if !found { + return Err(Report::new(KernelError::PermissionDenied) + .attach_printable("This account does not belong to the authenticated user")); + } + + let account_id = account.id().clone(); + let metadata_nanoid = Nanoid::::default(); + let metadata = self + .metadata_command_processor() + .create( + &mut transaction, + account_id, + MetadataLabel::new(label), + MetadataContent::new(content), + metadata_nanoid, + ) + .await?; + + Ok(MetadataDto::from(metadata)) + } + } +} + +impl CreateMetadataUseCase for T where + T: 'static + Sync + Send + DependOnMetadataCommandProcessor + DependOnAccountQueryProcessor +{ +} + +pub trait EditMetadataUseCase: + 'static + + Sync + + Send + + DependOnMetadataCommandProcessor + + DependOnMetadataQueryProcessor + + DependOnAccountQueryProcessor +{ + fn edit_metadata( + &self, + auth_account_id: &AuthAccountId, + account_nanoid: String, + metadata_nanoid: String, + label: String, + content: String, + ) -> impl Future> + Send { + async move { + let mut transaction = self.database_connection().begin_transaction().await?; + + let nanoid = kernel::prelude::entity::Nanoid::::new(account_nanoid); + let account = self + .account_query_processor() + .find_by_nanoid(&mut transaction, &nanoid) + .await? + .ok_or_else(|| { + Report::new(KernelError::NotFound).attach_printable(format!( + "Account not found with nanoid: {}", + nanoid.as_ref() + )) + })?; + + let accounts = self + .account_query_processor() + .find_by_auth_id(&mut transaction, auth_account_id) + .await?; + + let found = accounts.iter().any(|a| a.id() == account.id()); + if !found { + return Err(Report::new(KernelError::PermissionDenied) + .attach_printable("This account does not belong to the authenticated user")); + } + + let metadata_list = self + .metadata_query_processor() + .find_by_account_id(&mut transaction, account.id()) + .await?; + + let metadata = metadata_list + .into_iter() + .find(|m| m.nanoid().as_ref() == &metadata_nanoid) + .ok_or_else(|| { + Report::new(KernelError::NotFound).attach_printable(format!( + "Metadata not found with nanoid: {}", + metadata_nanoid + )) + })?; + + let metadata_id = metadata.id().clone(); + let current_version = metadata.version().clone(); + self.metadata_command_processor() + .update( + &mut transaction, + metadata_id, + MetadataLabel::new(label), + MetadataContent::new(content), + current_version, + ) + .await?; + + Ok(()) + } + } +} + +impl EditMetadataUseCase for T where + T: 'static + + Sync + + Send + + DependOnMetadataCommandProcessor + + DependOnMetadataQueryProcessor + + DependOnAccountQueryProcessor +{ +} + +pub trait DeleteMetadataUseCase: + 'static + + Sync + + Send + + DependOnMetadataCommandProcessor + + DependOnMetadataQueryProcessor + + DependOnAccountQueryProcessor +{ + fn delete_metadata( + &self, + auth_account_id: &AuthAccountId, + account_nanoid: String, + metadata_nanoid: String, + ) -> impl Future> + Send { + async move { + let mut transaction = self.database_connection().begin_transaction().await?; + + let nanoid = kernel::prelude::entity::Nanoid::::new(account_nanoid); + let account = self + .account_query_processor() + .find_by_nanoid(&mut transaction, &nanoid) + .await? + .ok_or_else(|| { + Report::new(KernelError::NotFound).attach_printable(format!( + "Account not found with nanoid: {}", + nanoid.as_ref() + )) + })?; + + let accounts = self + .account_query_processor() + .find_by_auth_id(&mut transaction, auth_account_id) + .await?; + + let found = accounts.iter().any(|a| a.id() == account.id()); + if !found { + return Err(Report::new(KernelError::PermissionDenied) + .attach_printable("This account does not belong to the authenticated user")); + } + + let metadata_list = self + .metadata_query_processor() + .find_by_account_id(&mut transaction, account.id()) + .await?; + + let metadata = metadata_list + .into_iter() + .find(|m| m.nanoid().as_ref() == &metadata_nanoid) + .ok_or_else(|| { + Report::new(KernelError::NotFound).attach_printable(format!( + "Metadata not found with nanoid: {}", + metadata_nanoid + )) + })?; + + let metadata_id = metadata.id().clone(); + let current_version = metadata.version().clone(); + self.metadata_command_processor() + .delete(&mut transaction, metadata_id, current_version) + .await?; + + Ok(()) + } + } +} + +impl DeleteMetadataUseCase for T where + T: 'static + + Sync + + Send + + DependOnMetadataCommandProcessor + + DependOnMetadataQueryProcessor + + DependOnAccountQueryProcessor +{ +} diff --git a/application/src/service/profile.rs b/application/src/service/profile.rs new file mode 100644 index 0000000..ce8691b --- /dev/null +++ b/application/src/service/profile.rs @@ -0,0 +1,368 @@ +use crate::transfer::profile::ProfileDto; +use adapter::processor::account::{AccountQueryProcessor, DependOnAccountQueryProcessor}; +use adapter::processor::profile::{ + DependOnProfileCommandProcessor, DependOnProfileQueryProcessor, ProfileCommandProcessor, + ProfileQueryProcessor, +}; +use error_stack::Report; +use kernel::interfaces::database::{DatabaseConnection, DependOnDatabaseConnection}; +use kernel::interfaces::event::EventApplier; +use kernel::interfaces::event_store::{DependOnProfileEventStore, ProfileEventStore}; +use kernel::interfaces::read_model::{DependOnProfileReadModel, ProfileReadModel}; +use kernel::prelude::entity::{ + Account, AuthAccountId, EventId, ImageId, Nanoid, Profile, ProfileDisplayName, ProfileId, + ProfileSummary, +}; +use kernel::KernelError; +use std::future::Future; + +pub trait UpdateProfile: + 'static + DependOnDatabaseConnection + DependOnProfileReadModel + DependOnProfileEventStore +{ + fn update_profile( + &self, + profile_id: ProfileId, + ) -> impl Future> { + async move { + let mut transaction = self.database_connection().begin_transaction().await?; + let existing = self + .profile_read_model() + .find_by_id(&mut transaction, &profile_id) + .await?; + let event_id = EventId::from(profile_id.clone()); + + if let Some(profile) = existing { + let events = self + .profile_event_store() + .find_by_id(&mut transaction, &event_id, Some(profile.version())) + .await?; + if events + .last() + .map(|event| &event.version != profile.version()) + .unwrap_or(false) + { + let mut profile = Some(profile); + for event in events { + Profile::apply(&mut profile, event)?; + } + if let Some(profile) = profile { + self.profile_read_model() + .update(&mut transaction, &profile) + .await?; + } else { + self.profile_read_model() + .delete(&mut transaction, &profile_id) + .await?; + } + } + } else { + let events = self + .profile_event_store() + .find_by_id(&mut transaction, &event_id, None) + .await?; + if !events.is_empty() { + let mut profile = None; + for event in events { + Profile::apply(&mut profile, event)?; + } + if let Some(profile) = profile { + self.profile_read_model() + .create(&mut transaction, &profile) + .await?; + } + } + } + Ok(()) + } + } +} + +impl UpdateProfile for T where + T: 'static + DependOnDatabaseConnection + DependOnProfileReadModel + DependOnProfileEventStore +{ +} + +pub trait GetProfileUseCase: + 'static + Sync + Send + DependOnProfileQueryProcessor + DependOnAccountQueryProcessor +{ + fn get_profile( + &self, + auth_account_id: &AuthAccountId, + account_nanoid: String, + ) -> impl Future> + Send { + async move { + let mut transaction = self.database_connection().begin_transaction().await?; + + let nanoid = kernel::prelude::entity::Nanoid::::new(account_nanoid); + let account = self + .account_query_processor() + .find_by_nanoid(&mut transaction, &nanoid) + .await? + .ok_or_else(|| { + Report::new(KernelError::NotFound).attach_printable(format!( + "Account not found with nanoid: {}", + nanoid.as_ref() + )) + })?; + + let accounts = self + .account_query_processor() + .find_by_auth_id(&mut transaction, auth_account_id) + .await?; + + let found = accounts.iter().any(|a| a.id() == account.id()); + if !found { + return Err(Report::new(KernelError::PermissionDenied) + .attach_printable("This account does not belong to the authenticated user")); + } + + let profile = self + .profile_query_processor() + .find_by_account_id(&mut transaction, account.id()) + .await? + .ok_or_else(|| { + Report::new(KernelError::NotFound) + .attach_printable("Profile not found for this account") + })?; + + Ok(ProfileDto::from(profile)) + } + } +} + +impl GetProfileUseCase for T where + T: 'static + Sync + Send + DependOnProfileQueryProcessor + DependOnAccountQueryProcessor +{ +} + +pub trait CreateProfileUseCase: + 'static + + Sync + + Send + + DependOnProfileCommandProcessor + + DependOnProfileQueryProcessor + + DependOnAccountQueryProcessor +{ + fn create_profile( + &self, + auth_account_id: &AuthAccountId, + account_nanoid: String, + display_name: Option, + summary: Option, + icon: Option, + banner: Option, + ) -> impl Future> + Send { + async move { + let mut transaction = self.database_connection().begin_transaction().await?; + + let nanoid = kernel::prelude::entity::Nanoid::::new(account_nanoid); + let account = self + .account_query_processor() + .find_by_nanoid(&mut transaction, &nanoid) + .await? + .ok_or_else(|| { + Report::new(KernelError::NotFound).attach_printable(format!( + "Account not found with nanoid: {}", + nanoid.as_ref() + )) + })?; + + let accounts = self + .account_query_processor() + .find_by_auth_id(&mut transaction, auth_account_id) + .await?; + + let found = accounts.iter().any(|a| a.id() == account.id()); + if !found { + return Err(Report::new(KernelError::PermissionDenied) + .attach_printable("This account does not belong to the authenticated user")); + } + + let existing_profile = self + .profile_query_processor() + .find_by_account_id(&mut transaction, account.id()) + .await?; + if existing_profile.is_some() { + return Err(Report::new(KernelError::Concurrency) + .attach_printable("Profile already exists for this account")); + } + + let account_id = account.id().clone(); + let profile_nanoid = Nanoid::::default(); + let profile = self + .profile_command_processor() + .create( + &mut transaction, + account_id, + display_name.map(ProfileDisplayName::new), + summary.map(ProfileSummary::new), + icon, + banner, + profile_nanoid, + ) + .await?; + + Ok(ProfileDto::from(profile)) + } + } +} + +impl CreateProfileUseCase for T where + T: 'static + + Sync + + Send + + DependOnProfileCommandProcessor + + DependOnProfileQueryProcessor + + DependOnAccountQueryProcessor +{ +} + +pub trait EditProfileUseCase: + 'static + + Sync + + Send + + DependOnProfileCommandProcessor + + DependOnProfileQueryProcessor + + DependOnAccountQueryProcessor +{ + fn edit_profile( + &self, + auth_account_id: &AuthAccountId, + account_nanoid: String, + display_name: Option, + summary: Option, + icon: Option, + banner: Option, + ) -> impl Future> + Send { + async move { + let mut transaction = self.database_connection().begin_transaction().await?; + + let nanoid = kernel::prelude::entity::Nanoid::::new(account_nanoid); + let account = self + .account_query_processor() + .find_by_nanoid(&mut transaction, &nanoid) + .await? + .ok_or_else(|| { + Report::new(KernelError::NotFound).attach_printable(format!( + "Account not found with nanoid: {}", + nanoid.as_ref() + )) + })?; + + let accounts = self + .account_query_processor() + .find_by_auth_id(&mut transaction, auth_account_id) + .await?; + + let found = accounts.iter().any(|a| a.id() == account.id()); + if !found { + return Err(Report::new(KernelError::PermissionDenied) + .attach_printable("This account does not belong to the authenticated user")); + } + + let profile = self + .profile_query_processor() + .find_by_account_id(&mut transaction, account.id()) + .await? + .ok_or_else(|| { + Report::new(KernelError::NotFound) + .attach_printable("Profile not found for this account") + })?; + + let profile_id = profile.id().clone(); + let current_version = profile.version().clone(); + self.profile_command_processor() + .update( + &mut transaction, + profile_id, + display_name.map(ProfileDisplayName::new), + summary.map(ProfileSummary::new), + icon, + banner, + current_version, + ) + .await?; + + Ok(()) + } + } +} + +impl EditProfileUseCase for T where + T: 'static + + Sync + + Send + + DependOnProfileCommandProcessor + + DependOnProfileQueryProcessor + + DependOnAccountQueryProcessor +{ +} + +pub trait DeleteProfileUseCase: + 'static + + Sync + + Send + + DependOnProfileCommandProcessor + + DependOnProfileQueryProcessor + + DependOnAccountQueryProcessor +{ + fn delete_profile( + &self, + auth_account_id: &AuthAccountId, + account_nanoid: String, + ) -> impl Future> + Send { + async move { + let mut transaction = self.database_connection().begin_transaction().await?; + + let nanoid = kernel::prelude::entity::Nanoid::::new(account_nanoid); + let account = self + .account_query_processor() + .find_by_nanoid(&mut transaction, &nanoid) + .await? + .ok_or_else(|| { + Report::new(KernelError::NotFound).attach_printable(format!( + "Account not found with nanoid: {}", + nanoid.as_ref() + )) + })?; + + let accounts = self + .account_query_processor() + .find_by_auth_id(&mut transaction, auth_account_id) + .await?; + + let found = accounts.iter().any(|a| a.id() == account.id()); + if !found { + return Err(Report::new(KernelError::PermissionDenied) + .attach_printable("This account does not belong to the authenticated user")); + } + + let profile = self + .profile_query_processor() + .find_by_account_id(&mut transaction, account.id()) + .await? + .ok_or_else(|| { + Report::new(KernelError::NotFound) + .attach_printable("Profile not found for this account") + })?; + + let profile_id = profile.id().clone(); + let current_version = profile.version().clone(); + self.profile_command_processor() + .delete(&mut transaction, profile_id, current_version) + .await?; + + Ok(()) + } + } +} + +impl DeleteProfileUseCase for T where + T: 'static + + Sync + + Send + + DependOnProfileCommandProcessor + + DependOnProfileQueryProcessor + + DependOnAccountQueryProcessor +{ +} diff --git a/application/src/transfer.rs b/application/src/transfer.rs index 971413f..bf0d70b 100644 --- a/application/src/transfer.rs +++ b/application/src/transfer.rs @@ -1,2 +1,4 @@ pub mod account; +pub mod metadata; pub mod pagination; +pub mod profile; diff --git a/application/src/transfer/metadata.rs b/application/src/transfer/metadata.rs new file mode 100644 index 0000000..24b0b4e --- /dev/null +++ b/application/src/transfer/metadata.rs @@ -0,0 +1,52 @@ +use kernel::prelude::entity::Metadata; + +#[derive(Debug)] +pub struct MetadataDto { + pub nanoid: String, + pub label: String, + pub content: String, +} + +impl From for MetadataDto { + fn from(metadata: Metadata) -> Self { + Self { + nanoid: metadata.nanoid().as_ref().to_string(), + label: metadata.label().as_ref().to_string(), + content: metadata.content().as_ref().to_string(), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use kernel::prelude::entity::{ + AccountId, EventVersion, Metadata, MetadataContent, MetadataId, MetadataLabel, Nanoid, + }; + use uuid::Uuid; + + #[test] + fn test_metadata_dto_from_metadata() { + let metadata_id = MetadataId::new(Uuid::now_v7()); + let account_id = AccountId::new(Uuid::now_v7()); + let label = MetadataLabel::new("test label".to_string()); + let content = MetadataContent::new("test content".to_string()); + let nanoid = Nanoid::default(); + let version = EventVersion::new(Uuid::now_v7()); + + let metadata = Metadata::new( + metadata_id, + account_id, + label.clone(), + content.clone(), + version, + nanoid.clone(), + ); + + let dto = MetadataDto::from(metadata); + + assert_eq!(dto.nanoid, nanoid.as_ref().to_string()); + assert_eq!(dto.label, label.as_ref().to_string()); + assert_eq!(dto.content, content.as_ref().to_string()); + } +} diff --git a/application/src/transfer/profile.rs b/application/src/transfer/profile.rs new file mode 100644 index 0000000..0500927 --- /dev/null +++ b/application/src/transfer/profile.rs @@ -0,0 +1,94 @@ +use kernel::prelude::entity::Profile; +use uuid::Uuid; + +#[derive(Debug)] +pub struct ProfileDto { + pub nanoid: String, + pub display_name: Option, + pub summary: Option, + pub icon_id: Option, + pub banner_id: Option, +} + +impl From for ProfileDto { + fn from(profile: Profile) -> Self { + Self { + nanoid: profile.nanoid().as_ref().to_string(), + display_name: profile + .display_name() + .as_ref() + .map(|d| d.as_ref().to_string()), + summary: profile.summary().as_ref().map(|s| s.as_ref().to_string()), + icon_id: profile.icon().as_ref().map(|i| *i.as_ref()), + banner_id: profile.banner().as_ref().map(|b| *b.as_ref()), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use kernel::prelude::entity::{ + AccountId, EventVersion, ImageId, Nanoid, Profile, ProfileDisplayName, ProfileId, + ProfileSummary, + }; + use uuid::Uuid; + + #[test] + fn test_profile_dto_from_profile_with_all_fields() { + let profile_id = ProfileId::new(Uuid::now_v7()); + let account_id = AccountId::new(Uuid::now_v7()); + let nanoid = Nanoid::default(); + let display_name = ProfileDisplayName::new("Test User".to_string()); + let summary = ProfileSummary::new("A test summary".to_string()); + let icon_id = ImageId::new(Uuid::now_v7()); + let banner_id = ImageId::new(Uuid::now_v7()); + let version = EventVersion::new(Uuid::now_v7()); + + let profile = Profile::new( + profile_id, + account_id, + Some(display_name.clone()), + Some(summary.clone()), + Some(icon_id.clone()), + Some(banner_id.clone()), + version, + nanoid.clone(), + ); + + let dto = ProfileDto::from(profile); + + assert_eq!(dto.nanoid, nanoid.as_ref().to_string()); + assert_eq!(dto.display_name, Some(display_name.as_ref().to_string())); + assert_eq!(dto.summary, Some(summary.as_ref().to_string())); + assert_eq!(dto.icon_id, Some(*icon_id.as_ref())); + assert_eq!(dto.banner_id, Some(*banner_id.as_ref())); + } + + #[test] + fn test_profile_dto_from_profile_with_no_optional_fields() { + let profile_id = ProfileId::new(Uuid::now_v7()); + let account_id = AccountId::new(Uuid::now_v7()); + let nanoid = Nanoid::default(); + let version = EventVersion::new(Uuid::now_v7()); + + let profile = Profile::new( + profile_id, + account_id, + None, + None, + None, + None, + version, + nanoid.clone(), + ); + + let dto = ProfileDto::from(profile); + + assert_eq!(dto.nanoid, nanoid.as_ref().to_string()); + assert!(dto.display_name.is_none()); + assert!(dto.summary.is_none()); + assert!(dto.icon_id.is_none()); + assert!(dto.banner_id.is_none()); + } +} diff --git a/driver/src/database/postgres.rs b/driver/src/database/postgres.rs index 877ade1..c09bc56 100644 --- a/driver/src/database/postgres.rs +++ b/driver/src/database/postgres.rs @@ -6,7 +6,9 @@ mod auth_host; mod follow; mod image; mod metadata; +mod metadata_event_store; mod profile; +mod profile_event_store; mod remote_account; use crate::database::env; diff --git a/driver/src/database/postgres/auth_account.rs b/driver/src/database/postgres/auth_account.rs index 3ce0bc4..b4348f3 100644 --- a/driver/src/database/postgres/auth_account.rs +++ b/driver/src/database/postgres/auth_account.rs @@ -152,8 +152,8 @@ mod test { mod query { use crate::database::PostgresDatabase; use kernel::interfaces::database::DatabaseConnection; - use kernel::interfaces::modify::{AuthHostModifier, DependOnAuthHostModifier}; use kernel::interfaces::read_model::{AuthAccountReadModel, DependOnAuthAccountReadModel}; + use kernel::interfaces::repository::{AuthHostRepository, DependOnAuthHostRepository}; use kernel::prelude::entity::{ AuthAccount, AuthAccountClientId, AuthAccountId, AuthHost, AuthHostId, AuthHostUrl, EventVersion, @@ -169,7 +169,7 @@ mod test { let auth_host_id = AuthHostId::new(Uuid::now_v7()); let auth_host = AuthHost::new(auth_host_id.clone(), AuthHostUrl::new(Uuid::now_v7())); database - .auth_host_modifier() + .auth_host_repository() .create(&mut transaction, &auth_host) .await .unwrap(); @@ -203,8 +203,8 @@ mod test { mod modify { use crate::database::PostgresDatabase; use kernel::interfaces::database::DatabaseConnection; - use kernel::interfaces::modify::{AuthHostModifier, DependOnAuthHostModifier}; use kernel::interfaces::read_model::{AuthAccountReadModel, DependOnAuthAccountReadModel}; + use kernel::interfaces::repository::{AuthHostRepository, DependOnAuthHostRepository}; use kernel::prelude::entity::{ AuthAccount, AuthAccountClientId, AuthAccountId, AuthHost, AuthHostId, AuthHostUrl, EventVersion, @@ -221,7 +221,7 @@ mod test { let account_id = AuthAccountId::new(Uuid::now_v7()); let auth_host = AuthHost::new(host_id.clone(), AuthHostUrl::new(Uuid::now_v7())); database - .auth_host_modifier() + .auth_host_repository() .create(&mut transaction, &auth_host) .await .unwrap(); @@ -259,7 +259,7 @@ mod test { let account_id = AuthAccountId::new(Uuid::now_v7()); let auth_host = AuthHost::new(host_id.clone(), AuthHostUrl::new(Uuid::now_v7())); database - .auth_host_modifier() + .auth_host_repository() .create(&mut transaction, &auth_host) .await .unwrap(); @@ -307,7 +307,7 @@ mod test { let host_id = AuthHostId::new(Uuid::now_v7()); let auth_host = AuthHost::new(host_id.clone(), AuthHostUrl::new(Uuid::now_v7())); database - .auth_host_modifier() + .auth_host_repository() .create(&mut transaction, &auth_host) .await .unwrap(); diff --git a/driver/src/database/postgres/auth_host.rs b/driver/src/database/postgres/auth_host.rs index e5b5077..f645dc2 100644 --- a/driver/src/database/postgres/auth_host.rs +++ b/driver/src/database/postgres/auth_host.rs @@ -1,7 +1,6 @@ use crate::database::{PostgresConnection, PostgresDatabase}; use crate::ConvertError; -use kernel::interfaces::modify::{AuthHostModifier, DependOnAuthHostModifier}; -use kernel::interfaces::query::{AuthHostQuery, DependOnAuthHostQuery}; +use kernel::interfaces::repository::{AuthHostRepository, DependOnAuthHostRepository}; use kernel::prelude::entity::{AuthHost, AuthHostId, AuthHostUrl}; use kernel::KernelError; use sqlx::PgConnection; @@ -21,7 +20,7 @@ impl From for AuthHost { pub struct PostgresAuthHostRepository; -impl AuthHostQuery for PostgresAuthHostRepository { +impl AuthHostRepository for PostgresAuthHostRepository { type Executor = PostgresConnection; async fn find_by_id( &self, @@ -64,18 +63,7 @@ impl AuthHostQuery for PostgresAuthHostRepository { .convert_error() .map(|row| row.map(AuthHost::from)) } -} - -impl DependOnAuthHostQuery for PostgresDatabase { - type AuthHostQuery = PostgresAuthHostRepository; - fn auth_host_query(&self) -> &Self::AuthHostQuery { - &PostgresAuthHostRepository - } -} - -impl AuthHostModifier for PostgresAuthHostRepository { - type Executor = PostgresConnection; async fn create( &self, executor: &mut Self::Executor, @@ -120,10 +108,10 @@ impl AuthHostModifier for PostgresAuthHostRepository { } } -impl DependOnAuthHostModifier for PostgresDatabase { - type AuthHostModifier = PostgresAuthHostRepository; +impl DependOnAuthHostRepository for PostgresDatabase { + type AuthHostRepository = PostgresAuthHostRepository; - fn auth_host_modifier(&self) -> &Self::AuthHostModifier { + fn auth_host_repository(&self) -> &Self::AuthHostRepository { &PostgresAuthHostRepository } } @@ -141,8 +129,7 @@ mod test { use crate::database::postgres::auth_host::test::url; use crate::database::PostgresDatabase; use kernel::interfaces::database::DatabaseConnection; - use kernel::interfaces::modify::{AuthHostModifier, DependOnAuthHostModifier}; - use kernel::interfaces::query::{AuthHostQuery, DependOnAuthHostQuery}; + use kernel::interfaces::repository::{AuthHostRepository, DependOnAuthHostRepository}; use kernel::prelude::entity::{AuthHost, AuthHostId}; use uuid::Uuid; @@ -154,13 +141,13 @@ mod test { let auth_host = AuthHost::new(AuthHostId::new(Uuid::now_v7()), url()); database - .auth_host_modifier() + .auth_host_repository() .create(&mut transaction, &auth_host) .await .unwrap(); let found_auth_host = database - .auth_host_query() + .auth_host_repository() .find_by_id(&mut transaction, auth_host.id()) .await .unwrap() @@ -176,13 +163,13 @@ mod test { let auth_host = AuthHost::new(AuthHostId::new(Uuid::now_v7()), url()); database - .auth_host_modifier() + .auth_host_repository() .create(&mut transaction, &auth_host) .await .unwrap(); let found_auth_host = database - .auth_host_query() + .auth_host_repository() .find_by_url(&mut transaction, auth_host.url()) .await .unwrap() @@ -195,8 +182,7 @@ mod test { use crate::database::postgres::auth_host::test::url; use crate::database::PostgresDatabase; use kernel::interfaces::database::DatabaseConnection; - use kernel::interfaces::modify::{AuthHostModifier, DependOnAuthHostModifier}; - use kernel::interfaces::query::{AuthHostQuery, DependOnAuthHostQuery}; + use kernel::interfaces::repository::{AuthHostRepository, DependOnAuthHostRepository}; use kernel::prelude::entity::{AuthHost, AuthHostId}; use uuid::Uuid; @@ -208,13 +194,13 @@ mod test { let auth_host = AuthHost::new(AuthHostId::new(Uuid::now_v7()), url()); database - .auth_host_modifier() + .auth_host_repository() .create(&mut transaction, &auth_host) .await .unwrap(); let found_auth_host = database - .auth_host_query() + .auth_host_repository() .find_by_id(&mut transaction, auth_host.id()) .await .unwrap() @@ -230,20 +216,20 @@ mod test { let auth_host = AuthHost::new(AuthHostId::new(Uuid::now_v7()), url()); database - .auth_host_modifier() + .auth_host_repository() .create(&mut transaction, &auth_host) .await .unwrap(); let updated_auth_host = AuthHost::new(auth_host.id().clone(), url()); database - .auth_host_modifier() + .auth_host_repository() .update(&mut transaction, &updated_auth_host) .await .unwrap(); let found_auth_host = database - .auth_host_query() + .auth_host_repository() .find_by_id(&mut transaction, auth_host.id()) .await .unwrap() diff --git a/driver/src/database/postgres/follow.rs b/driver/src/database/postgres/follow.rs index 359bfd8..86c44f2 100644 --- a/driver/src/database/postgres/follow.rs +++ b/driver/src/database/postgres/follow.rs @@ -1,8 +1,7 @@ use crate::database::{PostgresConnection, PostgresDatabase}; use crate::ConvertError; use error_stack::Report; -use kernel::interfaces::modify::{DependOnFollowModifier, FollowModifier}; -use kernel::interfaces::query::{DependOnFollowQuery, FollowQuery}; +use kernel::interfaces::repository::{DependOnFollowRepository, FollowRepository}; use kernel::prelude::entity::{ AccountId, Follow, FollowApprovedAt, FollowId, FollowTargetId, RemoteAccountId, }; @@ -62,7 +61,14 @@ impl TryFrom for Follow { pub struct PostgresFollowRepository; -impl FollowQuery for PostgresFollowRepository { +fn split_follow_target_id(target_id: &FollowTargetId) -> (Option<&Uuid>, Option<&Uuid>) { + match target_id { + FollowTargetId::Local(account_id) => (Some(account_id.as_ref()), None), + FollowTargetId::Remote(remote_account_id) => (None, Some(remote_account_id.as_ref())), + } +} + +impl FollowRepository for PostgresFollowRepository { type Executor = PostgresConnection; async fn find_followings( @@ -130,25 +136,6 @@ impl FollowQuery for PostgresFollowRepository { .convert_error() .and_then(|rows| rows.into_iter().map(Follow::try_from).collect::>()) } -} - -impl DependOnFollowQuery for PostgresDatabase { - type FollowQuery = PostgresFollowRepository; - - fn follow_query(&self) -> &Self::FollowQuery { - &PostgresFollowRepository - } -} - -fn split_follow_target_id(target_id: &FollowTargetId) -> (Option<&Uuid>, Option<&Uuid>) { - match target_id { - FollowTargetId::Local(account_id) => (Some(account_id.as_ref()), None), - FollowTargetId::Remote(remote_account_id) => (None, Some(remote_account_id.as_ref())), - } -} - -impl FollowModifier for PostgresFollowRepository { - type Executor = PostgresConnection; async fn create( &self, @@ -223,10 +210,10 @@ impl FollowModifier for PostgresFollowRepository { } } -impl DependOnFollowModifier for PostgresDatabase { - type FollowModifier = PostgresFollowRepository; +impl DependOnFollowRepository for PostgresDatabase { + type FollowRepository = PostgresFollowRepository; - fn follow_modifier(&self) -> &Self::FollowModifier { + fn follow_repository(&self) -> &Self::FollowRepository { &PostgresFollowRepository } } @@ -236,9 +223,8 @@ mod test { mod query { use crate::database::PostgresDatabase; use kernel::interfaces::database::DatabaseConnection; - use kernel::interfaces::modify::{DependOnFollowModifier, FollowModifier}; - use kernel::interfaces::query::{DependOnFollowQuery, FollowQuery}; use kernel::interfaces::read_model::{AccountReadModel, DependOnAccountReadModel}; + use kernel::interfaces::repository::{DependOnFollowRepository, FollowRepository}; use kernel::prelude::entity::{ Account, AccountId, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, CreatedAt, EventVersion, Follow, FollowApprovedAt, FollowId, FollowTargetId, Nanoid, @@ -293,26 +279,26 @@ mod test { .unwrap(); database - .follow_modifier() + .follow_repository() .create(&mut transaction, &follow) .await .unwrap(); let followers = database - .follow_query() + .follow_repository() .find_followings(&mut transaction, &FollowTargetId::from(follower_id)) .await .unwrap(); assert_eq!(followers[0].id(), follow.id()); let followers = database - .follow_query() + .follow_repository() .find_followings(&mut transaction, &FollowTargetId::from(followee_id)) .await .unwrap(); assert!(followers.is_empty()); database - .follow_modifier() + .follow_repository() .delete(&mut transaction, follow.id()) .await .unwrap(); @@ -376,26 +362,26 @@ mod test { .unwrap(); database - .follow_modifier() + .follow_repository() .create(&mut transaction, &follow) .await .unwrap(); let followings = database - .follow_query() + .follow_repository() .find_followers(&mut transaction, &FollowTargetId::from(followee_id)) .await .unwrap(); assert_eq!(followings[0].id(), follow.id()); let followings = database - .follow_query() + .follow_repository() .find_followers(&mut transaction, &FollowTargetId::from(follower_id)) .await .unwrap(); assert!(followings.is_empty()); database - .follow_modifier() + .follow_repository() .delete(&mut transaction, follow.id()) .await .unwrap(); @@ -415,9 +401,8 @@ mod test { mod modify { use crate::database::PostgresDatabase; use kernel::interfaces::database::DatabaseConnection; - use kernel::interfaces::modify::{DependOnFollowModifier, FollowModifier}; - use kernel::interfaces::query::{DependOnFollowQuery, FollowQuery}; use kernel::interfaces::read_model::{AccountReadModel, DependOnAccountReadModel}; + use kernel::interfaces::repository::{DependOnFollowRepository, FollowRepository}; use kernel::prelude::entity::{ Account, AccountId, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, CreatedAt, EventVersion, Follow, FollowApprovedAt, FollowId, FollowTargetId, Nanoid, @@ -472,12 +457,12 @@ mod test { .unwrap(); database - .follow_modifier() + .follow_repository() .create(&mut transaction, &follow) .await .unwrap(); database - .follow_modifier() + .follow_repository() .delete(&mut transaction, follow.id()) .await .unwrap(); @@ -542,14 +527,14 @@ mod test { .unwrap(); let following = database - .follow_query() + .follow_repository() .find_followings(&mut transaction, &FollowTargetId::from(follower_id.clone())) .await .unwrap(); assert!(following.is_empty()); database - .follow_modifier() + .follow_repository() .create(&mut transaction, &follow) .await .unwrap(); @@ -562,19 +547,19 @@ mod test { ) .unwrap(); database - .follow_modifier() + .follow_repository() .update(&mut transaction, &follow) .await .unwrap(); let following = database - .follow_query() + .follow_repository() .find_followers(&mut transaction, &FollowTargetId::from(followee_id)) .await .unwrap(); assert_eq!(following[0].id(), follow.id()); database - .follow_modifier() + .follow_repository() .delete(&mut transaction, follow.id()) .await .unwrap(); @@ -638,19 +623,19 @@ mod test { .unwrap(); database - .follow_modifier() + .follow_repository() .create(&mut transaction, &follow) .await .unwrap(); database - .follow_modifier() + .follow_repository() .delete(&mut transaction, follow.id()) .await .unwrap(); let following = database - .follow_query() + .follow_repository() .find_followers(&mut transaction, &FollowTargetId::from(follower_id)) .await .unwrap(); diff --git a/driver/src/database/postgres/image.rs b/driver/src/database/postgres/image.rs index 9304eeb..ffcec81 100644 --- a/driver/src/database/postgres/image.rs +++ b/driver/src/database/postgres/image.rs @@ -1,7 +1,6 @@ use crate::database::{PostgresConnection, PostgresDatabase}; use crate::ConvertError; -use kernel::interfaces::modify::{DependOnImageModifier, ImageModifier}; -use kernel::interfaces::query::{DependOnImageQuery, ImageQuery}; +use kernel::interfaces::repository::{DependOnImageRepository, ImageRepository}; use kernel::prelude::entity::{Image, ImageBlurHash, ImageHash, ImageId, ImageUrl}; use kernel::KernelError; use sqlx::PgConnection; @@ -28,7 +27,7 @@ impl From for Image { pub struct PostgresImageRepository; -impl ImageQuery for PostgresImageRepository { +impl ImageRepository for PostgresImageRepository { type Executor = PostgresConnection; async fn find_by_id( @@ -68,18 +67,6 @@ impl ImageQuery for PostgresImageRepository { .convert_error() .map(|option| option.map(|row| row.into())) } -} - -impl DependOnImageQuery for PostgresDatabase { - type ImageQuery = PostgresImageRepository; - - fn image_query(&self) -> &Self::ImageQuery { - &PostgresImageRepository - } -} - -impl ImageModifier for PostgresImageRepository { - type Executor = PostgresConnection; async fn create( &self, @@ -123,10 +110,10 @@ impl ImageModifier for PostgresImageRepository { } } -impl DependOnImageModifier for PostgresDatabase { - type ImageModifier = PostgresImageRepository; +impl DependOnImageRepository for PostgresDatabase { + type ImageRepository = PostgresImageRepository; - fn image_modifier(&self) -> &Self::ImageModifier { + fn image_repository(&self) -> &Self::ImageRepository { &PostgresImageRepository } } @@ -148,8 +135,7 @@ mod test { use crate::database::postgres::image::test::url; use crate::database::PostgresDatabase; use kernel::interfaces::database::DatabaseConnection; - use kernel::interfaces::modify::{DependOnImageModifier, ImageModifier}; - use kernel::interfaces::query::{DependOnImageQuery, ImageQuery}; + use kernel::interfaces::repository::{DependOnImageRepository, ImageRepository}; use kernel::prelude::entity::{Image, ImageBlurHash, ImageHash, ImageId}; use uuid::Uuid; @@ -169,18 +155,18 @@ mod test { ); database - .image_modifier() + .image_repository() .create(&mut transaction, &image) .await .unwrap(); let result = database - .image_query() + .image_repository() .find_by_id(&mut transaction, &id) .await .unwrap(); assert_eq!(result, Some(image)); database - .image_modifier() + .image_repository() .delete(&mut transaction, &id) .await .unwrap(); @@ -202,18 +188,18 @@ mod test { ); database - .image_modifier() + .image_repository() .create(&mut transaction, &image) .await .unwrap(); let result = database - .image_query() + .image_repository() .find_by_url(&mut transaction, &url) .await .unwrap(); assert_eq!(result, Some(image.clone())); database - .image_modifier() + .image_repository() .delete(&mut transaction, image.id()) .await .unwrap(); @@ -224,7 +210,7 @@ mod test { use crate::database::postgres::image::test::url; use crate::database::PostgresDatabase; use kernel::interfaces::database::DatabaseConnection; - use kernel::interfaces::modify::{DependOnImageModifier, ImageModifier}; + use kernel::interfaces::repository::{DependOnImageRepository, ImageRepository}; use kernel::prelude::entity::{Image, ImageBlurHash, ImageHash, ImageId}; use uuid::Uuid; @@ -244,12 +230,12 @@ mod test { ); database - .image_modifier() + .image_repository() .create(&mut transaction, &image) .await .unwrap(); database - .image_modifier() + .image_repository() .delete(&mut transaction, image.id()) .await .unwrap(); @@ -271,12 +257,12 @@ mod test { ); database - .image_modifier() + .image_repository() .create(&mut transaction, &image) .await .unwrap(); database - .image_modifier() + .image_repository() .delete(&mut transaction, &id) .await .unwrap(); diff --git a/driver/src/database/postgres/metadata.rs b/driver/src/database/postgres/metadata.rs index 4dfd7f4..d8d24fe 100644 --- a/driver/src/database/postgres/metadata.rs +++ b/driver/src/database/postgres/metadata.rs @@ -1,7 +1,6 @@ use crate::database::{PostgresConnection, PostgresDatabase}; use crate::ConvertError; -use kernel::interfaces::modify::{DependOnMetadataModifier, MetadataModifier}; -use kernel::interfaces::query::{DependOnMetadataQuery, MetadataQuery}; +use kernel::interfaces::read_model::{DependOnMetadataReadModel, MetadataReadModel}; use kernel::prelude::entity::{ AccountId, EventVersion, Metadata, MetadataContent, MetadataId, MetadataLabel, Nanoid, }; @@ -32,15 +31,15 @@ impl From for Metadata { } } -pub struct PostgresMetadataRepository; +pub struct PostgresMetadataReadModel; -impl MetadataQuery for PostgresMetadataRepository { +impl MetadataReadModel for PostgresMetadataReadModel { type Executor = PostgresConnection; async fn find_by_id( &self, executor: &mut Self::Executor, - metadata_id: &MetadataId, + id: &MetadataId, ) -> error_stack::Result, KernelError> { let con: &mut PgConnection = executor; sqlx::query_as::<_, MetadataRow>( @@ -51,7 +50,7 @@ impl MetadataQuery for PostgresMetadataRepository { WHERE id = $1 "#, ) - .bind(metadata_id.as_ref()) + .bind(id.as_ref()) .fetch_optional(con) .await .convert_error() @@ -78,18 +77,6 @@ impl MetadataQuery for PostgresMetadataRepository { .convert_error() .map(|rows| rows.into_iter().map(|row| row.into()).collect()) } -} - -impl DependOnMetadataQuery for PostgresDatabase { - type MetadataQuery = PostgresMetadataRepository; - - fn metadata_query(&self) -> &Self::MetadataQuery { - &PostgresMetadataRepository - } -} - -impl MetadataModifier for PostgresMetadataRepository { - type Executor = PostgresConnection; async fn create( &self, @@ -161,29 +148,54 @@ impl MetadataModifier for PostgresMetadataRepository { } } -impl DependOnMetadataModifier for PostgresDatabase { - type MetadataModifier = PostgresMetadataRepository; +impl DependOnMetadataReadModel for PostgresDatabase { + type MetadataReadModel = PostgresMetadataReadModel; - fn metadata_modifier(&self) -> &Self::MetadataModifier { - &PostgresMetadataRepository + fn metadata_read_model(&self) -> &Self::MetadataReadModel { + &PostgresMetadataReadModel } } #[cfg(test)] mod test { - - mod query { + mod read_model { use crate::database::PostgresDatabase; use kernel::interfaces::database::DatabaseConnection; - use kernel::interfaces::modify::{DependOnMetadataModifier, MetadataModifier}; - use kernel::interfaces::query::{DependOnMetadataQuery, MetadataQuery}; - use kernel::interfaces::read_model::{AccountReadModel, DependOnAccountReadModel}; + use kernel::interfaces::read_model::{ + AccountReadModel, DependOnAccountReadModel, DependOnMetadataReadModel, + MetadataReadModel, + }; use kernel::prelude::entity::{ Account, AccountId, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, CreatedAt, EventVersion, Metadata, MetadataContent, MetadataId, MetadataLabel, Nanoid, }; use uuid::Uuid; + fn make_account(account_id: AccountId) -> Account { + Account::new( + account_id, + AccountName::new("name"), + AccountPrivateKey::new("private_key"), + AccountPublicKey::new("public_key"), + AccountIsBot::new(false), + None, + EventVersion::new(Uuid::now_v7()), + Nanoid::default(), + CreatedAt::now(), + ) + } + + fn make_metadata(metadata_id: MetadataId, account_id: AccountId) -> Metadata { + Metadata::new( + metadata_id, + account_id, + MetadataLabel::new("label"), + MetadataContent::new("content"), + EventVersion::new(Uuid::now_v7()), + Nanoid::default(), + ) + } + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn find_by_id() { @@ -191,45 +203,41 @@ mod test { let mut transaction = database.begin_transaction().await.unwrap(); let account_id = AccountId::new(Uuid::now_v7()); - - let account = Account::new( - account_id.clone(), - AccountName::new("name".to_string()), - AccountPrivateKey::new("private_key".to_string()), - AccountPublicKey::new("public_key".to_string()), - AccountIsBot::new(false), - None, - EventVersion::new(Uuid::now_v7()), - Nanoid::default(), - CreatedAt::now(), - ); + let account = make_account(account_id.clone()); + let metadata_id = MetadataId::new(Uuid::now_v7()); + let metadata = make_metadata(metadata_id.clone(), account_id.clone()); database .account_read_model() .create(&mut transaction, &account) .await .unwrap(); - let metadata = Metadata::new( - MetadataId::new(Uuid::now_v7()), - account_id.clone(), - MetadataLabel::new("label".to_string()), - MetadataContent::new("content".to_string()), - EventVersion::new(Uuid::now_v7()), - Nanoid::default(), - ); - database - .metadata_modifier() + .metadata_read_model() .create(&mut transaction, &metadata) .await .unwrap(); let found = database - .metadata_query() - .find_by_id(&mut transaction, metadata.id()) + .metadata_read_model() + .find_by_id(&mut transaction, &metadata_id) .await .unwrap(); assert_eq!(found.as_ref().map(Metadata::id), Some(metadata.id())); + + // Non-existent id returns None + let not_found = database + .metadata_read_model() + .find_by_id(&mut transaction, &MetadataId::new(Uuid::now_v7())) + .await + .unwrap(); + assert!(not_found.is_none()); + + database + .account_read_model() + .delete(&mut transaction, account.id()) + .await + .unwrap(); } #[test_with::env(DATABASE_URL)] @@ -239,16 +247,16 @@ mod test { let mut transaction = database.begin_transaction().await.unwrap(); let account_id = AccountId::new(Uuid::now_v7()); - let account = Account::new( + let account = make_account(account_id.clone()); + + let metadata1 = make_metadata(MetadataId::new(Uuid::now_v7()), account_id.clone()); + let metadata2 = Metadata::new( + MetadataId::new(Uuid::now_v7()), account_id.clone(), - AccountName::new("name".to_string()), - AccountPrivateKey::new("private_key".to_string()), - AccountPublicKey::new("public_key".to_string()), - AccountIsBot::new(false), - None, + MetadataLabel::new("label2"), + MetadataContent::new("content2"), EventVersion::new(Uuid::now_v7()), Nanoid::default(), - CreatedAt::now(), ); database @@ -256,56 +264,41 @@ mod test { .create(&mut transaction, &account) .await .unwrap(); - let metadata = Metadata::new( - MetadataId::new(Uuid::now_v7()), - account_id.clone(), - MetadataLabel::new("label".to_string()), - MetadataContent::new("content".to_string()), - EventVersion::new(Uuid::now_v7()), - Nanoid::default(), - ); - let metadata2 = Metadata::new( - MetadataId::new(Uuid::now_v7()), - account_id.clone(), - MetadataLabel::new("label2".to_string()), - MetadataContent::new("content2".to_string()), - EventVersion::new(Uuid::now_v7()), - Nanoid::default(), - ); - database - .metadata_modifier() - .create(&mut transaction, &metadata) + .metadata_read_model() + .create(&mut transaction, &metadata1) .await .unwrap(); database - .metadata_modifier() + .metadata_read_model() .create(&mut transaction, &metadata2) .await .unwrap(); let found = database - .metadata_query() + .metadata_read_model() .find_by_account_id(&mut transaction, &account_id) .await .unwrap(); - assert_eq!( - found.iter().map(Metadata::id).collect::>(), - vec![metadata.id(), metadata2.id()] - ); + assert_eq!(found.len(), 2); + let ids: Vec<_> = found.iter().map(Metadata::id).collect(); + assert!(ids.contains(&metadata1.id())); + assert!(ids.contains(&metadata2.id())); + + // Non-existent account_id returns empty vec + let not_found = database + .metadata_read_model() + .find_by_account_id(&mut transaction, &AccountId::new(Uuid::now_v7())) + .await + .unwrap(); + assert!(not_found.is_empty()); + + database + .account_read_model() + .delete(&mut transaction, account.id()) + .await + .unwrap(); } - } - mod modify { - use crate::database::PostgresDatabase; - use kernel::interfaces::database::DatabaseConnection; - use kernel::interfaces::modify::{DependOnMetadataModifier, MetadataModifier}; - use kernel::interfaces::query::{DependOnMetadataQuery, MetadataQuery}; - use kernel::interfaces::read_model::{AccountReadModel, DependOnAccountReadModel}; - use kernel::prelude::entity::{ - Account, AccountId, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, - CreatedAt, EventVersion, Metadata, MetadataContent, MetadataId, MetadataLabel, Nanoid, - }; - use uuid::Uuid; #[test_with::env(DATABASE_URL)] #[tokio::test] @@ -314,44 +307,36 @@ mod test { let mut transaction = database.begin_transaction().await.unwrap(); let account_id = AccountId::new(Uuid::now_v7()); - let account = Account::new( - account_id.clone(), - AccountName::new("name".to_string()), - AccountPrivateKey::new("private_key".to_string()), - AccountPublicKey::new("public_key".to_string()), - AccountIsBot::new(false), - None, - EventVersion::new(Uuid::now_v7()), - Nanoid::default(), - CreatedAt::now(), - ); + let account = make_account(account_id.clone()); + let metadata_id = MetadataId::new(Uuid::now_v7()); + let metadata = make_metadata(metadata_id.clone(), account_id.clone()); database .account_read_model() .create(&mut transaction, &account) .await .unwrap(); - let metadata = Metadata::new( - MetadataId::new(Uuid::now_v7()), - account_id.clone(), - MetadataLabel::new("label".to_string()), - MetadataContent::new("content".to_string()), - EventVersion::new(Uuid::now_v7()), - Nanoid::default(), - ); - database - .metadata_modifier() + .metadata_read_model() .create(&mut transaction, &metadata) .await .unwrap(); let found = database - .metadata_query() - .find_by_id(&mut transaction, metadata.id()) + .metadata_read_model() + .find_by_id(&mut transaction, &metadata_id) + .await + .unwrap() + .unwrap(); + assert_eq!(found.id(), metadata.id()); + assert_eq!(found.label(), metadata.label()); + assert_eq!(found.content(), metadata.content()); + + database + .account_read_model() + .delete(&mut transaction, account.id()) .await .unwrap(); - assert_eq!(found.as_ref().map(Metadata::id), Some(metadata.id())); } #[test_with::env(DATABASE_URL)] @@ -361,62 +346,50 @@ mod test { let mut transaction = database.begin_transaction().await.unwrap(); let account_id = AccountId::new(Uuid::now_v7()); - let account = Account::new( - account_id.clone(), - AccountName::new("name".to_string()), - AccountPrivateKey::new("private_key".to_string()), - AccountPublicKey::new("public_key".to_string()), - AccountIsBot::new(false), - None, - EventVersion::new(Uuid::now_v7()), - Nanoid::default(), - CreatedAt::now(), - ); + let account = make_account(account_id.clone()); + let metadata_id = MetadataId::new(Uuid::now_v7()); + let metadata = make_metadata(metadata_id.clone(), account_id.clone()); database .account_read_model() .create(&mut transaction, &account) .await .unwrap(); - let metadata = Metadata::new( - MetadataId::new(Uuid::now_v7()), - account_id.clone(), - MetadataLabel::new("label".to_string()), - MetadataContent::new("content".to_string()), - EventVersion::new(Uuid::now_v7()), - Nanoid::default(), - ); - database - .metadata_modifier() + .metadata_read_model() .create(&mut transaction, &metadata) .await .unwrap(); let updated_metadata = Metadata::new( - metadata.id().clone(), + metadata_id.clone(), account_id.clone(), - MetadataLabel::new("label2".to_string()), - MetadataContent::new("content2".to_string()), + MetadataLabel::new("updated_label"), + MetadataContent::new("updated_content"), EventVersion::new(Uuid::now_v7()), Nanoid::default(), ); - database - .metadata_modifier() + .metadata_read_model() .update(&mut transaction, &updated_metadata) .await .unwrap(); let found = database - .metadata_query() - .find_by_id(&mut transaction, metadata.id()) + .metadata_read_model() + .find_by_id(&mut transaction, &metadata_id) + .await + .unwrap() + .unwrap(); + assert_eq!(found.id(), updated_metadata.id()); + assert_eq!(found.label(), updated_metadata.label()); + assert_eq!(found.content(), updated_metadata.content()); + + database + .account_read_model() + .delete(&mut transaction, account.id()) .await .unwrap(); - assert_eq!( - found.as_ref().map(Metadata::id), - Some(updated_metadata.id()) - ); } #[test_with::env(DATABASE_URL)] @@ -426,49 +399,39 @@ mod test { let mut transaction = database.begin_transaction().await.unwrap(); let account_id = AccountId::new(Uuid::now_v7()); - let account = Account::new( - account_id.clone(), - AccountName::new("name".to_string()), - AccountPrivateKey::new("private_key".to_string()), - AccountPublicKey::new("public_key".to_string()), - AccountIsBot::new(false), - None, - EventVersion::new(Uuid::now_v7()), - Nanoid::default(), - CreatedAt::now(), - ); + let account = make_account(account_id.clone()); + let metadata_id = MetadataId::new(Uuid::now_v7()); + let metadata = make_metadata(metadata_id.clone(), account_id.clone()); database .account_read_model() .create(&mut transaction, &account) .await .unwrap(); - let metadata = Metadata::new( - MetadataId::new(Uuid::now_v7()), - account_id.clone(), - MetadataLabel::new("label".to_string()), - MetadataContent::new("content".to_string()), - EventVersion::new(Uuid::now_v7()), - Nanoid::default(), - ); - database - .metadata_modifier() + .metadata_read_model() .create(&mut transaction, &metadata) .await .unwrap(); + database - .metadata_modifier() - .delete(&mut transaction, metadata.id()) + .metadata_read_model() + .delete(&mut transaction, &metadata_id) .await .unwrap(); let found = database - .metadata_query() - .find_by_id(&mut transaction, metadata.id()) + .metadata_read_model() + .find_by_id(&mut transaction, &metadata_id) .await .unwrap(); assert!(found.is_none()); + + database + .account_read_model() + .delete(&mut transaction, account.id()) + .await + .unwrap(); } } } diff --git a/driver/src/database/postgres/metadata_event_store.rs b/driver/src/database/postgres/metadata_event_store.rs new file mode 100644 index 0000000..7636154 --- /dev/null +++ b/driver/src/database/postgres/metadata_event_store.rs @@ -0,0 +1,445 @@ +use crate::database::postgres::{CountRow, VersionRow}; +use crate::database::{PostgresConnection, PostgresDatabase}; +use crate::ConvertError; +use error_stack::Report; +use kernel::interfaces::event_store::{DependOnMetadataEventStore, MetadataEventStore}; +use kernel::prelude::entity::{ + CommandEnvelope, EventEnvelope, EventId, EventVersion, KnownEventVersion, Metadata, + MetadataEvent, +}; +use kernel::KernelError; +use serde_json; +use sqlx::PgConnection; +use uuid::Uuid; + +#[derive(sqlx::FromRow)] +struct EventRow { + version: Uuid, + id: Uuid, + #[allow(dead_code)] + event_name: String, + data: serde_json::Value, +} + +impl TryFrom for EventEnvelope { + type Error = Report; + fn try_from(value: EventRow) -> Result { + let event: MetadataEvent = serde_json::from_value(value.data).convert_error()?; + Ok(EventEnvelope::new( + EventId::new(value.id), + event, + EventVersion::new(value.version), + )) + } +} + +pub struct PostgresMetadataEventStore; + +impl MetadataEventStore for PostgresMetadataEventStore { + type Executor = PostgresConnection; + + async fn find_by_id( + &self, + executor: &mut Self::Executor, + id: &EventId, + since: Option<&EventVersion>, + ) -> error_stack::Result>, KernelError> { + let con: &mut PgConnection = executor; + let rows = if let Some(version) = since { + sqlx::query_as::<_, EventRow>( + //language=postgresql + r#" + SELECT version, id, event_name, data + FROM metadata_events + WHERE id = $1 AND version > $2 + ORDER BY version + "#, + ) + .bind(id.as_ref()) + .bind(version.as_ref()) + .fetch_all(con) + .await + .convert_error()? + } else { + sqlx::query_as::<_, EventRow>( + //language=postgresql + r#" + SELECT version, id, event_name, data + FROM metadata_events + WHERE id = $1 + ORDER BY version + "#, + ) + .bind(id.as_ref()) + .fetch_all(con) + .await + .convert_error()? + }; + rows.into_iter() + .map(|row| row.try_into()) + .collect::, KernelError>>() + } + + async fn persist( + &self, + executor: &mut Self::Executor, + command: &CommandEnvelope, + ) -> error_stack::Result<(), KernelError> { + self.persist_internal(executor, command, Uuid::now_v7()) + .await + } + + async fn persist_and_transform( + &self, + executor: &mut Self::Executor, + command: CommandEnvelope, + ) -> error_stack::Result, KernelError> { + let version = Uuid::now_v7(); + self.persist_internal(executor, &command, version).await?; + + let command = command.into_destruct(); + Ok(EventEnvelope::new( + command.id, + command.event, + EventVersion::new(version), + )) + } +} + +impl PostgresMetadataEventStore { + async fn persist_internal( + &self, + executor: &mut PostgresConnection, + command: &CommandEnvelope, + version: Uuid, + ) -> error_stack::Result<(), KernelError> { + let con: &mut PgConnection = executor; + + let event_name = command.event_name(); + let prev_version = command.prev_version().as_ref(); + if let Some(prev_version) = prev_version { + match prev_version { + KnownEventVersion::Nothing => { + let amount = sqlx::query_as::<_, CountRow>( + //language=postgresql + r#" + SELECT COUNT(*) + FROM metadata_events + WHERE id = $1 + "#, + ) + .bind(command.id().as_ref()) + .fetch_one(&mut *con) + .await + .convert_error()?; + if amount.count != 0 { + return Err(Report::new(KernelError::Concurrency).attach_printable( + format!("Event {} already exists", command.id().as_ref()), + )); + } + } + KnownEventVersion::Prev(prev_version) => { + let last_version = sqlx::query_as::<_, VersionRow>( + //language=postgresql + r#" + SELECT version + FROM metadata_events + WHERE id = $1 + ORDER BY version DESC + LIMIT 1 + "#, + ) + .bind(command.id().as_ref()) + .fetch_optional(&mut *con) + .await + .convert_error()?; + if last_version + .map(|row: VersionRow| &row.version != prev_version.as_ref()) + .unwrap_or(true) + { + return Err(Report::new(KernelError::Concurrency).attach_printable( + format!( + "Event {} version {} already exists", + command.id().as_ref(), + prev_version.as_ref() + ), + )); + } + } + }; + } + + sqlx::query( + //language=postgresql + r#" + INSERT INTO metadata_events (version, id, event_name, data) + VALUES ($1, $2, $3, $4) + "#, + ) + .bind(version) + .bind(command.id().as_ref()) + .bind(event_name) + .bind(serde_json::to_value(command.event()).convert_error()?) + .execute(con) + .await + .convert_error()?; + + Ok(()) + } +} + +impl DependOnMetadataEventStore for PostgresDatabase { + type MetadataEventStore = PostgresMetadataEventStore; + + fn metadata_event_store(&self) -> &Self::MetadataEventStore { + &PostgresMetadataEventStore + } +} + +#[cfg(test)] +mod test { + mod query { + use crate::database::PostgresDatabase; + use kernel::interfaces::database::DatabaseConnection; + use kernel::interfaces::event_store::{DependOnMetadataEventStore, MetadataEventStore}; + use kernel::prelude::entity::{ + AccountId, CommandEnvelope, EventId, KnownEventVersion, Metadata, MetadataContent, + MetadataEvent, MetadataId, MetadataLabel, Nanoid, + }; + use uuid::Uuid; + + fn create_metadata_command( + metadata_id: MetadataId, + ) -> CommandEnvelope { + let event = MetadataEvent::Created { + account_id: AccountId::new(Uuid::now_v7()), + label: MetadataLabel::new("label".to_string()), + content: MetadataContent::new("content".to_string()), + nanoid: Nanoid::default(), + }; + CommandEnvelope::new( + EventId::from(metadata_id), + event.name(), + event, + Some(KnownEventVersion::Nothing), + ) + } + + #[test_with::env(DATABASE_URL)] + #[tokio::test] + async fn find_by_id() { + let db = PostgresDatabase::new().await.unwrap(); + let mut transaction = db.begin_transaction().await.unwrap(); + let metadata_id = MetadataId::new(Uuid::now_v7()); + let event_id = EventId::from(metadata_id.clone()); + let events = db + .metadata_event_store() + .find_by_id(&mut transaction, &event_id, None) + .await + .unwrap(); + assert_eq!(events.len(), 0); + let created_metadata = create_metadata_command(metadata_id.clone()); + let update_event = MetadataEvent::Updated { + label: MetadataLabel::new("new_label".to_string()), + content: MetadataContent::new("new_content".to_string()), + }; + let updated_metadata = CommandEnvelope::new( + EventId::from(metadata_id.clone()), + update_event.name(), + update_event, + None, + ); + let delete_event = MetadataEvent::Deleted; + let deleted_metadata = CommandEnvelope::new( + EventId::from(metadata_id.clone()), + delete_event.name(), + delete_event, + None, + ); + + db.metadata_event_store() + .persist(&mut transaction, &created_metadata) + .await + .unwrap(); + db.metadata_event_store() + .persist(&mut transaction, &updated_metadata) + .await + .unwrap(); + db.metadata_event_store() + .persist(&mut transaction, &deleted_metadata) + .await + .unwrap(); + let events = db + .metadata_event_store() + .find_by_id(&mut transaction, &event_id, None) + .await + .unwrap(); + assert_eq!(events.len(), 3); + assert_eq!(&events[0].event, created_metadata.event()); + assert_eq!(&events[1].event, updated_metadata.event()); + assert_eq!(&events[2].event, deleted_metadata.event()); + } + + #[test_with::env(DATABASE_URL)] + #[tokio::test] + async fn find_by_id_since_version() { + let db = PostgresDatabase::new().await.unwrap(); + let mut transaction = db.begin_transaction().await.unwrap(); + let metadata_id = MetadataId::new(Uuid::now_v7()); + let event_id = EventId::from(metadata_id.clone()); + + let created_metadata = create_metadata_command(metadata_id.clone()); + let update_event = MetadataEvent::Updated { + label: MetadataLabel::new("new_label".to_string()), + content: MetadataContent::new("new_content".to_string()), + }; + let updated_metadata = CommandEnvelope::new( + EventId::from(metadata_id.clone()), + update_event.name(), + update_event, + None, + ); + let delete_event = MetadataEvent::Deleted; + let deleted_metadata = CommandEnvelope::new( + EventId::from(metadata_id.clone()), + delete_event.name(), + delete_event, + None, + ); + + db.metadata_event_store() + .persist(&mut transaction, &created_metadata) + .await + .unwrap(); + db.metadata_event_store() + .persist(&mut transaction, &updated_metadata) + .await + .unwrap(); + db.metadata_event_store() + .persist(&mut transaction, &deleted_metadata) + .await + .unwrap(); + + // Get all events to obtain the first version + let all_events = db + .metadata_event_store() + .find_by_id(&mut transaction, &event_id, None) + .await + .unwrap(); + assert_eq!(all_events.len(), 3); + + // Query since the first event's version — should return the 2nd and 3rd events + let since_events = db + .metadata_event_store() + .find_by_id(&mut transaction, &event_id, Some(&all_events[0].version)) + .await + .unwrap(); + assert_eq!(since_events.len(), 2); + assert_eq!(&since_events[0].event, updated_metadata.event()); + assert_eq!(&since_events[1].event, deleted_metadata.event()); + + // Query since the last event's version — should return no events + let no_events = db + .metadata_event_store() + .find_by_id(&mut transaction, &event_id, Some(&all_events[2].version)) + .await + .unwrap(); + assert_eq!(no_events.len(), 0); + } + } + + mod persist { + use crate::database::PostgresDatabase; + use kernel::interfaces::database::DatabaseConnection; + use kernel::interfaces::event_store::{DependOnMetadataEventStore, MetadataEventStore}; + use kernel::prelude::entity::{ + AccountId, CommandEnvelope, EventId, KnownEventVersion, Metadata, MetadataContent, + MetadataEvent, MetadataId, MetadataLabel, Nanoid, + }; + use uuid::Uuid; + + fn create_metadata_command( + metadata_id: MetadataId, + ) -> CommandEnvelope { + let event = MetadataEvent::Created { + account_id: AccountId::new(Uuid::now_v7()), + label: MetadataLabel::new("label".to_string()), + content: MetadataContent::new("content".to_string()), + nanoid: Nanoid::default(), + }; + CommandEnvelope::new( + EventId::from(metadata_id), + event.name(), + event, + Some(KnownEventVersion::Nothing), + ) + } + + #[test_with::env(DATABASE_URL)] + #[tokio::test] + async fn basic_creation() { + let db = PostgresDatabase::new().await.unwrap(); + let mut transaction = db.begin_transaction().await.unwrap(); + let metadata_id = MetadataId::new(Uuid::now_v7()); + let created_metadata = create_metadata_command(metadata_id.clone()); + db.metadata_event_store() + .persist(&mut transaction, &created_metadata) + .await + .unwrap(); + let events = db + .metadata_event_store() + .find_by_id(&mut transaction, &EventId::from(metadata_id), None) + .await + .unwrap(); + assert_eq!(events.len(), 1); + } + + #[test_with::env(DATABASE_URL)] + #[tokio::test] + async fn persist_and_transform_test() { + let db = PostgresDatabase::new().await.unwrap(); + let mut transaction = db.begin_transaction().await.unwrap(); + let metadata_id = MetadataId::new(Uuid::now_v7()); + let created_metadata = create_metadata_command(metadata_id.clone()); + + let event_envelope = db + .metadata_event_store() + .persist_and_transform(&mut transaction, created_metadata.clone()) + .await + .unwrap(); + + assert_eq!(event_envelope.id, EventId::from(metadata_id.clone())); + assert_eq!(&event_envelope.event, created_metadata.event()); + + let events = db + .metadata_event_store() + .find_by_id(&mut transaction, &EventId::from(metadata_id), None) + .await + .unwrap(); + assert_eq!(events.len(), 1); + assert_eq!(&events[0].event, created_metadata.event()); + } + + #[test_with::env(DATABASE_URL)] + #[tokio::test] + async fn known_event_version_nothing_prevents_duplicate() { + let db = PostgresDatabase::new().await.unwrap(); + let mut transaction = db.begin_transaction().await.unwrap(); + let metadata_id = MetadataId::new(Uuid::now_v7()); + let created_metadata = create_metadata_command(metadata_id.clone()); + + // First persist should succeed + db.metadata_event_store() + .persist(&mut transaction, &created_metadata) + .await + .unwrap(); + + // Second persist with KnownEventVersion::Nothing should fail (concurrency error) + let result = db + .metadata_event_store() + .persist(&mut transaction, &created_metadata) + .await; + assert!(result.is_err()); + } + } +} diff --git a/driver/src/database/postgres/profile.rs b/driver/src/database/postgres/profile.rs index 8c2c737..3c21f3c 100644 --- a/driver/src/database/postgres/profile.rs +++ b/driver/src/database/postgres/profile.rs @@ -1,8 +1,7 @@ use sqlx::PgConnection; use uuid::Uuid; -use kernel::interfaces::modify::{DependOnProfileModifier, ProfileModifier}; -use kernel::interfaces::query::{DependOnProfileQuery, ProfileQuery}; +use kernel::interfaces::read_model::{DependOnProfileReadModel, ProfileReadModel}; use kernel::prelude::entity::{ AccountId, EventVersion, ImageId, Nanoid, Profile, ProfileDisplayName, ProfileId, ProfileSummary, @@ -39,9 +38,9 @@ impl From for Profile { } } -pub struct PostgresProfileRepository; +pub struct PostgresProfileReadModel; -impl ProfileQuery for PostgresProfileRepository { +impl ProfileReadModel for PostgresProfileReadModel { type Executor = PostgresConnection; async fn find_by_id( @@ -61,20 +60,28 @@ impl ProfileQuery for PostgresProfileRepository { .fetch_optional(con) .await .convert_error() - .map(|option| option.map(|row| row.into())) + .map(|option| option.map(Profile::from)) } -} - -impl DependOnProfileQuery for PostgresDatabase { - type ProfileQuery = PostgresProfileRepository; - fn profile_query(&self) -> &Self::ProfileQuery { - &PostgresProfileRepository + async fn find_by_account_id( + &self, + executor: &mut Self::Executor, + account_id: &AccountId, + ) -> error_stack::Result, KernelError> { + let con: &mut PgConnection = executor; + sqlx::query_as::<_, ProfileRow>( + //language=postgresql + r#" + SELECT id, account_id, display, summary, icon_id, banner_id, version, nanoid + FROM profiles WHERE account_id = $1 + "#, + ) + .bind(account_id.as_ref()) + .fetch_optional(con) + .await + .convert_error() + .map(|option| option.map(Profile::from)) } -} - -impl ProfileModifier for PostgresProfileRepository { - type Executor = PostgresConnection; async fn create( &self, @@ -118,39 +125,63 @@ impl ProfileModifier for PostgresProfileRepository { //language=postgresql r#" UPDATE profiles SET display = $2, summary = $3, icon_id = $4, banner_id = $5, version = $6 - WHERE account_id = $1 - "# + WHERE id = $1 + "#, + ) + .bind(profile.id().as_ref()) + .bind( + profile + .display_name() + .as_ref() + .map(ProfileDisplayName::as_ref), ) - .bind(profile.id().as_ref()) - .bind(profile.display_name().as_ref().map(ProfileDisplayName::as_ref)) - .bind(profile.summary().as_ref().map(ProfileSummary::as_ref)) - .bind(profile.icon().as_ref().map(ImageId::as_ref)) - .bind(profile.banner().as_ref().map(ImageId::as_ref)) - .bind(profile.version().as_ref()) - .execute(con) - .await - .convert_error()?; + .bind(profile.summary().as_ref().map(ProfileSummary::as_ref)) + .bind(profile.icon().as_ref().map(ImageId::as_ref)) + .bind(profile.banner().as_ref().map(ImageId::as_ref)) + .bind(profile.version().as_ref()) + .execute(con) + .await + .convert_error()?; + Ok(()) + } + + async fn delete( + &self, + executor: &mut Self::Executor, + profile_id: &ProfileId, + ) -> error_stack::Result<(), KernelError> { + let con: &mut PgConnection = executor; + sqlx::query( + //language=postgresql + r#" + DELETE FROM profiles WHERE id = $1 + "#, + ) + .bind(profile_id.as_ref()) + .execute(con) + .await + .convert_error()?; Ok(()) } } -impl DependOnProfileModifier for PostgresDatabase { - type ProfileModifier = PostgresProfileRepository; +impl DependOnProfileReadModel for PostgresDatabase { + type ProfileReadModel = PostgresProfileReadModel; - fn profile_modifier(&self) -> &Self::ProfileModifier { - &PostgresProfileRepository + fn profile_read_model(&self) -> &Self::ProfileReadModel { + &PostgresProfileReadModel } } #[cfg(test)] mod test { - mod query { + mod read_model { use uuid::Uuid; use kernel::interfaces::database::DatabaseConnection; - use kernel::interfaces::modify::{DependOnProfileModifier, ProfileModifier}; - use kernel::interfaces::query::{DependOnProfileQuery, ProfileQuery}; - use kernel::interfaces::read_model::{AccountReadModel, DependOnAccountReadModel}; + use kernel::interfaces::read_model::{ + AccountReadModel, DependOnAccountReadModel, DependOnProfileReadModel, ProfileReadModel, + }; use kernel::prelude::entity::{ Account, AccountId, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, CreatedAt, EventVersion, Nanoid, Profile, ProfileDisplayName, ProfileId, @@ -159,16 +190,9 @@ mod test { use crate::database::PostgresDatabase; - #[test_with::env(DATABASE_URL)] - #[tokio::test] - async fn find_by_id() { - let database = PostgresDatabase::new().await.unwrap(); - let mut transaction = database.begin_transaction().await.unwrap(); - - let profile_id = ProfileId::new(Uuid::now_v7()); - let account_id = AccountId::new(Uuid::now_v7()); - let account = Account::new( - account_id.clone(), + fn make_account(account_id: AccountId) -> Account { + Account::new( + account_id, AccountName::new("test"), AccountPrivateKey::new("test"), AccountPublicKey::new("test"), @@ -177,9 +201,12 @@ mod test { EventVersion::new(Uuid::now_v7()), Nanoid::default(), CreatedAt::now(), - ); - let profile = Profile::new( - profile_id.clone(), + ) + } + + fn make_profile(profile_id: ProfileId, account_id: AccountId) -> Profile { + Profile::new( + profile_id, account_id, Some(ProfileDisplayName::new("display name")), Some(ProfileSummary::new("summary")), @@ -187,46 +214,88 @@ mod test { None, EventVersion::new(Uuid::now_v7()), Nanoid::default(), - ); + ) + } + + #[test_with::env(DATABASE_URL)] + #[tokio::test] + async fn find_by_id() { + let database = PostgresDatabase::new().await.unwrap(); + let mut transaction = database.begin_transaction().await.unwrap(); + + let profile_id = ProfileId::new(Uuid::now_v7()); + let account_id = AccountId::new(Uuid::now_v7()); + let account = make_account(account_id.clone()); + let profile = make_profile(profile_id.clone(), account_id.clone()); + database .account_read_model() .create(&mut transaction, &account) .await .unwrap(); database - .profile_modifier() + .profile_read_model() .create(&mut transaction, &profile) .await .unwrap(); let result = database - .profile_query() + .profile_read_model() .find_by_id(&mut transaction, &profile_id) .await .unwrap(); assert_eq!(result.as_ref().map(Profile::id), Some(profile.id())); + database .account_read_model() .delete(&mut transaction, account.id()) .await .unwrap(); } - } - mod modify { - use uuid::Uuid; + #[test_with::env(DATABASE_URL)] + #[tokio::test] + async fn find_by_account_id() { + let database = PostgresDatabase::new().await.unwrap(); + let mut transaction = database.begin_transaction().await.unwrap(); - use kernel::interfaces::database::DatabaseConnection; - use kernel::interfaces::modify::{DependOnProfileModifier, ProfileModifier}; - use kernel::interfaces::query::{DependOnProfileQuery, ProfileQuery}; - use kernel::interfaces::read_model::{AccountReadModel, DependOnAccountReadModel}; - use kernel::prelude::entity::{ - Account, AccountId, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, - CreatedAt, EventVersion, Nanoid, Profile, ProfileDisplayName, ProfileId, - ProfileSummary, - }; + let profile_id = ProfileId::new(Uuid::now_v7()); + let account_id = AccountId::new(Uuid::now_v7()); + let account = make_account(account_id.clone()); + let profile = make_profile(profile_id.clone(), account_id.clone()); - use crate::database::PostgresDatabase; + database + .account_read_model() + .create(&mut transaction, &account) + .await + .unwrap(); + database + .profile_read_model() + .create(&mut transaction, &profile) + .await + .unwrap(); + + let result = database + .profile_read_model() + .find_by_account_id(&mut transaction, &account_id) + .await + .unwrap(); + assert_eq!(result.as_ref().map(Profile::id), Some(profile.id())); + + // Non-existent account_id returns None + let not_found = database + .profile_read_model() + .find_by_account_id(&mut transaction, &AccountId::new(Uuid::now_v7())) + .await + .unwrap(); + assert!(not_found.is_none()); + + database + .account_read_model() + .delete(&mut transaction, account.id()) + .await + .unwrap(); + } #[test_with::env(DATABASE_URL)] #[tokio::test] @@ -236,37 +305,28 @@ mod test { let profile_id = ProfileId::new(Uuid::now_v7()); let account_id = AccountId::new(Uuid::now_v7()); - let account = Account::new( - account_id.clone(), - AccountName::new("test"), - AccountPrivateKey::new("test"), - AccountPublicKey::new("test"), - AccountIsBot::new(false), - None, - EventVersion::new(Uuid::now_v7()), - Nanoid::default(), - CreatedAt::now(), - ); - let profile = Profile::new( - profile_id, - account_id, - Some(ProfileDisplayName::new("display name")), - Some(ProfileSummary::new("summary")), - None, - None, - EventVersion::new(Uuid::now_v7()), - Nanoid::default(), - ); + let account = make_account(account_id.clone()); + let profile = make_profile(profile_id.clone(), account_id.clone()); + database .account_read_model() .create(&mut transaction, &account) .await .unwrap(); database - .profile_modifier() + .profile_read_model() .create(&mut transaction, &profile) .await .unwrap(); + + let result = database + .profile_read_model() + .find_by_id(&mut transaction, &profile_id) + .await + .unwrap() + .unwrap(); + assert_eq!(result.id(), profile.id()); + database .account_read_model() .delete(&mut transaction, account.id()) @@ -282,41 +342,23 @@ mod test { let profile_id = ProfileId::new(Uuid::now_v7()); let account_id = AccountId::new(Uuid::now_v7()); - let account = Account::new( - account_id.clone(), - AccountName::new("test"), - AccountPrivateKey::new("test"), - AccountPublicKey::new("test"), - AccountIsBot::new(false), - None, - EventVersion::new(Uuid::now_v7()), - Nanoid::default(), - CreatedAt::now(), - ); - let profile = Profile::new( - profile_id.clone(), - account_id.clone(), - Some(ProfileDisplayName::new("display name")), - Some(ProfileSummary::new("summary")), - None, - None, - EventVersion::new(Uuid::now_v7()), - Nanoid::default(), - ); + let account = make_account(account_id.clone()); + let profile = make_profile(profile_id.clone(), account_id.clone()); + database .account_read_model() .create(&mut transaction, &account) .await .unwrap(); database - .profile_modifier() + .profile_read_model() .create(&mut transaction, &profile) .await .unwrap(); let updated_profile = Profile::new( profile_id.clone(), - account_id, + account_id.clone(), Some(ProfileDisplayName::new("updated display name")), Some(ProfileSummary::new("updated summary")), None, @@ -325,17 +367,63 @@ mod test { Nanoid::default(), ); database - .profile_modifier() + .profile_read_model() .update(&mut transaction, &updated_profile) .await .unwrap(); let result = database - .profile_query() + .profile_read_model() .find_by_id(&mut transaction, &profile_id) .await + .unwrap() + .unwrap(); + assert_eq!(result.id(), updated_profile.id()); + assert_eq!(result.display_name(), updated_profile.display_name()); + assert_eq!(result.summary(), updated_profile.summary()); + + database + .account_read_model() + .delete(&mut transaction, account.id()) + .await .unwrap(); - assert_eq!(result.as_ref().map(Profile::id), Some(updated_profile.id())); + } + + #[test_with::env(DATABASE_URL)] + #[tokio::test] + async fn delete() { + let database = PostgresDatabase::new().await.unwrap(); + let mut transaction = database.begin_transaction().await.unwrap(); + + let profile_id = ProfileId::new(Uuid::now_v7()); + let account_id = AccountId::new(Uuid::now_v7()); + let account = make_account(account_id.clone()); + let profile = make_profile(profile_id.clone(), account_id.clone()); + + database + .account_read_model() + .create(&mut transaction, &account) + .await + .unwrap(); + database + .profile_read_model() + .create(&mut transaction, &profile) + .await + .unwrap(); + + database + .profile_read_model() + .delete(&mut transaction, &profile_id) + .await + .unwrap(); + + let result = database + .profile_read_model() + .find_by_id(&mut transaction, &profile_id) + .await + .unwrap(); + assert!(result.is_none()); + database .account_read_model() .delete(&mut transaction, account.id()) diff --git a/driver/src/database/postgres/profile_event_store.rs b/driver/src/database/postgres/profile_event_store.rs new file mode 100644 index 0000000..4a2f2c8 --- /dev/null +++ b/driver/src/database/postgres/profile_event_store.rs @@ -0,0 +1,448 @@ +use crate::database::postgres::{CountRow, VersionRow}; +use crate::database::{PostgresConnection, PostgresDatabase}; +use crate::ConvertError; +use error_stack::Report; +use kernel::interfaces::event_store::{DependOnProfileEventStore, ProfileEventStore}; +use kernel::prelude::entity::{ + CommandEnvelope, EventEnvelope, EventId, EventVersion, KnownEventVersion, Profile, ProfileEvent, +}; +use kernel::KernelError; +use serde_json; +use sqlx::PgConnection; +use uuid::Uuid; + +#[derive(sqlx::FromRow)] +struct EventRow { + version: Uuid, + id: Uuid, + #[allow(dead_code)] + event_name: String, + data: serde_json::Value, +} + +impl TryFrom for EventEnvelope { + type Error = Report; + fn try_from(value: EventRow) -> Result { + let event: ProfileEvent = serde_json::from_value(value.data).convert_error()?; + Ok(EventEnvelope::new( + EventId::new(value.id), + event, + EventVersion::new(value.version), + )) + } +} + +pub struct PostgresProfileEventStore; + +impl ProfileEventStore for PostgresProfileEventStore { + type Executor = PostgresConnection; + + async fn find_by_id( + &self, + executor: &mut Self::Executor, + id: &EventId, + since: Option<&EventVersion>, + ) -> error_stack::Result>, KernelError> { + let con: &mut PgConnection = executor; + let rows = if let Some(version) = since { + sqlx::query_as::<_, EventRow>( + //language=postgresql + r#" + SELECT version, id, event_name, data + FROM profile_events + WHERE id = $1 AND version > $2 + ORDER BY version + "#, + ) + .bind(id.as_ref()) + .bind(version.as_ref()) + .fetch_all(con) + .await + .convert_error()? + } else { + sqlx::query_as::<_, EventRow>( + //language=postgresql + r#" + SELECT version, id, event_name, data + FROM profile_events + WHERE id = $1 + ORDER BY version + "#, + ) + .bind(id.as_ref()) + .fetch_all(con) + .await + .convert_error()? + }; + rows.into_iter() + .map(|row| row.try_into()) + .collect::, KernelError>>() + } + + async fn persist( + &self, + executor: &mut Self::Executor, + command: &CommandEnvelope, + ) -> error_stack::Result<(), KernelError> { + self.persist_internal(executor, command, Uuid::now_v7()) + .await + } + + async fn persist_and_transform( + &self, + executor: &mut Self::Executor, + command: CommandEnvelope, + ) -> error_stack::Result, KernelError> { + let version = Uuid::now_v7(); + self.persist_internal(executor, &command, version).await?; + + let command = command.into_destruct(); + Ok(EventEnvelope::new( + command.id, + command.event, + EventVersion::new(version), + )) + } +} + +impl PostgresProfileEventStore { + async fn persist_internal( + &self, + executor: &mut PostgresConnection, + command: &CommandEnvelope, + version: Uuid, + ) -> error_stack::Result<(), KernelError> { + let con: &mut PgConnection = executor; + + let event_name = command.event_name(); + let prev_version = command.prev_version().as_ref(); + if let Some(prev_version) = prev_version { + match prev_version { + KnownEventVersion::Nothing => { + let amount = sqlx::query_as::<_, CountRow>( + //language=postgresql + r#" + SELECT COUNT(*) + FROM profile_events + WHERE id = $1 + "#, + ) + .bind(command.id().as_ref()) + .fetch_one(&mut *con) + .await + .convert_error()?; + if amount.count != 0 { + return Err(Report::new(KernelError::Concurrency).attach_printable( + format!("Event {} already exists", command.id().as_ref()), + )); + } + } + KnownEventVersion::Prev(prev_version) => { + let last_version = sqlx::query_as::<_, VersionRow>( + //language=postgresql + r#" + SELECT version + FROM profile_events + WHERE id = $1 + ORDER BY version DESC + LIMIT 1 + "#, + ) + .bind(command.id().as_ref()) + .fetch_optional(&mut *con) + .await + .convert_error()?; + if last_version + .map(|row: VersionRow| &row.version != prev_version.as_ref()) + .unwrap_or(true) + { + return Err(Report::new(KernelError::Concurrency).attach_printable( + format!( + "Event {} version {} already exists", + command.id().as_ref(), + prev_version.as_ref() + ), + )); + } + } + }; + } + + sqlx::query( + //language=postgresql + r#" + INSERT INTO profile_events (version, id, event_name, data) + VALUES ($1, $2, $3, $4) + "#, + ) + .bind(version) + .bind(command.id().as_ref()) + .bind(event_name) + .bind(serde_json::to_value(command.event()).convert_error()?) + .execute(con) + .await + .convert_error()?; + + Ok(()) + } +} + +impl DependOnProfileEventStore for PostgresDatabase { + type ProfileEventStore = PostgresProfileEventStore; + + fn profile_event_store(&self) -> &Self::ProfileEventStore { + &PostgresProfileEventStore + } +} + +#[cfg(test)] +mod test { + mod query { + use crate::database::PostgresDatabase; + use kernel::interfaces::database::DatabaseConnection; + use kernel::interfaces::event_store::{DependOnProfileEventStore, ProfileEventStore}; + use kernel::prelude::entity::{ + AccountId, CommandEnvelope, EventId, KnownEventVersion, Nanoid, Profile, ProfileEvent, + ProfileId, + }; + use uuid::Uuid; + + fn create_profile_command(profile_id: ProfileId) -> CommandEnvelope { + let event = ProfileEvent::Created { + account_id: AccountId::new(Uuid::now_v7()), + display_name: None, + summary: None, + icon: None, + banner: None, + nanoid: Nanoid::default(), + }; + CommandEnvelope::new( + EventId::from(profile_id), + event.name(), + event, + Some(KnownEventVersion::Nothing), + ) + } + + #[test_with::env(DATABASE_URL)] + #[tokio::test] + async fn find_by_id() { + let db = PostgresDatabase::new().await.unwrap(); + let mut transaction = db.begin_transaction().await.unwrap(); + let profile_id = ProfileId::new(Uuid::now_v7()); + let event_id = EventId::from(profile_id.clone()); + let events = db + .profile_event_store() + .find_by_id(&mut transaction, &event_id, None) + .await + .unwrap(); + assert_eq!(events.len(), 0); + let created_profile = create_profile_command(profile_id.clone()); + let update_event = ProfileEvent::Updated { + display_name: None, + summary: None, + icon: None, + banner: None, + }; + let updated_profile = CommandEnvelope::new( + EventId::from(profile_id.clone()), + update_event.name(), + update_event, + None, + ); + let delete_event = ProfileEvent::Deleted; + let deleted_profile = CommandEnvelope::new( + EventId::from(profile_id.clone()), + delete_event.name(), + delete_event, + None, + ); + + db.profile_event_store() + .persist(&mut transaction, &created_profile) + .await + .unwrap(); + db.profile_event_store() + .persist(&mut transaction, &updated_profile) + .await + .unwrap(); + db.profile_event_store() + .persist(&mut transaction, &deleted_profile) + .await + .unwrap(); + let events = db + .profile_event_store() + .find_by_id(&mut transaction, &event_id, None) + .await + .unwrap(); + assert_eq!(events.len(), 3); + assert_eq!(&events[0].event, created_profile.event()); + assert_eq!(&events[1].event, updated_profile.event()); + assert_eq!(&events[2].event, deleted_profile.event()); + } + + #[test_with::env(DATABASE_URL)] + #[tokio::test] + async fn find_by_id_since_version() { + let db = PostgresDatabase::new().await.unwrap(); + let mut transaction = db.begin_transaction().await.unwrap(); + let profile_id = ProfileId::new(Uuid::now_v7()); + let event_id = EventId::from(profile_id.clone()); + + let created_profile = create_profile_command(profile_id.clone()); + let update_event = ProfileEvent::Updated { + display_name: None, + summary: None, + icon: None, + banner: None, + }; + let updated_profile = CommandEnvelope::new( + EventId::from(profile_id.clone()), + update_event.name(), + update_event, + None, + ); + let delete_event = ProfileEvent::Deleted; + let deleted_profile = CommandEnvelope::new( + EventId::from(profile_id.clone()), + delete_event.name(), + delete_event, + None, + ); + + db.profile_event_store() + .persist(&mut transaction, &created_profile) + .await + .unwrap(); + db.profile_event_store() + .persist(&mut transaction, &updated_profile) + .await + .unwrap(); + db.profile_event_store() + .persist(&mut transaction, &deleted_profile) + .await + .unwrap(); + + // Get all events to obtain the first version + let all_events = db + .profile_event_store() + .find_by_id(&mut transaction, &event_id, None) + .await + .unwrap(); + assert_eq!(all_events.len(), 3); + + // Query since the first event's version — should return the 2nd and 3rd events + let since_events = db + .profile_event_store() + .find_by_id(&mut transaction, &event_id, Some(&all_events[0].version)) + .await + .unwrap(); + assert_eq!(since_events.len(), 2); + assert_eq!(&since_events[0].event, updated_profile.event()); + assert_eq!(&since_events[1].event, deleted_profile.event()); + + // Query since the last event's version — should return no events + let no_events = db + .profile_event_store() + .find_by_id(&mut transaction, &event_id, Some(&all_events[2].version)) + .await + .unwrap(); + assert_eq!(no_events.len(), 0); + } + } + + mod persist { + use crate::database::PostgresDatabase; + use kernel::interfaces::database::DatabaseConnection; + use kernel::interfaces::event_store::{DependOnProfileEventStore, ProfileEventStore}; + use kernel::prelude::entity::{ + AccountId, CommandEnvelope, EventId, KnownEventVersion, Nanoid, Profile, ProfileEvent, + ProfileId, + }; + use uuid::Uuid; + + fn create_profile_command(profile_id: ProfileId) -> CommandEnvelope { + let event = ProfileEvent::Created { + account_id: AccountId::new(Uuid::now_v7()), + display_name: None, + summary: None, + icon: None, + banner: None, + nanoid: Nanoid::default(), + }; + CommandEnvelope::new( + EventId::from(profile_id), + event.name(), + event, + Some(KnownEventVersion::Nothing), + ) + } + + #[test_with::env(DATABASE_URL)] + #[tokio::test] + async fn basic_creation() { + let db = PostgresDatabase::new().await.unwrap(); + let mut transaction = db.begin_transaction().await.unwrap(); + let profile_id = ProfileId::new(Uuid::now_v7()); + let created_profile = create_profile_command(profile_id.clone()); + db.profile_event_store() + .persist(&mut transaction, &created_profile) + .await + .unwrap(); + let events = db + .profile_event_store() + .find_by_id(&mut transaction, &EventId::from(profile_id), None) + .await + .unwrap(); + assert_eq!(events.len(), 1); + } + + #[test_with::env(DATABASE_URL)] + #[tokio::test] + async fn persist_and_transform_test() { + let db = PostgresDatabase::new().await.unwrap(); + let mut transaction = db.begin_transaction().await.unwrap(); + let profile_id = ProfileId::new(Uuid::now_v7()); + let created_profile = create_profile_command(profile_id.clone()); + + let event_envelope = db + .profile_event_store() + .persist_and_transform(&mut transaction, created_profile.clone()) + .await + .unwrap(); + + assert_eq!(event_envelope.id, EventId::from(profile_id.clone())); + assert_eq!(&event_envelope.event, created_profile.event()); + + let events = db + .profile_event_store() + .find_by_id(&mut transaction, &EventId::from(profile_id), None) + .await + .unwrap(); + assert_eq!(events.len(), 1); + assert_eq!(&events[0].event, created_profile.event()); + } + + #[test_with::env(DATABASE_URL)] + #[tokio::test] + async fn known_event_version_nothing_prevents_duplicate() { + let db = PostgresDatabase::new().await.unwrap(); + let mut transaction = db.begin_transaction().await.unwrap(); + let profile_id = ProfileId::new(Uuid::now_v7()); + let created_profile = create_profile_command(profile_id.clone()); + + // First persist should succeed + db.profile_event_store() + .persist(&mut transaction, &created_profile) + .await + .unwrap(); + + // Second persist with KnownEventVersion::Nothing should fail (concurrency error) + let result = db + .profile_event_store() + .persist(&mut transaction, &created_profile) + .await; + assert!(result.is_err()); + } + } +} diff --git a/driver/src/database/postgres/remote_account.rs b/driver/src/database/postgres/remote_account.rs index 819df1c..d32c757 100644 --- a/driver/src/database/postgres/remote_account.rs +++ b/driver/src/database/postgres/remote_account.rs @@ -1,7 +1,6 @@ use crate::database::{PostgresConnection, PostgresDatabase}; use crate::ConvertError; -use kernel::interfaces::modify::{DependOnRemoteAccountModifier, RemoteAccountModifier}; -use kernel::interfaces::query::{DependOnRemoteAccountQuery, RemoteAccountQuery}; +use kernel::interfaces::repository::{DependOnRemoteAccountRepository, RemoteAccountRepository}; use kernel::prelude::entity::{ ImageId, RemoteAccount, RemoteAccountAcct, RemoteAccountId, RemoteAccountUrl, }; @@ -30,7 +29,7 @@ impl From for RemoteAccount { pub struct PostgresRemoteAccountRepository; -impl RemoteAccountQuery for PostgresRemoteAccountRepository { +impl RemoteAccountRepository for PostgresRemoteAccountRepository { type Executor = PostgresConnection; async fn find_by_id( @@ -95,18 +94,6 @@ impl RemoteAccountQuery for PostgresRemoteAccountRepository { .convert_error() .map(|option| option.map(RemoteAccount::from)) } -} - -impl DependOnRemoteAccountQuery for PostgresDatabase { - type RemoteAccountQuery = PostgresRemoteAccountRepository; - - fn remote_account_query(&self) -> &Self::RemoteAccountQuery { - &PostgresRemoteAccountRepository - } -} - -impl RemoteAccountModifier for PostgresRemoteAccountRepository { - type Executor = PostgresConnection; async fn create( &self, @@ -176,10 +163,10 @@ impl RemoteAccountModifier for PostgresRemoteAccountRepository { } } -impl DependOnRemoteAccountModifier for PostgresDatabase { - type RemoteAccountModifier = PostgresRemoteAccountRepository; +impl DependOnRemoteAccountRepository for PostgresDatabase { + type RemoteAccountRepository = PostgresRemoteAccountRepository; - fn remote_account_modifier(&self) -> &Self::RemoteAccountModifier { + fn remote_account_repository(&self) -> &Self::RemoteAccountRepository { &PostgresRemoteAccountRepository } } @@ -209,8 +196,9 @@ mod test { use crate::database::postgres::remote_account::test::acct_url; use crate::database::PostgresDatabase; use kernel::interfaces::database::DatabaseConnection; - use kernel::interfaces::modify::{DependOnRemoteAccountModifier, RemoteAccountModifier}; - use kernel::interfaces::query::{DependOnRemoteAccountQuery, RemoteAccountQuery}; + use kernel::interfaces::repository::{ + DependOnRemoteAccountRepository, RemoteAccountRepository, + }; use kernel::prelude::entity::{RemoteAccount, RemoteAccountId}; use uuid::Uuid; @@ -224,18 +212,18 @@ mod test { let (acct, url) = acct_url(None); let remote_account = RemoteAccount::new(id.clone(), acct, url, None); database - .remote_account_modifier() + .remote_account_repository() .create(&mut transaction, &remote_account) .await .unwrap(); let result = database - .remote_account_query() + .remote_account_repository() .find_by_id(&mut transaction, &id) .await .unwrap(); assert_eq!(result, Some(remote_account)); database - .remote_account_modifier() + .remote_account_repository() .delete(&mut transaction, &id) .await .unwrap(); @@ -255,19 +243,19 @@ mod test { None, ); database - .remote_account_modifier() + .remote_account_repository() .create(&mut transaction, &remote_account) .await .unwrap(); let result = database - .remote_account_query() + .remote_account_repository() .find_by_acct(&mut transaction, &acct) .await .unwrap(); assert_eq!(result, Some(remote_account.clone())); database - .remote_account_modifier() + .remote_account_repository() .delete(&mut transaction, remote_account.id()) .await .unwrap(); @@ -287,18 +275,18 @@ mod test { None, ); database - .remote_account_modifier() + .remote_account_repository() .create(&mut transaction, &remote_account) .await .unwrap(); let result = database - .remote_account_query() + .remote_account_repository() .find_by_url(&mut transaction, &url) .await .unwrap(); assert_eq!(result, Some(remote_account.clone())); database - .remote_account_modifier() + .remote_account_repository() .delete(&mut transaction, remote_account.id()) .await .unwrap(); @@ -309,8 +297,9 @@ mod test { use crate::database::postgres::remote_account::test::acct_url; use crate::database::PostgresDatabase; use kernel::interfaces::database::DatabaseConnection; - use kernel::interfaces::modify::{DependOnRemoteAccountModifier, RemoteAccountModifier}; - use kernel::interfaces::query::{DependOnRemoteAccountQuery, RemoteAccountQuery}; + use kernel::interfaces::repository::{ + DependOnRemoteAccountRepository, RemoteAccountRepository, + }; use kernel::prelude::entity::{RemoteAccount, RemoteAccountId}; use uuid::Uuid; @@ -324,12 +313,12 @@ mod test { let (acct, url) = acct_url(None); let remote_account = RemoteAccount::new(id, acct, url, None); database - .remote_account_modifier() + .remote_account_repository() .create(&mut transaction, &remote_account) .await .unwrap(); database - .remote_account_modifier() + .remote_account_repository() .delete(&mut transaction, remote_account.id()) .await .unwrap(); @@ -345,7 +334,7 @@ mod test { let (acct, url) = acct_url(None); let remote_account = RemoteAccount::new(id.clone(), acct, url, None); database - .remote_account_modifier() + .remote_account_repository() .create(&mut transaction, &remote_account) .await .unwrap(); @@ -353,18 +342,18 @@ mod test { let (acct, url) = acct_url(None); let remote_account = RemoteAccount::new(id.clone(), acct, url, None); database - .remote_account_modifier() + .remote_account_repository() .update(&mut transaction, &remote_account) .await .unwrap(); let result = database - .remote_account_query() + .remote_account_repository() .find_by_id(&mut transaction, &id) .await .unwrap(); assert_eq!(result, Some(remote_account.clone())); database - .remote_account_modifier() + .remote_account_repository() .delete(&mut transaction, remote_account.id()) .await .unwrap(); @@ -380,18 +369,18 @@ mod test { let (acct, url) = acct_url(None); let remote_account = RemoteAccount::new(id.clone(), acct, url, None); database - .remote_account_modifier() + .remote_account_repository() .create(&mut transaction, &remote_account) .await .unwrap(); database - .remote_account_modifier() + .remote_account_repository() .delete(&mut transaction, &id) .await .unwrap(); let result = database - .remote_account_query() + .remote_account_repository() .find_by_id(&mut transaction, &id) .await .unwrap(); diff --git a/kernel/src/entity/follow.rs b/kernel/src/entity/follow.rs index 5afd479..d979290 100644 --- a/kernel/src/entity/follow.rs +++ b/kernel/src/entity/follow.rs @@ -4,13 +4,10 @@ mod target_id; pub use self::{approved_at::*, id::*, target_id::*}; -use crate::entity::{CommandEnvelope, EventEnvelope, EventId}; -use crate::event::EventApplier; use crate::KernelError; -use error_stack::{Report, ResultExt}; +use error_stack::ResultExt; use serde::{Deserialize, Serialize}; -use time::OffsetDateTime; -use vodca::{Nameln, References}; +use vodca::References; #[derive(Debug, Clone, Hash, Eq, PartialEq, References, Serialize, Deserialize)] pub struct Follow { @@ -43,175 +40,3 @@ impl Follow { } } } - -#[derive(Debug, Clone, Eq, PartialEq, Nameln, Serialize, Deserialize)] -#[serde(tag = "type", rename_all = "snake_case")] -#[vodca(prefix = "follow", snake_case)] -pub enum FollowEvent { - Created { - source: FollowTargetId, - destination: FollowTargetId, - }, - Approved, - Deleted, -} - -impl Follow { - pub fn create( - id: FollowId, - source: FollowTargetId, - destination: FollowTargetId, - ) -> error_stack::Result, KernelError> { - match (source, destination) { - (source @ FollowTargetId::Remote(_), destination @ FollowTargetId::Remote(_)) => { - Err(KernelError::Internal).attach_printable(format!( - "Cannot create remote to remote follow data. source: {:?}, destination: {:?}", - source, destination - )) - } - (source, destination) => { - let event = FollowEvent::Created { - source, - destination, - }; - Ok(CommandEnvelope::new( - EventId::from(id), - event.name(), - event, - None, - )) - } - } - } - - pub fn approve(id: FollowId) -> CommandEnvelope { - let event = FollowEvent::Approved; - CommandEnvelope::new(EventId::from(id), event.name(), event, None) - } - - pub fn delete(id: FollowId) -> CommandEnvelope { - let event = FollowEvent::Deleted; - CommandEnvelope::new(EventId::from(id), event.name(), event, None) - } -} - -impl EventApplier for Follow { - type Event = FollowEvent; - const ENTITY_NAME: &'static str = "Follow"; - - fn apply( - entity: &mut Option, - event: EventEnvelope, - ) -> error_stack::Result<(), KernelError> - where - Self: Sized, - { - match event.event { - FollowEvent::Created { - source, - destination, - } => { - if let Some(entity) = entity { - return Err(KernelError::Internal) - .attach_printable(Self::already_exists(entity)); - } - *entity = Some(Follow::new( - FollowId::new(event.id), - source, - destination, - None, - )?); - } - FollowEvent::Approved => { - if let Some(entity) = entity { - let timestamp = event.id.as_ref().get_timestamp().ok_or_else(|| { - Report::new(KernelError::Internal) - .attach_printable("Failed to get timestamp from uuid") - })?; - let (seconds, nanos) = timestamp.to_unix(); - let datetime = OffsetDateTime::from_unix_timestamp_nanos( - i128::from(nanos) + i128::from(seconds * 1_000_000_000), - ) - .change_context_lazy(|| KernelError::Internal)?; - entity.approved_at = Some(FollowApprovedAt::new(datetime)); - } else { - return Err(KernelError::Internal) - .attach_printable(Self::not_exists(event.id.as_ref())); - } - } - FollowEvent::Deleted => { - *entity = None; - } - } - Ok(()) - } -} - -#[cfg(test)] -mod test { - use crate::entity::{ - AccountId, EventEnvelope, EventVersion, Follow, FollowId, FollowTargetId, RemoteAccountId, - }; - use crate::event::EventApplier; - use uuid::Uuid; - - #[test] - fn create_event() { - let id = FollowId::new(Uuid::now_v7()); - let source = FollowTargetId::from(AccountId::new(Uuid::now_v7())); - let destination = FollowTargetId::from(RemoteAccountId::new(Uuid::now_v7())); - let event = Follow::create(id.clone(), source.clone(), destination.clone()).unwrap(); - let envelope = EventEnvelope::new( - event.id().clone(), - event.event().clone(), - EventVersion::new(Uuid::now_v7()), - ); - let mut entity = None; - Follow::apply(&mut entity, envelope).unwrap(); - assert!(entity.is_some()); - let entity = entity.unwrap(); - assert_eq!(entity.id(), &id); - assert_eq!(entity.source(), &source); - assert_eq!(entity.destination(), &destination); - assert!(entity.approved_at().is_none()); - } - - #[test] - fn update_event() { - let id = FollowId::new(Uuid::now_v7()); - let source = FollowTargetId::from(AccountId::new(Uuid::now_v7())); - let destination = FollowTargetId::from(RemoteAccountId::new(Uuid::now_v7())); - let follow = Follow::new(id.clone(), source.clone(), destination.clone(), None).unwrap(); - let event = Follow::approve(id.clone()); - let envelope = EventEnvelope::new( - event.id().clone(), - event.event().clone(), - EventVersion::new(Uuid::now_v7()), - ); - let mut entity = Some(follow); - Follow::apply(&mut entity, envelope).unwrap(); - assert!(entity.is_some()); - let entity = entity.unwrap(); - assert_eq!(entity.id(), &id); - assert_eq!(entity.source(), &source); - assert_eq!(entity.destination(), &destination); - assert!(entity.approved_at().is_some()); - } - - #[test] - fn delete_event() { - let id = FollowId::new(Uuid::now_v7()); - let source = FollowTargetId::from(AccountId::new(Uuid::now_v7())); - let destination = FollowTargetId::from(RemoteAccountId::new(Uuid::now_v7())); - let follow = Follow::new(id.clone(), source.clone(), destination.clone(), None).unwrap(); - let event = Follow::delete(id.clone()); - let envelope = EventEnvelope::new( - event.id().clone(), - event.event().clone(), - EventVersion::new(Uuid::now_v7()), - ); - let mut entity = Some(follow); - Follow::apply(&mut entity, envelope).unwrap(); - assert!(entity.is_none()); - } -} diff --git a/kernel/src/entity/follow/id.rs b/kernel/src/entity/follow/id.rs index aea4bdf..f2d256a 100644 --- a/kernel/src/entity/follow/id.rs +++ b/kernel/src/entity/follow/id.rs @@ -1,13 +1,6 @@ -use crate::entity::{EventId, Follow, FollowEvent}; use serde::{Deserialize, Serialize}; use uuid::Uuid; use vodca::{AsRefln, Fromln, Newln}; #[derive(Debug, Clone, PartialEq, Eq, Hash, Fromln, AsRefln, Newln, Serialize, Deserialize)] pub struct FollowId(Uuid); - -impl From for EventId { - fn from(value: FollowId) -> Self { - EventId::new(value.0) - } -} diff --git a/kernel/src/entity/metadata.rs b/kernel/src/entity/metadata.rs index a41b68c..a0fc92d 100644 --- a/kernel/src/entity/metadata.rs +++ b/kernel/src/entity/metadata.rs @@ -25,7 +25,7 @@ pub struct Metadata { nanoid: Nanoid, } -#[derive(Debug, Clone, Nameln, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Nameln, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] #[vodca(prefix = "metadata", snake_case)] pub enum MetadataEvent { @@ -68,14 +68,28 @@ impl Metadata { id: MetadataId, label: MetadataLabel, content: MetadataContent, + current_version: EventVersion, ) -> CommandEnvelope { let event = MetadataEvent::Updated { label, content }; - CommandEnvelope::new(EventId::from(id), event.name(), event, None) + CommandEnvelope::new( + EventId::from(id), + event.name(), + event, + Some(KnownEventVersion::Prev(current_version)), + ) } - pub fn delete(id: MetadataId) -> CommandEnvelope { + pub fn delete( + id: MetadataId, + current_version: EventVersion, + ) -> CommandEnvelope { let event = MetadataEvent::Deleted; - CommandEnvelope::new(EventId::from(id), event.name(), event, None) + CommandEnvelope::new( + EventId::from(id), + event.name(), + event, + Some(KnownEventVersion::Prev(current_version)), + ) } } @@ -188,7 +202,9 @@ mod test { ); let label = MetadataLabel::new("new_label".to_string()); let content = MetadataContent::new("new_content".to_string()); - let update_event = Metadata::update(id.clone(), label.clone(), content.clone()); + let current_version = metadata.version().clone(); + let update_event = + Metadata::update(id.clone(), label.clone(), content.clone(), current_version); let version = EventVersion::new(Uuid::now_v7()); let envelope = EventEnvelope::new( update_event.id().clone(), @@ -222,7 +238,8 @@ mod test { EventVersion::new(Uuid::now_v7()), nano_id.clone(), ); - let delete_event = Metadata::delete(id.clone()); + let current_version = metadata.version().clone(); + let delete_event = Metadata::delete(id.clone(), current_version); let envelope = EventEnvelope::new( delete_event.id().clone(), delete_event.event().clone(), diff --git a/kernel/src/entity/profile.rs b/kernel/src/entity/profile.rs index db8ed2a..8c65845 100644 --- a/kernel/src/entity/profile.rs +++ b/kernel/src/entity/profile.rs @@ -31,7 +31,7 @@ pub struct Profile { nanoid: Nanoid, } -#[derive(Debug, Clone, Nameln, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Nameln, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] #[vodca(prefix = "profile", snake_case)] pub enum ProfileEvent { @@ -49,6 +49,7 @@ pub enum ProfileEvent { icon: Option, banner: Option, }, + Deleted, } impl Profile { @@ -83,6 +84,7 @@ impl Profile { summary: Option, icon: Option, banner: Option, + current_version: EventVersion, ) -> CommandEnvelope { let event = ProfileEvent::Updated { display_name, @@ -90,7 +92,25 @@ impl Profile { icon, banner, }; - CommandEnvelope::new(EventId::from(id), event.name(), event, None) + CommandEnvelope::new( + EventId::from(id), + event.name(), + event, + Some(KnownEventVersion::Prev(current_version)), + ) + } + + pub fn delete( + id: ProfileId, + current_version: EventVersion, + ) -> CommandEnvelope { + let event = ProfileEvent::Deleted; + CommandEnvelope::new( + EventId::from(id), + event.name(), + event, + Some(KnownEventVersion::Prev(current_version)), + ) } } @@ -151,6 +171,14 @@ impl EventApplier for Profile { .attach_printable(Self::not_exists(event.id.as_ref()))); } } + ProfileEvent::Deleted => { + if entity.is_some() { + *entity = None; + } else { + return Err(Report::new(KernelError::Internal) + .attach_printable(Self::not_exists(event.id.as_ref()))); + } + } } Ok(()) } @@ -163,6 +191,7 @@ mod test { ProfileId, ProfileSummary, }; use crate::event::EventApplier; + use crate::KernelError; use uuid::Uuid; #[test] @@ -216,12 +245,14 @@ mod test { let summary = ProfileSummary::new("summary".to_string()); let icon = ImageId::new(Uuid::now_v7()); let banner = ImageId::new(Uuid::now_v7()); + let current_version = profile.version().clone(); let update_event = Profile::update( id.clone(), Some(display_name.clone()), Some(summary.clone()), Some(icon.clone()), Some(banner.clone()), + current_version, ); let version = EventVersion::new(Uuid::now_v7()); let envelope = EventEnvelope::new( @@ -242,4 +273,46 @@ mod test { assert_eq!(profile.version(), &version); assert_eq!(profile.nanoid(), &nano_id); } + + #[test] + fn delete_profile() { + let account_id = AccountId::new(Uuid::now_v7()); + let id = ProfileId::new(Uuid::now_v7()); + let nano_id = Nanoid::default(); + let profile = Profile::new( + id.clone(), + account_id.clone(), + None, + None, + None, + None, + EventVersion::new(Uuid::now_v7()), + nano_id.clone(), + ); + let version = profile.version().clone(); + let event = Profile::delete(id.clone(), version); + let envelope = EventEnvelope::new( + event.id().clone(), + event.event().clone(), + EventVersion::new(Uuid::now_v7()), + ); + let mut profile = Some(profile); + Profile::apply(&mut profile, envelope).unwrap(); + assert!(profile.is_none()); + } + + #[test] + fn delete_not_exist_profile() { + let id = ProfileId::new(Uuid::now_v7()); + let version = EventVersion::new(Uuid::now_v7()); + let event = Profile::delete(id.clone(), version); + let envelope = EventEnvelope::new( + event.id().clone(), + event.event().clone(), + EventVersion::new(Uuid::now_v7()), + ); + let mut profile = None; + assert!(Profile::apply(&mut profile, envelope) + .is_err_and(|e| e.current_context() == &KernelError::Internal)); + } } diff --git a/kernel/src/entity/remote_account.rs b/kernel/src/entity/remote_account.rs index f2a986a..f4f1a53 100644 --- a/kernel/src/entity/remote_account.rs +++ b/kernel/src/entity/remote_account.rs @@ -6,12 +6,8 @@ pub use self::acct::*; pub use self::id::*; pub use self::url::*; use crate::entity::image::ImageId; -use crate::entity::{CommandEnvelope, EventEnvelope, EventId, KnownEventVersion}; -use crate::event::EventApplier; -use crate::KernelError; -use error_stack::Report; use serde::{Deserialize, Serialize}; -use vodca::{Nameln, Newln, References}; +use vodca::{Newln, References}; #[derive(Debug, Clone, Eq, PartialEq, References, Newln, Serialize, Deserialize)] pub struct RemoteAccount { @@ -20,159 +16,3 @@ pub struct RemoteAccount { url: RemoteAccountUrl, icon_id: Option, } - -#[derive(Debug, Clone, Eq, PartialEq, Nameln, Serialize, Deserialize)] -#[serde(tag = "type", rename_all_fields = "snake_case")] -#[vodca(prefix = "remote_account", snake_case)] -pub enum RemoteAccountEvent { - Created { - acct: RemoteAccountAcct, - url: RemoteAccountUrl, - icon_id: Option, - }, - Updated { - icon_id: Option, - }, - Deleted, -} - -impl RemoteAccount { - pub fn create( - id: RemoteAccountId, - acct: RemoteAccountAcct, - url: RemoteAccountUrl, - icon_id: Option, - ) -> CommandEnvelope { - let event = RemoteAccountEvent::Created { acct, url, icon_id }; - CommandEnvelope::new( - EventId::from(id), - event.name(), - event, - Some(KnownEventVersion::Nothing), - ) - } - - pub fn update( - id: RemoteAccountId, - icon_id: Option, - ) -> CommandEnvelope { - let event = RemoteAccountEvent::Updated { icon_id }; - CommandEnvelope::new(EventId::from(id), event.name(), event, None) - } - - pub fn delete(id: RemoteAccountId) -> CommandEnvelope { - let event = RemoteAccountEvent::Deleted; - CommandEnvelope::new(EventId::from(id), event.name(), event, None) - } -} - -impl EventApplier for RemoteAccount { - type Event = RemoteAccountEvent; - const ENTITY_NAME: &'static str = "RemoteAccount"; - - fn apply( - entity: &mut Option, - event: EventEnvelope, - ) -> error_stack::Result<(), KernelError> - where - Self: Sized, - { - match event.event { - RemoteAccountEvent::Created { acct, url, icon_id } => { - if let Some(entity) = entity { - return Err(Report::new(KernelError::Internal) - .attach_printable(Self::already_exists(entity))); - } - *entity = Some(RemoteAccount { - id: RemoteAccountId::new(event.id), - acct, - url, - icon_id, - }); - } - RemoteAccountEvent::Updated { icon_id } => { - if let Some(entity) = entity { - entity.icon_id = icon_id; - } else { - return Err(Report::new(KernelError::Internal) - .attach_printable(Self::not_exists(event.id.as_ref()))); - } - } - RemoteAccountEvent::Deleted => { - *entity = None; - } - } - Ok(()) - } -} - -#[cfg(test)] -mod test { - use crate::entity::{ - EventEnvelope, EventVersion, ImageId, RemoteAccount, RemoteAccountAcct, RemoteAccountId, - RemoteAccountUrl, - }; - use crate::event::EventApplier; - use uuid::Uuid; - - #[test] - fn create_remote_account() { - let id = RemoteAccountId::new(Uuid::now_v7()); - let acct = RemoteAccountAcct::new("acct:".to_string()); - let url = RemoteAccountUrl::new("https://example.com".to_string()); - let create = RemoteAccount::create(id.clone(), acct.clone(), url.clone(), None); - let envelope = EventEnvelope::new( - create.id().clone(), - create.event().clone(), - EventVersion::new(Uuid::now_v7()), - ); - let mut entity = None; - RemoteAccount::apply(&mut entity, envelope).unwrap(); - assert!(entity.is_some()); - let entity = entity.unwrap(); - assert_eq!(entity.id(), &id); - assert_eq!(entity.acct(), &acct); - assert_eq!(entity.url(), &url); - assert!(entity.icon_id().is_none()); - } - - #[test] - fn update_remote_account() { - let id = RemoteAccountId::new(Uuid::now_v7()); - let acct = RemoteAccountAcct::new("acct:".to_string()); - let url = RemoteAccountUrl::new("https://example.com".to_string()); - let remote_account = RemoteAccount::new(id.clone(), acct.clone(), url.clone(), None); - let new_icon_id = Some(ImageId::new(Uuid::now_v7())); - let update = RemoteAccount::update(id.clone(), new_icon_id); - let envelope = EventEnvelope::new( - update.id().clone(), - update.event().clone(), - EventVersion::new(Uuid::now_v7()), - ); - let mut entity = Some(remote_account); - RemoteAccount::apply(&mut entity, envelope).unwrap(); - assert!(entity.is_some()); - let entity = entity.unwrap(); - assert_eq!(entity.id(), &id); - assert_eq!(entity.acct(), &acct); - assert_eq!(entity.url(), &url); - assert!(entity.icon_id().is_some()); - } - - #[test] - fn delete_remote_account() { - let id = RemoteAccountId::new(Uuid::now_v7()); - let acct = RemoteAccountAcct::new("acct:".to_string()); - let url = RemoteAccountUrl::new("https://example.com".to_string()); - let remote_account = RemoteAccount::new(id.clone(), acct.clone(), url.clone(), None); - let delete = RemoteAccount::delete(id.clone()); - let envelope = EventEnvelope::new( - delete.id().clone(), - delete.event().clone(), - EventVersion::new(Uuid::now_v7()), - ); - let mut entity = Some(remote_account); - RemoteAccount::apply(&mut entity, envelope).unwrap(); - assert!(entity.is_none()); - } -} diff --git a/kernel/src/entity/remote_account/id.rs b/kernel/src/entity/remote_account/id.rs index 3e2232c..329f3d9 100644 --- a/kernel/src/entity/remote_account/id.rs +++ b/kernel/src/entity/remote_account/id.rs @@ -1,13 +1,6 @@ -use crate::entity::{EventId, RemoteAccount, RemoteAccountEvent}; use serde::{Deserialize, Serialize}; use uuid::Uuid; use vodca::{AsRefln, Fromln, Newln}; #[derive(Debug, Clone, PartialEq, Eq, Hash, Fromln, AsRefln, Newln, Serialize, Deserialize)] pub struct RemoteAccountId(Uuid); - -impl From for EventId { - fn from(id: RemoteAccountId) -> Self { - EventId::new(id.0) - } -} diff --git a/kernel/src/event_store.rs b/kernel/src/event_store.rs index bd9ae1a..fa7d58c 100644 --- a/kernel/src/event_store.rs +++ b/kernel/src/event_store.rs @@ -1,5 +1,9 @@ mod account; mod auth_account; +mod metadata; +mod profile; pub use self::account::*; pub use self::auth_account::*; +pub use self::metadata::*; +pub use self::profile::*; diff --git a/kernel/src/event_store/metadata.rs b/kernel/src/event_store/metadata.rs new file mode 100644 index 0000000..95cd152 --- /dev/null +++ b/kernel/src/event_store/metadata.rs @@ -0,0 +1,40 @@ +use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Executor}; +use crate::entity::{ + CommandEnvelope, EventEnvelope, EventId, EventVersion, Metadata, MetadataEvent, +}; +use crate::KernelError; +use std::future::Future; + +pub trait MetadataEventStore: Sync + Send + 'static { + type Executor: Executor; + + fn persist( + &self, + executor: &mut Self::Executor, + command: &CommandEnvelope, + ) -> impl Future> + Send; + + fn persist_and_transform( + &self, + executor: &mut Self::Executor, + command: CommandEnvelope, + ) -> impl Future, KernelError>> + + Send; + + fn find_by_id( + &self, + executor: &mut Self::Executor, + id: &EventId, + since: Option<&EventVersion>, + ) -> impl Future< + Output = error_stack::Result>, KernelError>, + > + Send; +} + +pub trait DependOnMetadataEventStore: Sync + Send + DependOnDatabaseConnection { + type MetadataEventStore: MetadataEventStore< + Executor = ::Executor, + >; + + fn metadata_event_store(&self) -> &Self::MetadataEventStore; +} diff --git a/kernel/src/event_store/profile.rs b/kernel/src/event_store/profile.rs new file mode 100644 index 0000000..5e32323 --- /dev/null +++ b/kernel/src/event_store/profile.rs @@ -0,0 +1,38 @@ +use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Executor}; +use crate::entity::{CommandEnvelope, EventEnvelope, EventId, EventVersion, Profile, ProfileEvent}; +use crate::KernelError; +use std::future::Future; + +pub trait ProfileEventStore: Sync + Send + 'static { + type Executor: Executor; + + fn persist( + &self, + executor: &mut Self::Executor, + command: &CommandEnvelope, + ) -> impl Future> + Send; + + fn persist_and_transform( + &self, + executor: &mut Self::Executor, + command: CommandEnvelope, + ) -> impl Future, KernelError>> + + Send; + + fn find_by_id( + &self, + executor: &mut Self::Executor, + id: &EventId, + since: Option<&EventVersion>, + ) -> impl Future< + Output = error_stack::Result>, KernelError>, + > + Send; +} + +pub trait DependOnProfileEventStore: Sync + Send + DependOnDatabaseConnection { + type ProfileEventStore: ProfileEventStore< + Executor = ::Executor, + >; + + fn profile_event_store(&self) -> &Self::ProfileEventStore; +} diff --git a/kernel/src/lib.rs b/kernel/src/lib.rs index 68159b7..0671286 100644 --- a/kernel/src/lib.rs +++ b/kernel/src/lib.rs @@ -4,9 +4,8 @@ mod entity; mod error; mod event; mod event_store; -mod modify; -mod query; mod read_model; +mod repository; mod signal; pub use self::error::*; @@ -26,12 +25,6 @@ pub mod interfaces { pub mod database { pub use crate::database::*; } - pub mod query { - pub use crate::query::*; - } - pub mod modify { - pub use crate::modify::*; - } pub mod event { pub use crate::event::*; } @@ -41,6 +34,9 @@ pub mod interfaces { pub mod read_model { pub use crate::read_model::*; } + pub mod repository { + pub use crate::repository::*; + } pub mod signal { pub use crate::signal::*; } @@ -52,8 +48,12 @@ pub mod interfaces { /// - DependOnDatabaseConnection /// - DependOnAccountReadModel, DependOnAccountEventStore /// - DependOnAuthAccountReadModel, DependOnAuthAccountEventStore -/// - DependOnAuthHostQuery -/// - DependOnAuthHostModifier +/// - DependOnProfileReadModel, DependOnProfileEventStore +/// - DependOnMetadataReadModel, DependOnMetadataEventStore +/// - DependOnAuthHostRepository +/// - DependOnFollowRepository +/// - DependOnRemoteAccountRepository +/// - DependOnImageRepository /// /// # Usage /// ```ignore @@ -102,17 +102,59 @@ macro_rules! impl_database_delegation { } } - impl $crate::interfaces::query::DependOnAuthHostQuery for $impl_type { - type AuthHostQuery = <$db_type as $crate::interfaces::query::DependOnAuthHostQuery>::AuthHostQuery; - fn auth_host_query(&self) -> &Self::AuthHostQuery { - $crate::interfaces::query::DependOnAuthHostQuery::auth_host_query(&self.$field) + impl $crate::interfaces::read_model::DependOnProfileReadModel for $impl_type { + type ProfileReadModel = <$db_type as $crate::interfaces::read_model::DependOnProfileReadModel>::ProfileReadModel; + fn profile_read_model(&self) -> &Self::ProfileReadModel { + $crate::interfaces::read_model::DependOnProfileReadModel::profile_read_model(&self.$field) + } + } + + impl $crate::interfaces::event_store::DependOnProfileEventStore for $impl_type { + type ProfileEventStore = <$db_type as $crate::interfaces::event_store::DependOnProfileEventStore>::ProfileEventStore; + fn profile_event_store(&self) -> &Self::ProfileEventStore { + $crate::interfaces::event_store::DependOnProfileEventStore::profile_event_store(&self.$field) + } + } + + impl $crate::interfaces::read_model::DependOnMetadataReadModel for $impl_type { + type MetadataReadModel = <$db_type as $crate::interfaces::read_model::DependOnMetadataReadModel>::MetadataReadModel; + fn metadata_read_model(&self) -> &Self::MetadataReadModel { + $crate::interfaces::read_model::DependOnMetadataReadModel::metadata_read_model(&self.$field) + } + } + + impl $crate::interfaces::event_store::DependOnMetadataEventStore for $impl_type { + type MetadataEventStore = <$db_type as $crate::interfaces::event_store::DependOnMetadataEventStore>::MetadataEventStore; + fn metadata_event_store(&self) -> &Self::MetadataEventStore { + $crate::interfaces::event_store::DependOnMetadataEventStore::metadata_event_store(&self.$field) + } + } + + impl $crate::interfaces::repository::DependOnAuthHostRepository for $impl_type { + type AuthHostRepository = <$db_type as $crate::interfaces::repository::DependOnAuthHostRepository>::AuthHostRepository; + fn auth_host_repository(&self) -> &Self::AuthHostRepository { + $crate::interfaces::repository::DependOnAuthHostRepository::auth_host_repository(&self.$field) + } + } + + impl $crate::interfaces::repository::DependOnFollowRepository for $impl_type { + type FollowRepository = <$db_type as $crate::interfaces::repository::DependOnFollowRepository>::FollowRepository; + fn follow_repository(&self) -> &Self::FollowRepository { + $crate::interfaces::repository::DependOnFollowRepository::follow_repository(&self.$field) + } + } + + impl $crate::interfaces::repository::DependOnRemoteAccountRepository for $impl_type { + type RemoteAccountRepository = <$db_type as $crate::interfaces::repository::DependOnRemoteAccountRepository>::RemoteAccountRepository; + fn remote_account_repository(&self) -> &Self::RemoteAccountRepository { + $crate::interfaces::repository::DependOnRemoteAccountRepository::remote_account_repository(&self.$field) } } - impl $crate::interfaces::modify::DependOnAuthHostModifier for $impl_type { - type AuthHostModifier = <$db_type as $crate::interfaces::modify::DependOnAuthHostModifier>::AuthHostModifier; - fn auth_host_modifier(&self) -> &Self::AuthHostModifier { - $crate::interfaces::modify::DependOnAuthHostModifier::auth_host_modifier(&self.$field) + impl $crate::interfaces::repository::DependOnImageRepository for $impl_type { + type ImageRepository = <$db_type as $crate::interfaces::repository::DependOnImageRepository>::ImageRepository; + fn image_repository(&self) -> &Self::ImageRepository { + $crate::interfaces::repository::DependOnImageRepository::image_repository(&self.$field) } } diff --git a/kernel/src/modify.rs b/kernel/src/modify.rs deleted file mode 100644 index c673868..0000000 --- a/kernel/src/modify.rs +++ /dev/null @@ -1,8 +0,0 @@ -mod auth_host; -mod follow; -mod image; -mod metadata; -mod profile; -mod remote_account; - -pub use self::{auth_host::*, follow::*, image::*, metadata::*, profile::*, remote_account::*}; diff --git a/kernel/src/modify/auth_host.rs b/kernel/src/modify/auth_host.rs deleted file mode 100644 index 8aeb656..0000000 --- a/kernel/src/modify/auth_host.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Executor}; -use crate::entity::AuthHost; -use crate::KernelError; -use std::future::Future; - -pub trait AuthHostModifier: Sync + Send + 'static { - type Executor: Executor; - - fn create( - &self, - executor: &mut Self::Executor, - auth_host: &AuthHost, - ) -> impl Future> + Send; - - fn update( - &self, - executor: &mut Self::Executor, - auth_host: &AuthHost, - ) -> impl Future> + Send; -} - -pub trait DependOnAuthHostModifier: Sync + Send + DependOnDatabaseConnection { - type AuthHostModifier: AuthHostModifier< - Executor = ::Executor, - >; - - fn auth_host_modifier(&self) -> &Self::AuthHostModifier; -} diff --git a/kernel/src/modify/image.rs b/kernel/src/modify/image.rs deleted file mode 100644 index 93b9593..0000000 --- a/kernel/src/modify/image.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Executor}; -use crate::entity::{Image, ImageId}; -use crate::KernelError; -use std::future::Future; - -pub trait ImageModifier: Sync + Send + 'static { - type Executor: Executor; - - fn create( - &self, - executor: &mut Self::Executor, - image: &Image, - ) -> impl Future> + Send; - - fn delete( - &self, - executor: &mut Self::Executor, - image_id: &ImageId, - ) -> impl Future> + Send; -} - -pub trait DependOnImageModifier: Sync + Send + DependOnDatabaseConnection { - type ImageModifier: ImageModifier< - Executor = ::Executor, - >; - - fn image_modifier(&self) -> &Self::ImageModifier; -} diff --git a/kernel/src/modify/metadata.rs b/kernel/src/modify/metadata.rs deleted file mode 100644 index f31a748..0000000 --- a/kernel/src/modify/metadata.rs +++ /dev/null @@ -1,34 +0,0 @@ -use crate::database::{DependOnDatabaseConnection, Executor}; -use crate::entity::{Metadata, MetadataId}; -use crate::KernelError; -use std::future::Future; - -pub trait MetadataModifier: Sync + Send + 'static { - type Executor: Executor; - - fn create( - &self, - executor: &mut Self::Executor, - metadata: &Metadata, - ) -> impl Future> + Send; - - fn update( - &self, - executor: &mut Self::Executor, - metadata: &Metadata, - ) -> impl Future> + Send; - - fn delete( - &self, - executor: &mut Self::Executor, - metadata_id: &MetadataId, - ) -> impl Future> + Send; -} - -pub trait DependOnMetadataModifier: Sync + Send + DependOnDatabaseConnection { - type MetadataModifier: MetadataModifier< - Executor = ::Executor, - >; - - fn metadata_modifier(&self) -> &Self::MetadataModifier; -} diff --git a/kernel/src/modify/profile.rs b/kernel/src/modify/profile.rs deleted file mode 100644 index 9435133..0000000 --- a/kernel/src/modify/profile.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::database::{DependOnDatabaseConnection, Executor}; -use crate::entity::Profile; -use crate::KernelError; -use std::future::Future; - -pub trait ProfileModifier: Sync + Send + 'static { - type Executor: Executor; - - fn create( - &self, - executor: &mut Self::Executor, - profile: &Profile, - ) -> impl Future> + Send; - - fn update( - &self, - executor: &mut Self::Executor, - profile: &Profile, - ) -> impl Future> + Send; -} - -pub trait DependOnProfileModifier: Sync + Send + DependOnDatabaseConnection { - type ProfileModifier: ProfileModifier< - Executor = ::Executor, - >; - - fn profile_modifier(&self) -> &Self::ProfileModifier; -} diff --git a/kernel/src/modify/remote_account.rs b/kernel/src/modify/remote_account.rs deleted file mode 100644 index 9639826..0000000 --- a/kernel/src/modify/remote_account.rs +++ /dev/null @@ -1,34 +0,0 @@ -use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Executor}; -use crate::entity::{RemoteAccount, RemoteAccountId}; -use crate::KernelError; -use std::future::Future; - -pub trait RemoteAccountModifier: Sync + Send + 'static { - type Executor: Executor; - - fn create( - &self, - executor: &mut Self::Executor, - account: &RemoteAccount, - ) -> impl Future> + Send; - - fn update( - &self, - executor: &mut Self::Executor, - account: &RemoteAccount, - ) -> impl Future> + Send; - - fn delete( - &self, - executor: &mut Self::Executor, - account_id: &RemoteAccountId, - ) -> impl Future> + Send; -} - -pub trait DependOnRemoteAccountModifier: Sync + Send + DependOnDatabaseConnection { - type RemoteAccountModifier: RemoteAccountModifier< - Executor = ::Executor, - >; - - fn remote_account_modifier(&self) -> &Self::RemoteAccountModifier; -} diff --git a/kernel/src/query.rs b/kernel/src/query.rs deleted file mode 100644 index c673868..0000000 --- a/kernel/src/query.rs +++ /dev/null @@ -1,8 +0,0 @@ -mod auth_host; -mod follow; -mod image; -mod metadata; -mod profile; -mod remote_account; - -pub use self::{auth_host::*, follow::*, image::*, metadata::*, profile::*, remote_account::*}; diff --git a/kernel/src/query/follow.rs b/kernel/src/query/follow.rs deleted file mode 100644 index 21a6429..0000000 --- a/kernel/src/query/follow.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Executor}; -use crate::entity::{Follow, FollowTargetId}; -use crate::KernelError; -use std::future::Future; - -pub trait FollowQuery: Sync + Send + 'static { - type Executor: Executor; - - fn find_followings( - &self, - executor: &mut Self::Executor, - source: &FollowTargetId, - ) -> impl Future, KernelError>> + Send; - - fn find_followers( - &self, - executor: &mut Self::Executor, - destination: &FollowTargetId, - ) -> impl Future, KernelError>> + Send; -} - -pub trait DependOnFollowQuery: Sync + Send + DependOnDatabaseConnection { - type FollowQuery: FollowQuery< - Executor = ::Executor, - >; - - fn follow_query(&self) -> &Self::FollowQuery; -} diff --git a/kernel/src/query/metadata.rs b/kernel/src/query/metadata.rs deleted file mode 100644 index 824b421..0000000 --- a/kernel/src/query/metadata.rs +++ /dev/null @@ -1,28 +0,0 @@ -use crate::database::{DependOnDatabaseConnection, Executor}; -use crate::entity::{AccountId, Metadata, MetadataId}; -use crate::KernelError; -use std::future::Future; - -pub trait MetadataQuery: Sync + Send + 'static { - type Executor: Executor; - - fn find_by_id( - &self, - executor: &mut Self::Executor, - metadata_id: &MetadataId, - ) -> impl Future, KernelError>> + Send; - - fn find_by_account_id( - &self, - executor: &mut Self::Executor, - account_id: &AccountId, - ) -> impl Future, KernelError>> + Send; -} - -pub trait DependOnMetadataQuery: Sync + Send + DependOnDatabaseConnection { - type MetadataQuery: MetadataQuery< - Executor = ::Executor, - >; - - fn metadata_query(&self) -> &Self::MetadataQuery; -} diff --git a/kernel/src/query/profile.rs b/kernel/src/query/profile.rs deleted file mode 100644 index 0fa6130..0000000 --- a/kernel/src/query/profile.rs +++ /dev/null @@ -1,22 +0,0 @@ -use crate::database::{DependOnDatabaseConnection, Executor}; -use crate::entity::{Profile, ProfileId}; -use crate::KernelError; -use std::future::Future; - -pub trait ProfileQuery: Sync + Send + 'static { - type Executor: Executor; - - fn find_by_id( - &self, - executor: &mut Self::Executor, - id: &ProfileId, - ) -> impl Future, KernelError>> + Send; -} - -pub trait DependOnProfileQuery: Sync + Send + DependOnDatabaseConnection { - type ProfileQuery: ProfileQuery< - Executor = ::Executor, - >; - - fn profile_query(&self) -> &Self::ProfileQuery; -} diff --git a/kernel/src/read_model.rs b/kernel/src/read_model.rs index bd9ae1a..fa7d58c 100644 --- a/kernel/src/read_model.rs +++ b/kernel/src/read_model.rs @@ -1,5 +1,9 @@ mod account; mod auth_account; +mod metadata; +mod profile; pub use self::account::*; pub use self::auth_account::*; +pub use self::metadata::*; +pub use self::profile::*; diff --git a/kernel/src/read_model/metadata.rs b/kernel/src/read_model/metadata.rs new file mode 100644 index 0000000..eebf338 --- /dev/null +++ b/kernel/src/read_model/metadata.rs @@ -0,0 +1,48 @@ +use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Executor}; +use crate::entity::{AccountId, Metadata, MetadataId}; +use crate::KernelError; +use std::future::Future; + +pub trait MetadataReadModel: Sync + Send + 'static { + type Executor: Executor; + + // Query operations (projection reads) + fn find_by_id( + &self, + executor: &mut Self::Executor, + id: &MetadataId, + ) -> impl Future, KernelError>> + Send; + + fn find_by_account_id( + &self, + executor: &mut Self::Executor, + account_id: &AccountId, + ) -> impl Future, KernelError>> + Send; + + // Projection update operations (called by EventApplier pipeline) + fn create( + &self, + executor: &mut Self::Executor, + metadata: &Metadata, + ) -> impl Future> + Send; + + fn update( + &self, + executor: &mut Self::Executor, + metadata: &Metadata, + ) -> impl Future> + Send; + + fn delete( + &self, + executor: &mut Self::Executor, + metadata_id: &MetadataId, + ) -> impl Future> + Send; +} + +pub trait DependOnMetadataReadModel: Sync + Send + DependOnDatabaseConnection { + type MetadataReadModel: MetadataReadModel< + Executor = ::Executor, + >; + + fn metadata_read_model(&self) -> &Self::MetadataReadModel; +} diff --git a/kernel/src/read_model/profile.rs b/kernel/src/read_model/profile.rs new file mode 100644 index 0000000..881a28f --- /dev/null +++ b/kernel/src/read_model/profile.rs @@ -0,0 +1,48 @@ +use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Executor}; +use crate::entity::{AccountId, Profile, ProfileId}; +use crate::KernelError; +use std::future::Future; + +pub trait ProfileReadModel: Sync + Send + 'static { + type Executor: Executor; + + // Query operations (projection reads) + fn find_by_id( + &self, + executor: &mut Self::Executor, + id: &ProfileId, + ) -> impl Future, KernelError>> + Send; + + fn find_by_account_id( + &self, + executor: &mut Self::Executor, + account_id: &AccountId, + ) -> impl Future, KernelError>> + Send; + + // Projection update operations (called by EventApplier pipeline) + fn create( + &self, + executor: &mut Self::Executor, + profile: &Profile, + ) -> impl Future> + Send; + + fn update( + &self, + executor: &mut Self::Executor, + profile: &Profile, + ) -> impl Future> + Send; + + fn delete( + &self, + executor: &mut Self::Executor, + profile_id: &ProfileId, + ) -> impl Future> + Send; +} + +pub trait DependOnProfileReadModel: Sync + Send + DependOnDatabaseConnection { + type ProfileReadModel: ProfileReadModel< + Executor = ::Executor, + >; + + fn profile_read_model(&self) -> &Self::ProfileReadModel; +} diff --git a/kernel/src/repository.rs b/kernel/src/repository.rs new file mode 100644 index 0000000..41ed3c9 --- /dev/null +++ b/kernel/src/repository.rs @@ -0,0 +1,9 @@ +mod auth_host; +mod follow; +mod image; +mod remote_account; + +pub use self::auth_host::*; +pub use self::follow::*; +pub use self::image::*; +pub use self::remote_account::*; diff --git a/kernel/src/query/auth_host.rs b/kernel/src/repository/auth_host.rs similarity index 51% rename from kernel/src/query/auth_host.rs rename to kernel/src/repository/auth_host.rs index 8396075..3add4c2 100644 --- a/kernel/src/query/auth_host.rs +++ b/kernel/src/repository/auth_host.rs @@ -3,7 +3,7 @@ use crate::entity::{AuthHost, AuthHostId, AuthHostUrl}; use crate::KernelError; use std::future::Future; -pub trait AuthHostQuery: Sync + Send + 'static { +pub trait AuthHostRepository: Sync + Send + 'static { type Executor: Executor; fn find_by_id( @@ -15,14 +15,26 @@ pub trait AuthHostQuery: Sync + Send + 'static { fn find_by_url( &self, executor: &mut Self::Executor, - domain: &AuthHostUrl, + url: &AuthHostUrl, ) -> impl Future, KernelError>> + Send; + + fn create( + &self, + executor: &mut Self::Executor, + auth_host: &AuthHost, + ) -> impl Future> + Send; + + fn update( + &self, + executor: &mut Self::Executor, + auth_host: &AuthHost, + ) -> impl Future> + Send; } -pub trait DependOnAuthHostQuery: Sync + Send + DependOnDatabaseConnection { - type AuthHostQuery: AuthHostQuery< +pub trait DependOnAuthHostRepository: Sync + Send + DependOnDatabaseConnection { + type AuthHostRepository: AuthHostRepository< Executor = ::Executor, >; - fn auth_host_query(&self) -> &Self::AuthHostQuery; + fn auth_host_repository(&self) -> &Self::AuthHostRepository; } diff --git a/kernel/src/modify/follow.rs b/kernel/src/repository/follow.rs similarity index 52% rename from kernel/src/modify/follow.rs rename to kernel/src/repository/follow.rs index b807aa8..11e7508 100644 --- a/kernel/src/modify/follow.rs +++ b/kernel/src/repository/follow.rs @@ -1,11 +1,23 @@ use crate::database::{DatabaseConnection, DependOnDatabaseConnection, Executor}; -use crate::entity::{Follow, FollowId}; +use crate::entity::{Follow, FollowId, FollowTargetId}; use crate::KernelError; use std::future::Future; -pub trait FollowModifier: Sync + Send + 'static { +pub trait FollowRepository: Sync + Send + 'static { type Executor: Executor; + fn find_followings( + &self, + executor: &mut Self::Executor, + source: &FollowTargetId, + ) -> impl Future, KernelError>> + Send; + + fn find_followers( + &self, + executor: &mut Self::Executor, + destination: &FollowTargetId, + ) -> impl Future, KernelError>> + Send; + fn create( &self, executor: &mut Self::Executor, @@ -25,10 +37,10 @@ pub trait FollowModifier: Sync + Send + 'static { ) -> impl Future> + Send; } -pub trait DependOnFollowModifier: Sync + Send + DependOnDatabaseConnection { - type FollowModifier: FollowModifier< +pub trait DependOnFollowRepository: Sync + Send + DependOnDatabaseConnection { + type FollowRepository: FollowRepository< Executor = ::Executor, >; - fn follow_modifier(&self) -> &Self::FollowModifier; + fn follow_repository(&self) -> &Self::FollowRepository; } diff --git a/kernel/src/query/image.rs b/kernel/src/repository/image.rs similarity index 53% rename from kernel/src/query/image.rs rename to kernel/src/repository/image.rs index 71568fc..8c0ba1c 100644 --- a/kernel/src/query/image.rs +++ b/kernel/src/repository/image.rs @@ -3,7 +3,7 @@ use crate::entity::{Image, ImageId, ImageUrl}; use crate::KernelError; use std::future::Future; -pub trait ImageQuery: Sync + Send + 'static { +pub trait ImageRepository: Sync + Send + 'static { type Executor: Executor; fn find_by_id( @@ -17,12 +17,24 @@ pub trait ImageQuery: Sync + Send + 'static { executor: &mut Self::Executor, url: &ImageUrl, ) -> impl Future, KernelError>> + Send; + + fn create( + &self, + executor: &mut Self::Executor, + image: &Image, + ) -> impl Future> + Send; + + fn delete( + &self, + executor: &mut Self::Executor, + image_id: &ImageId, + ) -> impl Future> + Send; } -pub trait DependOnImageQuery: Sync + Send + DependOnDatabaseConnection { - type ImageQuery: ImageQuery< +pub trait DependOnImageRepository: Sync + Send + DependOnDatabaseConnection { + type ImageRepository: ImageRepository< Executor = ::Executor, >; - fn image_query(&self) -> &Self::ImageQuery; + fn image_repository(&self) -> &Self::ImageRepository; } diff --git a/kernel/src/query/remote_account.rs b/kernel/src/repository/remote_account.rs similarity index 53% rename from kernel/src/query/remote_account.rs rename to kernel/src/repository/remote_account.rs index ee911bf..beab5d5 100644 --- a/kernel/src/query/remote_account.rs +++ b/kernel/src/repository/remote_account.rs @@ -3,7 +3,7 @@ use crate::entity::{RemoteAccount, RemoteAccountAcct, RemoteAccountId, RemoteAcc use crate::KernelError; use std::future::Future; -pub trait RemoteAccountQuery: Sync + Send + 'static { +pub trait RemoteAccountRepository: Sync + Send + 'static { type Executor: Executor; fn find_by_id( @@ -23,12 +23,30 @@ pub trait RemoteAccountQuery: Sync + Send + 'static { executor: &mut Self::Executor, url: &RemoteAccountUrl, ) -> impl Future, KernelError>> + Send; + + fn create( + &self, + executor: &mut Self::Executor, + account: &RemoteAccount, + ) -> impl Future> + Send; + + fn update( + &self, + executor: &mut Self::Executor, + account: &RemoteAccount, + ) -> impl Future> + Send; + + fn delete( + &self, + executor: &mut Self::Executor, + account_id: &RemoteAccountId, + ) -> impl Future> + Send; } -pub trait DependOnRemoteAccountQuery: Sync + Send + DependOnDatabaseConnection { - type RemoteAccountQuery: RemoteAccountQuery< +pub trait DependOnRemoteAccountRepository: Sync + Send + DependOnDatabaseConnection { + type RemoteAccountRepository: RemoteAccountRepository< Executor = ::Executor, >; - fn remote_account_query(&self) -> &Self::RemoteAccountQuery; + fn remote_account_repository(&self) -> &Self::RemoteAccountRepository; } diff --git a/migrations/20260310000000_profile_events.sql b/migrations/20260310000000_profile_events.sql new file mode 100644 index 0000000..0ba3a77 --- /dev/null +++ b/migrations/20260310000000_profile_events.sql @@ -0,0 +1,10 @@ +CREATE TABLE "profile_events" ( + "version" UUID NOT NULL, + "id" UUID NOT NULL, + "event_name" TEXT NOT NULL, + "data" JSONB NOT NULL, + "occurred_at" TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY ("id", "version") +); + +ALTER TABLE "profiles" ADD CONSTRAINT "profiles_account_id_unique" UNIQUE ("account_id"); diff --git a/migrations/20260311000000_metadata_events.sql b/migrations/20260311000000_metadata_events.sql new file mode 100644 index 0000000..b763920 --- /dev/null +++ b/migrations/20260311000000_metadata_events.sql @@ -0,0 +1,8 @@ +CREATE TABLE "metadata_events" ( + "version" UUID NOT NULL, + "id" UUID NOT NULL, + "event_name" TEXT NOT NULL, + "data" JSONB NOT NULL, + "occurred_at" TIMESTAMPTZ NOT NULL DEFAULT now(), + PRIMARY KEY ("id", "version") +); diff --git a/server/src/applier.rs b/server/src/applier.rs index a585fe0..20737bd 100644 --- a/server/src/applier.rs +++ b/server/src/applier.rs @@ -2,15 +2,21 @@ use crate::handler::Handler; use account_applier::AccountApplier; use auth_account_applier::AuthAccountApplier; use kernel::interfaces::signal::Signal; -use kernel::prelude::entity::{AccountId, AuthAccountId}; +use kernel::prelude::entity::{AccountId, AuthAccountId, MetadataId, ProfileId}; +use metadata_applier::MetadataApplier; +use profile_applier::ProfileApplier; use std::sync::Arc; mod account_applier; mod auth_account_applier; +mod metadata_applier; +mod profile_applier; pub struct ApplierContainer { account_applier: AccountApplier, auth_account_applier: AuthAccountApplier, + profile_applier: ProfileApplier, + metadata_applier: MetadataApplier, } impl ApplierContainer { @@ -18,6 +24,8 @@ impl ApplierContainer { Self { account_applier: AccountApplier::new(module.clone()), auth_account_applier: AuthAccountApplier::new(module.clone()), + profile_applier: ProfileApplier::new(module.clone()), + metadata_applier: MetadataApplier::new(module.clone()), } } } @@ -34,3 +42,5 @@ macro_rules! impl_signal { impl_signal!(AccountId, account_applier); impl_signal!(AuthAccountId, auth_account_applier); +impl_signal!(ProfileId, profile_applier); +impl_signal!(MetadataId, metadata_applier); diff --git a/server/src/applier/metadata_applier.rs b/server/src/applier/metadata_applier.rs new file mode 100644 index 0000000..0fe659b --- /dev/null +++ b/server/src/applier/metadata_applier.rs @@ -0,0 +1,41 @@ +use crate::handler::Handler; +use application::service::metadata::UpdateMetadata; +use error_stack::ResultExt; +use kernel::interfaces::signal::Signal; +use kernel::prelude::entity::MetadataId; +use rikka_mq::define::redis::mq::RedisMessageQueue; +use rikka_mq::mq::MessageQueue; +use std::sync::Arc; +use uuid::Uuid; + +pub(crate) struct MetadataApplier(RedisMessageQueue, Uuid, MetadataId>); + +impl MetadataApplier { + pub fn new(handler: Arc) -> Self { + let queue = RedisMessageQueue::new( + handler.redis().pool().clone(), + handler, + "metadata_applier".to_string(), + rikka_mq::config::MQConfig::default(), + Uuid::new_v4, + |handler: Arc, id: MetadataId| async move { + handler + .pgpool() + .update_metadata(id) + .await + .map_err(|e| rikka_mq::error::ErrorOperation::Delay(format!("{:?}", e))) + }, + ); + MetadataApplier(queue) + } +} + +impl Signal for MetadataApplier { + async fn emit(&self, signal_id: MetadataId) -> error_stack::Result<(), kernel::KernelError> { + self.0 + .queue(rikka_mq::info::QueueInfo::new(Uuid::new_v4(), signal_id)) + .await + .map_err(|e| error_stack::Report::new(e)) + .change_context_lazy(|| kernel::KernelError::Internal) + } +} diff --git a/server/src/applier/profile_applier.rs b/server/src/applier/profile_applier.rs new file mode 100644 index 0000000..2405813 --- /dev/null +++ b/server/src/applier/profile_applier.rs @@ -0,0 +1,41 @@ +use crate::handler::Handler; +use application::service::profile::UpdateProfile; +use error_stack::ResultExt; +use kernel::interfaces::signal::Signal; +use kernel::prelude::entity::ProfileId; +use rikka_mq::define::redis::mq::RedisMessageQueue; +use rikka_mq::mq::MessageQueue; +use std::sync::Arc; +use uuid::Uuid; + +pub(crate) struct ProfileApplier(RedisMessageQueue, Uuid, ProfileId>); + +impl ProfileApplier { + pub fn new(handler: Arc) -> Self { + let queue = RedisMessageQueue::new( + handler.redis().pool().clone(), + handler, + "profile_applier".to_string(), + rikka_mq::config::MQConfig::default(), + Uuid::new_v4, + |handler: Arc, id: ProfileId| async move { + handler + .pgpool() + .update_profile(id) + .await + .map_err(|e| rikka_mq::error::ErrorOperation::Delay(format!("{:?}", e))) + }, + ); + ProfileApplier(queue) + } +} + +impl Signal for ProfileApplier { + async fn emit(&self, signal_id: ProfileId) -> error_stack::Result<(), kernel::KernelError> { + self.0 + .queue(rikka_mq::info::QueueInfo::new(Uuid::new_v4(), signal_id)) + .await + .map_err(|e| error_stack::Report::new(e)) + .change_context_lazy(|| kernel::KernelError::Internal) + } +} diff --git a/server/src/handler.rs b/server/src/handler.rs index ddc8e0b..1e8fc61 100644 --- a/server/src/handler.rs +++ b/server/src/handler.rs @@ -1,6 +1,8 @@ use crate::applier::ApplierContainer; use adapter::processor::account::DependOnAccountSignal; use adapter::processor::auth_account::DependOnAuthAccountSignal; +use adapter::processor::metadata::DependOnMetadataSignal; +use adapter::processor::profile::DependOnProfileSignal; use driver::crypto::{ Argon2Encryptor, FilePasswordProvider, Rsa2048RawGenerator, Rsa2048Signer, Rsa2048Verifier, }; @@ -111,20 +113,91 @@ impl kernel::interfaces::event_store::DependOnAuthAccountEventStore for AppModul } } -impl kernel::interfaces::query::DependOnAuthHostQuery for AppModule { - type AuthHostQuery = - ::AuthHostQuery; - fn auth_host_query(&self) -> &Self::AuthHostQuery { - kernel::interfaces::query::DependOnAuthHostQuery::auth_host_query( +impl kernel::interfaces::read_model::DependOnProfileReadModel for AppModule { + type ProfileReadModel = ::ProfileReadModel; + fn profile_read_model(&self) -> &Self::ProfileReadModel { + kernel::interfaces::read_model::DependOnProfileReadModel::profile_read_model( self.handler.as_ref().database_connection(), ) } } -impl kernel::interfaces::modify::DependOnAuthHostModifier for AppModule { - type AuthHostModifier = ::AuthHostModifier; - fn auth_host_modifier(&self) -> &Self::AuthHostModifier { - kernel::interfaces::modify::DependOnAuthHostModifier::auth_host_modifier( +impl kernel::interfaces::event_store::DependOnProfileEventStore for AppModule { + type ProfileEventStore = ::ProfileEventStore; + fn profile_event_store(&self) -> &Self::ProfileEventStore { + kernel::interfaces::event_store::DependOnProfileEventStore::profile_event_store( + self.handler.as_ref().database_connection(), + ) + } +} + +impl DependOnProfileSignal for AppModule { + type ProfileSignal = ApplierContainer; + fn profile_signal(&self) -> &Self::ProfileSignal { + &self.applier_container + } +} + +impl kernel::interfaces::read_model::DependOnMetadataReadModel for AppModule { + type MetadataReadModel = ::MetadataReadModel; + fn metadata_read_model(&self) -> &Self::MetadataReadModel { + kernel::interfaces::read_model::DependOnMetadataReadModel::metadata_read_model( + self.handler.as_ref().database_connection(), + ) + } +} + +impl kernel::interfaces::event_store::DependOnMetadataEventStore for AppModule { + type MetadataEventStore = ::MetadataEventStore; + fn metadata_event_store(&self) -> &Self::MetadataEventStore { + kernel::interfaces::event_store::DependOnMetadataEventStore::metadata_event_store( + self.handler.as_ref().database_connection(), + ) + } +} + +impl DependOnMetadataSignal for AppModule { + type MetadataSignal = ApplierContainer; + fn metadata_signal(&self) -> &Self::MetadataSignal { + &self.applier_container + } +} + +impl kernel::interfaces::repository::DependOnAuthHostRepository for AppModule { + type AuthHostRepository = + ::AuthHostRepository; + fn auth_host_repository(&self) -> &Self::AuthHostRepository { + kernel::interfaces::repository::DependOnAuthHostRepository::auth_host_repository( + self.handler.as_ref().database_connection(), + ) + } +} + +impl kernel::interfaces::repository::DependOnFollowRepository for AppModule { + type FollowRepository = + ::FollowRepository; + fn follow_repository(&self) -> &Self::FollowRepository { + kernel::interfaces::repository::DependOnFollowRepository::follow_repository( + self.handler.as_ref().database_connection(), + ) + } +} + +impl kernel::interfaces::repository::DependOnRemoteAccountRepository for AppModule { + type RemoteAccountRepository = + ::RemoteAccountRepository; + fn remote_account_repository(&self) -> &Self::RemoteAccountRepository { + kernel::interfaces::repository::DependOnRemoteAccountRepository::remote_account_repository( + self.handler.as_ref().database_connection(), + ) + } +} + +impl kernel::interfaces::repository::DependOnImageRepository for AppModule { + type ImageRepository = + ::ImageRepository; + fn image_repository(&self) -> &Self::ImageRepository { + kernel::interfaces::repository::DependOnImageRepository::image_repository( self.handler.as_ref().database_connection(), ) } diff --git a/server/src/keycloak.rs b/server/src/keycloak.rs index b9858e0..1544adc 100644 --- a/server/src/keycloak.rs +++ b/server/src/keycloak.rs @@ -7,8 +7,7 @@ use axum_keycloak_auth::decode::KeycloakToken; use axum_keycloak_auth::instance::{KeycloakAuthInstance, KeycloakConfig}; use axum_keycloak_auth::role::Role; use kernel::interfaces::database::{DatabaseConnection, DependOnDatabaseConnection}; -use kernel::interfaces::modify::{AuthHostModifier, DependOnAuthHostModifier}; -use kernel::interfaces::query::{AuthHostQuery, DependOnAuthHostQuery}; +use kernel::interfaces::repository::{AuthHostRepository, DependOnAuthHostRepository}; use kernel::prelude::entity::{ AuthAccountClientId, AuthAccountId, AuthHost, AuthHostId, AuthHostUrl, }; @@ -64,14 +63,14 @@ pub async fn resolve_auth_account_id( } else { let url = AuthHostUrl::new(auth_info.host_url); let auth_host = app - .auth_host_query() + .auth_host_repository() .find_by_url(&mut executor, &url) .await?; let auth_host = if let Some(auth_host) = auth_host { auth_host } else { let auth_host = AuthHost::new(AuthHostId::default(), url); - app.auth_host_modifier() + app.auth_host_repository() .create(&mut executor, &auth_host) .await?; auth_host diff --git a/server/src/main.rs b/server/src/main.rs index 3bb8d0b..ff83d4b 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -9,9 +9,12 @@ use crate::error::StackTrace; use crate::handler::AppModule; use crate::keycloak::create_keycloak_instance; use crate::route::account::AccountRouter; +use crate::route::metadata::MetadataRouter; +use crate::route::profile::ProfileRouter; use error_stack::ResultExt; use kernel::KernelError; use std::net::SocketAddr; +use std::sync::Arc; use tokio::net::TcpListener; use tower_http::cors::CorsLayer; use tracing_subscriber::layer::SubscriberExt; @@ -40,11 +43,13 @@ async fn main() -> Result<(), StackTrace> { ) .init(); - let keycloak_auth_instance = create_keycloak_instance(); + let keycloak_auth_instance = Arc::new(create_keycloak_instance()); let app = AppModule::new().await?; let router = axum::Router::new() - .route_account(keycloak_auth_instance) + .route_account(keycloak_auth_instance.clone()) + .route_profile(keycloak_auth_instance.clone()) + .route_metadata(keycloak_auth_instance) .layer(CorsLayer::new()) .with_state(app); diff --git a/server/src/route.rs b/server/src/route.rs index ad3c54d..708cf58 100644 --- a/server/src/route.rs +++ b/server/src/route.rs @@ -3,6 +3,8 @@ use application::transfer::pagination::Direction; use axum::http::StatusCode; pub mod account; +pub mod metadata; +pub mod profile; trait DirectionConverter { fn convert_to_direction(self) -> Result; diff --git a/server/src/route/account.rs b/server/src/route/account.rs index 7a2c5da..4f62091 100644 --- a/server/src/route/account.rs +++ b/server/src/route/account.rs @@ -17,6 +17,7 @@ use axum_keycloak_auth::instance::KeycloakAuthInstance; use axum_keycloak_auth::layer::KeycloakAuthLayer; use axum_keycloak_auth::PassthroughMode; use serde::{Deserialize, Serialize}; +use std::sync::Arc; use time::OffsetDateTime; #[derive(Debug, Deserialize)] @@ -54,7 +55,7 @@ struct AccountsResponse { } pub trait AccountRouter { - fn route_account(self, instance: KeycloakAuthInstance) -> Self; + fn route_account(self, instance: Arc) -> Self; } async fn get_accounts( @@ -236,7 +237,7 @@ async fn delete_account_by_id( } impl AccountRouter for Router { - fn route_account(self, instance: KeycloakAuthInstance) -> Self { + fn route_account(self, instance: Arc) -> Self { self.route("/accounts", get(get_accounts)) .route("/accounts", post(create_account)) .route("/accounts/:id", get(get_account_by_id)) diff --git a/server/src/route/metadata.rs b/server/src/route/metadata.rs new file mode 100644 index 0000000..82be9e7 --- /dev/null +++ b/server/src/route/metadata.rs @@ -0,0 +1,235 @@ +use crate::error::ErrorStatus; +use crate::expect_role; +use crate::handler::AppModule; +use crate::keycloak::{resolve_auth_account_id, KeycloakAuthAccount}; +use application::service::metadata::{ + CreateMetadataUseCase, DeleteMetadataUseCase, EditMetadataUseCase, GetMetadataUseCase, +}; +use axum::extract::{Path, State}; +use axum::http::StatusCode; +use axum::http::{Method, Uri}; +use axum::routing::{get, put}; +use axum::{Extension, Json, Router}; +use axum_keycloak_auth::decode::KeycloakToken; +use axum_keycloak_auth::instance::KeycloakAuthInstance; +use axum_keycloak_auth::layer::KeycloakAuthLayer; +use axum_keycloak_auth::PassthroughMode; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; + +#[derive(Debug, Deserialize)] +struct CreateMetadataRequest { + label: String, + content: String, +} + +#[derive(Debug, Deserialize)] +struct UpdateMetadataRequest { + label: String, + content: String, +} + +#[derive(Debug, Serialize)] +struct MetadataResponse { + nanoid: String, + label: String, + content: String, +} + +impl From for MetadataResponse { + fn from(dto: application::transfer::metadata::MetadataDto) -> Self { + Self { + nanoid: dto.nanoid, + label: dto.label, + content: dto.content, + } + } +} + +pub trait MetadataRouter { + fn route_metadata(self, keycloak: Arc) -> Self; +} + +async fn get_metadata( + Extension(token): Extension>, + State(module): State, + method: Method, + uri: Uri, + Path(account_id): Path, +) -> Result>, ErrorStatus> { + expect_role!(&token, uri, method); + let auth_info = KeycloakAuthAccount::from(token); + + if account_id.trim().is_empty() { + return Err(ErrorStatus::from(( + StatusCode::BAD_REQUEST, + "Account ID cannot be empty".to_string(), + ))); + } + + let auth_account_id = resolve_auth_account_id(&module, auth_info) + .await + .map_err(ErrorStatus::from)?; + + let metadata_list = module + .get_metadata(&auth_account_id, account_id) + .await + .map_err(ErrorStatus::from)?; + + Ok(Json( + metadata_list + .into_iter() + .map(MetadataResponse::from) + .collect(), + )) +} + +async fn create_metadata( + Extension(token): Extension>, + State(module): State, + method: Method, + uri: Uri, + Path(account_id): Path, + Json(body): Json, +) -> Result<(StatusCode, Json), ErrorStatus> { + expect_role!(&token, uri, method); + let auth_info = KeycloakAuthAccount::from(token); + + if account_id.trim().is_empty() { + return Err(ErrorStatus::from(( + StatusCode::BAD_REQUEST, + "Account ID cannot be empty".to_string(), + ))); + } + + let auth_account_id = resolve_auth_account_id(&module, auth_info) + .await + .map_err(ErrorStatus::from)?; + + let metadata = module + .create_metadata(&auth_account_id, account_id, body.label, body.content) + .await + .map_err(ErrorStatus::from)?; + + Ok((StatusCode::CREATED, Json(MetadataResponse::from(metadata)))) +} + +async fn update_metadata( + Extension(token): Extension>, + State(module): State, + method: Method, + uri: Uri, + Path((account_id, metadata_id)): Path<(String, String)>, + Json(body): Json, +) -> Result { + expect_role!(&token, uri, method); + let auth_info = KeycloakAuthAccount::from(token); + + if account_id.trim().is_empty() { + return Err(ErrorStatus::from(( + StatusCode::BAD_REQUEST, + "Account ID cannot be empty".to_string(), + ))); + } + + if metadata_id.trim().is_empty() { + return Err(ErrorStatus::from(( + StatusCode::BAD_REQUEST, + "Metadata ID cannot be empty".to_string(), + ))); + } + + let auth_account_id = resolve_auth_account_id(&module, auth_info) + .await + .map_err(ErrorStatus::from)?; + + module + .edit_metadata( + &auth_account_id, + account_id, + metadata_id, + body.label, + body.content, + ) + .await + .map_err(ErrorStatus::from)?; + + Ok(StatusCode::NO_CONTENT) +} + +async fn delete_metadata( + Extension(token): Extension>, + State(module): State, + method: Method, + uri: Uri, + Path((account_id, metadata_id)): Path<(String, String)>, +) -> Result { + expect_role!(&token, uri, method); + let auth_info = KeycloakAuthAccount::from(token); + + if account_id.trim().is_empty() { + return Err(ErrorStatus::from(( + StatusCode::BAD_REQUEST, + "Account ID cannot be empty".to_string(), + ))); + } + + if metadata_id.trim().is_empty() { + return Err(ErrorStatus::from(( + StatusCode::BAD_REQUEST, + "Metadata ID cannot be empty".to_string(), + ))); + } + + let auth_account_id = resolve_auth_account_id(&module, auth_info) + .await + .map_err(ErrorStatus::from)?; + + module + .delete_metadata(&auth_account_id, account_id, metadata_id) + .await + .map_err(ErrorStatus::from)?; + + Ok(StatusCode::NO_CONTENT) +} + +impl MetadataRouter for Router { + fn route_metadata(self, keycloak: Arc) -> Self { + self.route( + "/accounts/:account_id/metadata", + get(get_metadata).post(create_metadata), + ) + .route( + "/accounts/:account_id/metadata/:id", + put(update_metadata).delete(delete_metadata), + ) + .layer( + KeycloakAuthLayer::::builder() + .instance(keycloak) + .passthrough_mode(PassthroughMode::Block) + .expected_audiences(vec![String::from("account")]) + .build(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use application::transfer::metadata::MetadataDto; + + #[test] + fn test_metadata_response_from_dto() { + let dto = MetadataDto { + nanoid: "test-nanoid".to_string(), + label: "test-label".to_string(), + content: "test-content".to_string(), + }; + + let response = MetadataResponse::from(dto); + + assert_eq!(response.nanoid, "test-nanoid"); + assert_eq!(response.label, "test-label"); + assert_eq!(response.content, "test-content"); + } +} diff --git a/server/src/route/profile.rs b/server/src/route/profile.rs new file mode 100644 index 0000000..c076598 --- /dev/null +++ b/server/src/route/profile.rs @@ -0,0 +1,262 @@ +use crate::error::ErrorStatus; +use crate::expect_role; +use crate::handler::AppModule; +use crate::keycloak::{resolve_auth_account_id, KeycloakAuthAccount}; +use application::service::profile::{ + CreateProfileUseCase, DeleteProfileUseCase, EditProfileUseCase, GetProfileUseCase, +}; +use axum::extract::{Path, State}; +use axum::http::StatusCode; +use axum::http::{Method, Uri}; +use axum::routing::{delete, get, post, put}; +use axum::{Extension, Json, Router}; +use axum_keycloak_auth::decode::KeycloakToken; +use axum_keycloak_auth::instance::KeycloakAuthInstance; +use axum_keycloak_auth::layer::KeycloakAuthLayer; +use axum_keycloak_auth::PassthroughMode; +use kernel::prelude::entity::ImageId; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use uuid::Uuid; + +#[derive(Debug, Deserialize)] +struct CreateProfileRequest { + display_name: Option, + summary: Option, + icon: Option, + banner: Option, +} + +#[derive(Debug, Deserialize)] +struct UpdateProfileRequest { + display_name: Option, + summary: Option, + icon: Option, + banner: Option, +} + +#[derive(Debug, Serialize)] +struct ProfileResponse { + nanoid: String, + display_name: Option, + summary: Option, + icon_id: Option, + banner_id: Option, +} + +impl From for ProfileResponse { + fn from(dto: application::transfer::profile::ProfileDto) -> Self { + Self { + nanoid: dto.nanoid, + display_name: dto.display_name, + summary: dto.summary, + icon_id: dto.icon_id, + banner_id: dto.banner_id, + } + } +} + +pub trait ProfileRouter { + fn route_profile(self, keycloak: Arc) -> Self; +} + +async fn get_profile( + Extension(token): Extension>, + State(module): State, + method: Method, + uri: Uri, + Path(account_id): Path, +) -> Result, ErrorStatus> { + expect_role!(&token, uri, method); + let auth_info = KeycloakAuthAccount::from(token); + + if account_id.trim().is_empty() { + return Err(ErrorStatus::from(( + StatusCode::BAD_REQUEST, + "Account ID cannot be empty".to_string(), + ))); + } + + let auth_account_id = resolve_auth_account_id(&module, auth_info) + .await + .map_err(ErrorStatus::from)?; + + let profile = module + .get_profile(&auth_account_id, account_id) + .await + .map_err(ErrorStatus::from)?; + + Ok(Json(ProfileResponse::from(profile))) +} + +async fn create_profile( + Extension(token): Extension>, + State(module): State, + method: Method, + uri: Uri, + Path(account_id): Path, + Json(body): Json, +) -> Result<(StatusCode, Json), ErrorStatus> { + expect_role!(&token, uri, method); + let auth_info = KeycloakAuthAccount::from(token); + + if account_id.trim().is_empty() { + return Err(ErrorStatus::from(( + StatusCode::BAD_REQUEST, + "Account ID cannot be empty".to_string(), + ))); + } + + let auth_account_id = resolve_auth_account_id(&module, auth_info) + .await + .map_err(ErrorStatus::from)?; + + let icon = body.icon.map(ImageId::new); + let banner = body.banner.map(ImageId::new); + + let profile = module + .create_profile( + &auth_account_id, + account_id, + body.display_name, + body.summary, + icon, + banner, + ) + .await + .map_err(ErrorStatus::from)?; + + Ok((StatusCode::CREATED, Json(ProfileResponse::from(profile)))) +} + +async fn update_profile( + Extension(token): Extension>, + State(module): State, + method: Method, + uri: Uri, + Path(account_id): Path, + Json(body): Json, +) -> Result { + expect_role!(&token, uri, method); + let auth_info = KeycloakAuthAccount::from(token); + + if account_id.trim().is_empty() { + return Err(ErrorStatus::from(( + StatusCode::BAD_REQUEST, + "Account ID cannot be empty".to_string(), + ))); + } + + let auth_account_id = resolve_auth_account_id(&module, auth_info) + .await + .map_err(ErrorStatus::from)?; + + let icon = body.icon.map(ImageId::new); + let banner = body.banner.map(ImageId::new); + + module + .edit_profile( + &auth_account_id, + account_id, + body.display_name, + body.summary, + icon, + banner, + ) + .await + .map_err(ErrorStatus::from)?; + + Ok(StatusCode::NO_CONTENT) +} + +async fn delete_profile( + Extension(token): Extension>, + State(module): State, + method: Method, + uri: Uri, + Path(account_id): Path, +) -> Result { + expect_role!(&token, uri, method); + let auth_info = KeycloakAuthAccount::from(token); + + if account_id.trim().is_empty() { + return Err(ErrorStatus::from(( + StatusCode::BAD_REQUEST, + "Account ID cannot be empty".to_string(), + ))); + } + + let auth_account_id = resolve_auth_account_id(&module, auth_info) + .await + .map_err(ErrorStatus::from)?; + + module + .delete_profile(&auth_account_id, account_id) + .await + .map_err(ErrorStatus::from)?; + + Ok(StatusCode::NO_CONTENT) +} + +impl ProfileRouter for Router { + fn route_profile(self, keycloak: Arc) -> Self { + self.route( + "/accounts/:account_id/profile", + get(get_profile) + .post(create_profile) + .put(update_profile) + .delete(delete_profile), + ) + .layer( + KeycloakAuthLayer::::builder() + .instance(keycloak) + .passthrough_mode(PassthroughMode::Block) + .expected_audiences(vec![String::from("account")]) + .build(), + ) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use application::transfer::profile::ProfileDto; + + #[test] + fn test_profile_response_from_dto_with_all_fields() { + let dto = ProfileDto { + nanoid: "test-nanoid".to_string(), + display_name: Some("Test User".to_string()), + summary: Some("A test summary".to_string()), + icon_id: Some(Uuid::nil()), + banner_id: Some(Uuid::nil()), + }; + + let response = ProfileResponse::from(dto); + + assert_eq!(response.nanoid, "test-nanoid"); + assert_eq!(response.display_name, Some("Test User".to_string())); + assert_eq!(response.summary, Some("A test summary".to_string())); + assert_eq!(response.icon_id, Some(Uuid::nil())); + assert_eq!(response.banner_id, Some(Uuid::nil())); + } + + #[test] + fn test_profile_response_from_dto_with_no_optional_fields() { + let dto = ProfileDto { + nanoid: "test-nanoid-2".to_string(), + display_name: None, + summary: None, + icon_id: None, + banner_id: None, + }; + + let response = ProfileResponse::from(dto); + + assert_eq!(response.nanoid, "test-nanoid-2"); + assert!(response.display_name.is_none()); + assert!(response.summary.is_none()); + assert!(response.icon_id.is_none()); + assert!(response.banner_id.is_none()); + } +} From 21867bd87451653e594c3d85034465dad079bd46 Mon Sep 17 00:00:00 2001 From: turtton Date: Thu, 12 Mar 2026 10:37:22 +0900 Subject: [PATCH 53/59] :recycle: Replace Keycloak with Ory Kratos + Hydra for authentication MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Phase 1: Keycloak → Ory 移行 - Keycloak 依存を全て削除 (axum-keycloak-auth, KeycloakAuthLayer, KeycloakToken, expect_role!) - JWT 検証ミドルウェア実装 (OIDC Discovery, JWKS キャッシュ, RS256) - OAuth2 Login/Consent Provider エンドポイント (GET/POST /oauth2/login, /oauth2/consent) - Hydra Admin API クライアント + Kratos セッションクライアント - ルートハンドラ書き換え (AuthClaims ベース) - Handler/AppModule に Hydra/Kratos クライアント統合 - compose.yml で開発環境一括起動 (PostgreSQL, Redis, Kratos, Hydra) - E2E 検証完了 (Kratos login → OAuth2 flow → JWT → API access) --- .env.example | 18 +- .gitignore | 5 +- .review/keycloak-to-ory-migration.md | 183 ++ CLAUDE.md | 72 +- Cargo.lock | 469 +--- README.md | 45 +- compose.yml | 127 + .../01-create-databases.sh | 7 + keycloak-data/import/emumet-realm.json | 2046 ----------------- keycloak-data/import/emumet-users-0.json | 27 - ory/hydra/hydra.yml | 31 + ory/kratos/identity.schema.json | 28 + ory/kratos/kratos.yml | 33 + ory/kratos/seed-users.json | 14 + server/Cargo.toml | 16 +- server/src/auth.rs | 645 ++++++ server/src/handler.rs | 20 + server/src/hydra.rs | 212 ++ server/src/keycloak.rs | 105 - server/src/kratos.rs | 70 + server/src/main.rs | 38 +- server/src/route.rs | 28 +- server/src/route/account.rs | 56 +- server/src/route/metadata.rs | 48 +- server/src/route/oauth2.rs | 319 +++ server/src/route/profile.rs | 50 +- 26 files changed, 1905 insertions(+), 2807 deletions(-) create mode 100644 .review/keycloak-to-ory-migration.md create mode 100644 compose.yml create mode 100755 docker-entrypoint-initdb.d/01-create-databases.sh delete mode 100644 keycloak-data/import/emumet-realm.json delete mode 100644 keycloak-data/import/emumet-users-0.json create mode 100644 ory/hydra/hydra.yml create mode 100644 ory/kratos/identity.schema.json create mode 100644 ory/kratos/kratos.yml create mode 100644 ory/kratos/seed-users.json create mode 100644 server/src/auth.rs create mode 100644 server/src/hydra.rs delete mode 100644 server/src/keycloak.rs create mode 100644 server/src/kratos.rs create mode 100644 server/src/route/oauth2.rs diff --git a/.env.example b/.env.example index ba47af7..d78956f 100644 --- a/.env.example +++ b/.env.example @@ -1,10 +1,16 @@ -DATABASE_URL=postgres://user:password@localhost:5432/dbname +DATABASE_URL=postgres://postgres:develop@localhost:5432/postgres # or DATABASE_HOST=localhost DATABASE_PORT=5432 -DATABASE_USER=user -DATABASE_PASSWORD=password -DATABASE_NAME=dbname +DATABASE_USER=postgres +DATABASE_PASSWORD=develop +DATABASE_NAME=postgres -KEYCLOAK_SERVER=http://localhost:18080/ -KEYCLOAK_REALM=emumet \ No newline at end of file +HYDRA_ISSUER_URL=http://localhost:4444/ +HYDRA_ADMIN_URL=http://localhost:4445/ +KRATOS_PUBLIC_URL=http://localhost:4433/ +EXPECTED_AUDIENCE=account + +REDIS_URL=redis://localhost:6379 +# or +REDIS_HOST=localhost \ No newline at end of file diff --git a/.gitignore b/.gitignore index eb70f34..1a5d6c7 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,5 @@ .direnv /migrations/dbml-error.log .env -keycloak-data/* -!keycloak-data/import -logs \ No newline at end of file +logs +master-key-password diff --git a/.review/keycloak-to-ory-migration.md b/.review/keycloak-to-ory-migration.md new file mode 100644 index 0000000..c1a4704 --- /dev/null +++ b/.review/keycloak-to-ory-migration.md @@ -0,0 +1,183 @@ +--- +feature: keycloak-to-ory-migration +started: 2026-03-11 +phase: implementing +--- + +# Keycloak to Ory Migration (Phase 1: Kratos + Hydra) + +## Requirements + +### Purpose + +Replace Keycloak with Ory Kratos (identity management) + Ory Hydra (OAuth2/OIDC) to reduce infrastructure weight. Enable self-service registration. Login/Consent UI is ShuttlePub frontend's responsibility. + +### Scope - Do + +- Remove all Keycloak dependencies from server layer (`axum-keycloak-auth`, `KeycloakAuthLayer`, `KeycloakToken`, `expect_role!` macro) +- Implement JWT validation middleware for Hydra-issued tokens + - Validate: `iss`, `aud`, `exp`, `sub` + - JWKS: auto-resolve via OIDC Discovery, in-memory cache, re-fetch on kid miss (rate-limited), lazy init on startup failure + - Audience: configurable via `EXPECTED_AUDIENCE` env var +- Implement Login/Consent Provider backend in server layer (Hydra login/consent accept/reject API) +- Value mapping: `AuthHost.url` = Hydra issuer URL, `AuthAccount.client_id` = Kratos identity UUID (JWT `sub`) +- Simplify authorization to auth-check only (JWT validity). Role-based authz deferred to Phase 2 (Ketos) +- Dev environment: Keycloak -> Kratos + Hydra containers, self-service registration enabled +- Kratos identity schema: minimal (email + password) +- Seed data: test user (email: testuser@example.com / password: testuser) via Kratos import +- Existing AuthHost/AuthAccount data: discard (dev only, no migration) +- Env vars: `HYDRA_ISSUER_URL`, `HYDRA_ADMIN_URL`, `EXPECTED_AUDIENCE` +- Update server-layer auth tests. JWT unit tests use test RSA keypair + mock JWKS. Integration tests use `test_with::env` pattern + +### Scope - Don't + +- Ory Ketos introduction (Phase 2) +- Authorization checker trait in kernel (Phase 2) +- Multi-AuthAccount permission separation (Phase 2+) +- Entity structure changes (AuthAccount, AuthHost, etc.) +- kernel / adapter / application / driver layer changes +- Login UI implementation (ShuttlePub frontend responsibility) + +## Design + +### A. JWT Validation Middleware + +Replace `axum-keycloak-auth` with `jsonwebtoken` + `reqwest`. + +``` +server/src/auth.rs (new, replaces keycloak.rs) +├── OidcConfig — issuer URL, expected audience, jwks_refetch_interval_secs (configurable for tests) +├── JwksCache — in-memory (Arc>), kid miss → re-fetch (rate-limited), lazy init +├── AuthClaims — standard OIDC claims (iss, sub, aud, exp) +├── OidcAuthInfo — from AuthClaims: issuer → AuthHost.url, subject → AuthAccount.client_id +├── auth_middleware() — axum middleware: Bearer token → validate → Extension +└── resolve_auth_account_id() — rewritten with OidcAuthInfo (same find-or-create logic) +``` + +Value mapping: JWT `iss` → AuthHost.url, JWT `sub` (= Kratos identity UUID) → AuthAccount.client_id. +Hydra login accept sets `subject = Kratos identity.id`, so JWT `sub` = Kratos identity UUID. + +### B. Login/Consent Provider + +``` +server/src/route/oauth2.rs (new) — NOT under auth_middleware +├── GET /oauth2/login — login_challenge → Kratos session check → Hydra login accept/redirect +├── GET /oauth2/consent — consent_challenge → skip check → unified JSON response +│ skip: { action: "redirect", redirect_to: "..." } +│ non-skip: { action: "show_consent", consent_challenge, client_name, requested_scope } +├── POST /oauth2/consent — consent result → Hydra consent accept/reject +``` + +### C. Hydra Admin API Client + +``` +server/src/hydra.rs (new, reqwest-based) +├── HydraAdminClient +│ ├── get_login_request / accept_login / reject_login +│ └── get_consent_request / accept_consent / reject_consent +``` + +### D. Kratos Client + +``` +server/src/kratos.rs (new) +├── KratosClient +│ └── whoami(cookie) -> Option +``` + +Domain premise: Kratos and Emumet on same domain (subdomain) for cookie reachability. + +### E. Route Handler Changes + +account.rs, profile.rs, metadata.rs: KeycloakToken → AuthClaims, remove KeycloakAuthLayer, remove expect_role!. +route.rs: remove `to_permission_strings` and its test, add oauth2 module. + +Router structure in main.rs: +``` +Router::new() + .route_account/profile/metadata(...) + .layer(auth_middleware(...)) // JWT required + .route_oauth2(...) // NO auth_middleware + .layer(CorsLayer) + .with_state(app) +``` + +### F. Handler / AppModule + +Handler gets: HydraAdminClient, KratosClient, JwksCache, OidcConfig as fields. +Access via `#[derive(References)]` auto-generated getters. No DependOn* traits needed (server-layer only). + +### G. Dev Environment + +podman-compose (docker-compose.yml). Helm charts for production separately later. + +``` +ory/ +├── kratos/ +│ ├── kratos.yml, identity.schema.json, seed-users.json +└── hydra/ + └── hydra.yml +docker-compose.yml — postgres, redis, kratos, hydra +``` + +Env vars: HYDRA_ISSUER_URL, HYDRA_ADMIN_URL, KRATOS_PUBLIC_URL, EXPECTED_AUDIENCE + +### H. Data Cleanup + +Dev switch: TRUNCATE auth_hosts, auth_accounts, auth_account_events. No schema migration needed. + +### I. Files NOT Changed/Deleted + +- permission.rs: kept (potential Phase 2 reference) +- kernel / adapter / application / driver: no changes + +### J. Testing + +- JWT validation: test RSA keypair + in-memory JwkSet injection (configurable refetch interval = 0) +- Login/Consent: test_with::env(HYDRA_ADMIN_URL) skip +- Route handlers: Extension directly set, bypass middleware + +## Tasks + +- [x] 1. Dev environment (Ory Kratos + Hydra) + - [x] 1.1 Kratos config files (kratos.yml, identity.schema.json, seed-users.json) (P) + - [x] 1.2 Hydra config file (hydra.yml) (P) + - [x] 1.3 docker-compose.yml (postgres, redis, kratos, hydra; replaces standalone podman run; shared postgres with DB name separation) + - [x] 1.4 .env.example update (add HYDRA_ISSUER_URL, HYDRA_ADMIN_URL, KRATOS_PUBLIC_URL, EXPECTED_AUDIENCE; remove KEYCLOAK_SERVER, KEYCLOAK_REALM) + - [x] 1.5 Startup verification (podman-compose up, health endpoints respond) +- [x] 2. Cargo.toml deps and type definitions + - [x] 2.1 Add jsonwebtoken / reqwest to Cargo.toml (keep axum-keycloak-auth for now) + - [x] 2.2 OidcConfig / AuthClaims / OidcAuthInfo type definitions (server/src/auth.rs new) +- [x] 3. JWT validation and external clients + - [x] 3.1 JwksCache (OIDC Discovery, in-memory cache, kid miss re-fetch, lazy init) (P) + - [x] 3.2 HydraAdminClient types and methods (server/src/hydra.rs) (P) + - [x] 3.3 KratosClient and whoami (server/src/kratos.rs) (P) + - [x] 3.4 auth_middleware (Bearer extraction, JWT validation, Extension) + - [x] 3.5 JWT validation unit tests (test RSA keypair + JwkSet injection) +- [x] R1. Code review: auth foundation (auth.rs, hydra.rs, kratos.rs) +- [x] 4. Login/Consent Provider endpoints + - [x] 4.1 OAuth2Router trait and oauth2 module in route.rs + - [x] 4.2 GET /oauth2/login endpoint + - [x] 4.3 GET /oauth2/consent endpoint (skip check, unified JSON response) + - [x] 4.4 POST /oauth2/consent endpoint (consent result -> Hydra accept/reject) +- [x] R2. Code review: OAuth2 flow (route/oauth2.rs) +- [x] 5. Keycloak removal and route handler rewrite + - [x] 5.1 Rewrite resolve_auth_account_id with OidcAuthInfo (in auth.rs) + - [x] 5.2 account.rs rewrite (KeycloakToken -> AuthClaims, remove KeycloakAuthLayer/expect_role!) (P) + - [x] 5.3 profile.rs rewrite (same) (P) + - [x] 5.4 metadata.rs rewrite (same) (P) + - [x] 5.5 Remove to_permission_strings + test from route.rs, remove expect_role! macro from keycloak.rs + - [x] 5.6 Route handler unit test updates (Extension direct injection) +- [x] 6. Handler / AppModule / main.rs integration + - [x] 6.1 Handler: add HydraAdminClient / KratosClient / JwksCache / OidcConfig fields + AppModule accessors + init + - [x] 6.2 main.rs rewrite (remove KeycloakAuthInstance, add auth_middleware + OAuth2 routes, middleware scoping) + - [x] 6.3 Remove axum-keycloak-auth from Cargo.toml +- [x] R3. Code review: integration (handler.rs, main.rs, route handlers consistency) +- [x] 7. Cleanup and documentation + - [x] 7.1 Delete keycloak.rs, keycloak-data/, remove keycloak-data lines from .gitignore (P) + - [x] 7.2 Update CLAUDE.md / README.md (podman-compose setup instructions) (P) + - [x] 7.3 Document data cleanup procedure (TRUNCATE auth_hosts / auth_accounts / auth_account_events) +- [x] 8. Integration tests and verification + - [ ] 8.1 Login/Consent flow integration test (test_with::env(HYDRA_ADMIN_URL) skip) + - [x] 8.2 E2E verification with podman-compose (register -> login -> JWT -> API access) +- [x] R4. Final review: full alignment check (requirements/design coverage, missing items, code quality) diff --git a/CLAUDE.md b/CLAUDE.md index 2d18853..6b3f470 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -24,29 +24,52 @@ cargo run -p server ## Required Services -### PostgreSQL +Use `podman-compose up` (or `docker-compose up`) to start all required services: + +```bash +podman-compose up -d +``` + +This starts: PostgreSQL, Redis, Ory Kratos, and Ory Hydra. + +### Manual startup (alternative) + +#### PostgreSQL ```bash podman run --rm --name emumet-postgres -e POSTGRES_PASSWORD=develop -p 5432:5432 docker.io/postgres ``` - User: postgres / Password: develop -### Redis +#### Redis Required for message queue (rikka-mq). -### Keycloak -```bash -mkdir -p keycloak-data/h2 -podman run --rm -it -v ./keycloak-data/h2:/opt/keycloak/data/h2:Z,U -v ./keycloak-data/import:/opt/keycloak/data/import:Z,U -p 18080:8080 -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin --name emumet-keycloak quay.io/keycloak/keycloak:26.1 start-dev --import-realm -``` -- URL: http://localhost:18080 -- Admin: admin / admin -- Realm: emumet, Client: myclient, User: testuser / testuser +### Auth: Ory Kratos + Hydra + +- **Kratos** (identity management): http://localhost:4433 (public), http://localhost:4434 (admin) + - Self-service registration enabled + - Identity schema: email + password + - Test user: testuser@example.com / testuser +- **Hydra** (OAuth2/OIDC): http://localhost:4444 (public), http://localhost:4445 (admin) + - Login/Consent Provider: Emumet server (GET /oauth2/login, GET/POST /oauth2/consent) + - JWT issuer + +Config files: `ory/kratos/`, `ory/hydra/` ## Environment Variables Copy `.env.example` to `.env`: - `DATABASE_URL` or individual `DATABASE_HOST`, `DATABASE_PORT`, `DATABASE_USER`, `DATABASE_PASSWORD`, `DATABASE_NAME` -- `KEYCLOAK_SERVER`, `KEYCLOAK_REALM` +- `HYDRA_ISSUER_URL` — Hydra public URL for JWT validation (default: http://localhost:4444/) +- `HYDRA_ADMIN_URL` — Hydra admin URL for Login/Consent API (default: http://localhost:4445/) +- `KRATOS_PUBLIC_URL` — Kratos public URL for session verification (default: http://localhost:4433/) +- `EXPECTED_AUDIENCE` — Expected JWT audience claim (default: account) +- `REDIS_URL` or `REDIS_HOST` — Redis connection for message queue + +### Master Key Password + +Account creation requires a master key password file for signing key encryption: +- Production: `/run/secrets/master-key-password` +- Development: `./master-key-password` (create manually, `chmod 600`) ## Architecture @@ -61,7 +84,7 @@ kernel → driver → server - **adapter**: CQRS processors (CommandProcessor/QueryProcessor) that compose kernel traits, crypto trait composition (SigningKeyGenerator) - **application**: Use case services (Account CRUD use cases), event appliers (projection update), DTOs - **driver**: PostgreSQL/Redis implementations of kernel interfaces -- **server**: Axum HTTP server, Keycloak auth, route handlers, DI wiring (Handler/AppModule) +- **server**: Axum HTTP server, JWT auth (Ory Hydra), OAuth2 Login/Consent Provider, route handlers, DI wiring (Handler/AppModule) ### CQRS + Event Sourcing Pattern @@ -129,6 +152,20 @@ These use the Repository pattern — a single trait combining read and write ope **Signal → Applier pipeline**: `Signal` trait emits entity IDs via Redis (rikka-mq). `ApplierContainer` (server/src/applier.rs) receives and dispatches to entity-specific appliers that update ReadModel projections. +### Auth Architecture + +JWT validation middleware (`server/src/auth.rs`): +- OIDC Discovery → JWKS cache (with kid-miss re-fetch, rate-limited) +- Bearer token → RS256 validation → `Extension` inserted into request +- `AuthClaims` → `OidcAuthInfo` → `resolve_auth_account_id` (find-or-create AuthHost + AuthAccount) + +OAuth2 Login/Consent Provider (`server/src/route/oauth2.rs`): +- GET /oauth2/login — Kratos session → Hydra login accept +- GET /oauth2/consent — skip check → redirect or show consent +- POST /oauth2/consent — accept/reject with scope validation + +Value mapping: JWT `iss` → `AuthHost.url`, JWT `sub` (Kratos identity UUID) → `AuthAccount.client_id` + ### Entity Structure Entities use vodca macros (`References`, `Newln`, `Nameln`) and `destructure::Destructure` for field access. @@ -145,10 +182,17 @@ Event Sourcing対象エンティティ (Account, AuthAccount, Profile, Metadata) ### Server DI Architecture -`Handler` — owns PostgresDatabase + RedisDatabase + crypto providers. `impl_database_delegation!` wires kernel traits. +`Handler` — owns PostgresDatabase + RedisDatabase + crypto providers + HydraAdminClient + KratosClient. `impl_database_delegation!` wires kernel traits. -`AppModule` — wraps `Arc` + `Arc`. Manually implements `DependOn*` for adapter-layer traits (Signal, ReadModel, EventStore, Repository). Blanket impls provide CommandProcessor/QueryProcessor automatically. +`AppModule` — wraps `Arc` + `Arc`. Manually implements `DependOn*` for adapter-layer traits (Signal, ReadModel, EventStore, Repository). Blanket impls provide CommandProcessor/QueryProcessor automatically. Provides `hydra_admin_client()` and `kratos_client()` accessors. ### Testing Database tests use `#[test_with::env(DATABASE_URL)]` attribute to skip when database is unavailable. + +### Data Cleanup (after auth migration) + +If migrating from Keycloak to Ory, truncate auth-related tables: +```sql +TRUNCATE auth_hosts, auth_accounts, auth_account_events; +``` diff --git a/Cargo.lock b/Cargo.lock index 98a00d5..7fb82a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -184,16 +184,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "atomic-time" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9622f5c6fb50377516c70f65159e70b25465409760c6bd6d4e581318bf704e83" -dependencies = [ - "once_cell", - "portable-atomic", -] - [[package]] name = "atomic-waker" version = "1.1.2" @@ -234,7 +224,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", - "tower", + "tower 0.5.2", "tower-layer", "tower-service", "tracing", @@ -261,59 +251,6 @@ dependencies = [ "tracing", ] -[[package]] -name = "axum-extra" -version = "0.9.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "73c3220b188aea709cf1b6c5f9b01c3bd936bb08bd2b5184a12b35ac8131b1f9" -dependencies = [ - "axum", - "axum-core", - "bytes", - "futures-util", - "headers", - "http", - "http-body", - "http-body-util", - "mime", - "pin-project-lite", - "serde", - "serde_html_form", - "tower", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "axum-keycloak-auth" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be9f06871201a9be2a42da89f98ad7b588681d7ac6c6b18193e010732f90b8d3" -dependencies = [ - "atomic-time", - "axum", - "educe", - "futures", - "http", - "jsonwebtoken", - "nonempty", - "reqwest", - "serde", - "serde-querystring", - "serde_json", - "serde_with", - "snafu", - "time", - "tokio", - "tower", - "tracing", - "try-again", - "typed-builder", - "url", - "uuid", -] - [[package]] name = "backtrace" version = "0.3.73" @@ -494,7 +431,6 @@ dependencies = [ "iana-time-zone", "js-sys", "num-traits", - "serde", "wasm-bindgen", "windows-targets 0.52.6", ] @@ -632,41 +568,6 @@ dependencies = [ "cipher", ] -[[package]] -name = "darling" -version = "0.20.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6f63b86c8a8826a49b8c21f08a2d07338eec8d900540f8630dc76284be802989" -dependencies = [ - "darling_core", - "darling_macro", -] - -[[package]] -name = "darling_core" -version = "0.20.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95133861a8032aaea082871032f5815eb9e98cef03fa916ab4500513994df9e5" -dependencies = [ - "fnv", - "ident_case", - "proc-macro2", - "quote", - "strsim", - "syn 2.0.96", -] - -[[package]] -name = "darling_macro" -version = "0.20.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d336a2a514f6ccccaa3e09b02d41d35330c07ddf03a62165fcec10bb561c7806" -dependencies = [ - "darling_core", - "quote", - "syn 2.0.96", -] - [[package]] name = "deadpool" version = "0.12.1" @@ -783,18 +684,6 @@ dependencies = [ "zeroize", ] -[[package]] -name = "educe" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" -dependencies = [ - "enum-ordinalize", - "proc-macro2", - "quote", - "syn 2.0.96", -] - [[package]] name = "either" version = "1.13.0" @@ -813,26 +702,6 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "enum-ordinalize" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fea0dcfa4e54eeb516fe454635a95753ddd39acda650ce703031c6973e315dd5" -dependencies = [ - "enum-ordinalize-derive", -] - -[[package]] -name = "enum-ordinalize-derive" -version = "4.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.96", -] - [[package]] name = "env_home" version = "0.1.0" @@ -935,21 +804,6 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-executor", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - [[package]] name = "futures-channel" version = "0.3.31" @@ -994,17 +848,6 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.96", -] - [[package]] name = "futures-sink" version = "0.3.31" @@ -1023,10 +866,8 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ - "futures-channel", "futures-core", "futures-io", - "futures-macro", "futures-sink", "futures-task", "memchr", @@ -1086,7 +927,7 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap 2.3.0", + "indexmap", "slab", "tokio", "tokio-util", @@ -1121,30 +962,6 @@ dependencies = [ "hashbrown 0.14.5", ] -[[package]] -name = "headers" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "322106e6bd0cba2d5ead589ddb8150a13d7c4217cf80d7c4f682ca994ccc6aa9" -dependencies = [ - "base64 0.21.7", - "bytes", - "headers-core", - "http", - "httpdate", - "mime", - "sha1", -] - -[[package]] -name = "headers-core" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54b4a22553d4242c49fddb9ba998a99962b5cc6f22cb5a3482bec22522403ce4" -dependencies = [ - "http", -] - [[package]] name = "heck" version = "0.4.1" @@ -1453,12 +1270,6 @@ dependencies = [ "syn 2.0.96", ] -[[package]] -name = "ident_case" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b9e0384b61958566e926dc50660321d12159025e767c18e043daf26b70104c39" - [[package]] name = "idna" version = "1.0.3" @@ -1480,17 +1291,6 @@ dependencies = [ "icu_properties", ] -[[package]] -name = "indexmap" -version = "1.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" -dependencies = [ - "autocfg", - "hashbrown 0.12.3", - "serde", -] - [[package]] name = "indexmap" version = "2.3.0" @@ -1499,7 +1299,6 @@ checksum = "de3fc2e30ba82dd1b3911c8de1ffc143c74a914a14e99514d7637e3099df5ea0" dependencies = [ "equivalent", "hashbrown 0.14.5", - "serde", ] [[package]] @@ -1573,79 +1372,6 @@ dependencies = [ "spin", ] -[[package]] -name = "lexical" -version = "6.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7aefb36fd43fef7003334742cbf77b243fcd36418a1d1bdd480d613a67968f6" -dependencies = [ - "lexical-core", -] - -[[package]] -name = "lexical-core" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2cde5de06e8d4c2faabc400238f9ae1c74d5412d03a7bd067645ccbc47070e46" -dependencies = [ - "lexical-parse-float", - "lexical-parse-integer", - "lexical-util", - "lexical-write-float", - "lexical-write-integer", -] - -[[package]] -name = "lexical-parse-float" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "683b3a5ebd0130b8fb52ba0bdc718cc56815b6a097e28ae5a6997d0ad17dc05f" -dependencies = [ - "lexical-parse-integer", - "lexical-util", - "static_assertions", -] - -[[package]] -name = "lexical-parse-integer" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d0994485ed0c312f6d965766754ea177d07f9c00c9b82a5ee62ed5b47945ee9" -dependencies = [ - "lexical-util", - "static_assertions", -] - -[[package]] -name = "lexical-util" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5255b9ff16ff898710eb9eb63cb39248ea8a5bb036bea8085b1a767ff6c4e3fc" -dependencies = [ - "static_assertions", -] - -[[package]] -name = "lexical-write-float" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "accabaa1c4581f05a3923d1b4cfd124c329352288b7b9da09e766b0668116862" -dependencies = [ - "lexical-util", - "lexical-write-integer", - "static_assertions", -] - -[[package]] -name = "lexical-write-integer" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1b6f3d1f4422866b68192d62f77bc5c700bee84f3069f2469d7bc8c77852446" -dependencies = [ - "lexical-util", - "static_assertions", -] - [[package]] name = "libc" version = "0.2.169" @@ -1797,12 +1523,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "nonempty" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "303e8749c804ccd6ca3b428de7fe0d86cb86bc7606bc15291f100fd487960bb8" - [[package]] name = "ntapi" version = "0.4.1" @@ -2031,6 +1751,26 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pin-project" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "pin-project-lite" version = "0.2.14" @@ -2093,12 +1833,6 @@ dependencies = [ "universal-hash", ] -[[package]] -name = "portable-atomic" -version = "1.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "280dc24453071f1b63954171985a0b0d30058d287960968b9b2aca264c8d4ee6" - [[package]] name = "powerfmt" version = "0.2.0" @@ -2367,7 +2101,7 @@ dependencies = [ "system-configuration", "tokio", "tokio-native-tls", - "tower", + "tower 0.5.2", "tower-service", "url", "wasm-bindgen", @@ -2610,16 +2344,6 @@ dependencies = [ "serde_derive", ] -[[package]] -name = "serde-querystring" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce88f8e75d0920545b35b78775e0927137337401f65b01326ce299734885fd21" -dependencies = [ - "lexical", - "serde", -] - [[package]] name = "serde_derive" version = "1.0.217" @@ -2631,19 +2355,6 @@ dependencies = [ "syn 2.0.96", ] -[[package]] -name = "serde_html_form" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8de514ef58196f1fc96dcaef80fe6170a1ce6215df9687a93fe8300e773fefc5" -dependencies = [ - "form_urlencoded", - "indexmap 2.3.0", - "itoa", - "ryu", - "serde", -] - [[package]] name = "serde_json" version = "1.0.137" @@ -2678,36 +2389,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_with" -version = "3.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6b6f7f2fcb69f747921f79f3926bd1e203fce4fef62c268dd3abfb6d86029aa" -dependencies = [ - "base64 0.22.1", - "chrono", - "hex", - "indexmap 1.9.3", - "indexmap 2.3.0", - "serde", - "serde_derive", - "serde_json", - "serde_with_macros", - "time", -] - -[[package]] -name = "serde_with_macros" -version = "3.12.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8d00caa5193a3c8362ac2b73be6b9e768aa5a4b2f721d8f4b339600c3cb51f8e" -dependencies = [ - "darling", - "proc-macro2", - "quote", - "syn 2.0.96", -] - [[package]] name = "server" version = "0.1.0" @@ -2715,21 +2396,22 @@ dependencies = [ "adapter", "application", "axum", - "axum-extra", - "axum-keycloak-auth", + "base64 0.22.1", "destructure", "dotenvy", "driver", "error-stack", + "jsonwebtoken", "kernel", - "log", "rand", - "rand_chacha", + "reqwest", "rikka-mq", + "rsa", "serde", - "sha2", + "serde_json", "time", "tokio", + "tower 0.4.13", "tower-http", "tracing", "tracing-appender", @@ -2770,15 +2452,6 @@ dependencies = [ "lazy_static", ] -[[package]] -name = "signal-hook-registry" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" -dependencies = [ - "libc", -] - [[package]] name = "signature" version = "2.2.0" @@ -2822,27 +2495,6 @@ version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" -[[package]] -name = "snafu" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "223891c85e2a29c3fe8fb900c1fae5e69c2e42415e3177752e8718475efa5019" -dependencies = [ - "snafu-derive", -] - -[[package]] -name = "snafu-derive" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03c3c6b7927ffe7ecaa769ee0e3994da3b8cafc8f444578982c83ecb161af917" -dependencies = [ - "heck", - "proc-macro2", - "quote", - "syn 2.0.96", -] - [[package]] name = "socket2" version = "0.4.10" @@ -2926,7 +2578,7 @@ dependencies = [ "futures-util", "hashlink", "hex", - "indexmap 2.3.0", + "indexmap", "log", "memchr", "native-tls", @@ -3101,12 +2753,6 @@ version = "1.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - [[package]] name = "stringprep" version = "0.1.5" @@ -3118,12 +2764,6 @@ dependencies = [ "unicode-properties", ] -[[package]] -name = "strsim" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7da8b5736845d9f2fcb837ea5d9e2628564b3b043a70948a3f0b778838c5fb4f" - [[package]] name = "subtle" version = "2.6.1" @@ -3363,9 +3003,7 @@ dependencies = [ "bytes", "libc", "mio", - "parking_lot", "pin-project-lite", - "signal-hook-registry", "socket2 0.5.7", "tokio-macros", "windows-sys 0.52.0", @@ -3438,11 +3076,26 @@ version = "0.22.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" dependencies = [ - "indexmap 2.3.0", + "indexmap", "toml_datetime", "winnow", ] +[[package]] +name = "tower" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" +dependencies = [ + "futures-core", + "futures-util", + "pin-project", + "pin-project-lite", + "tower-layer", + "tower-service", + "tracing", +] + [[package]] name = "tower" version = "0.5.2" @@ -3562,42 +3215,12 @@ dependencies = [ "tracing-log", ] -[[package]] -name = "try-again" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1006cfd66eadaaa9ec7a3d011336e40b98cd6c4d8f7b1e331080f6145ac00671" -dependencies = [ - "tokio", - "tracing", -] - [[package]] name = "try-lock" version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" -[[package]] -name = "typed-builder" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7e14ed59dc8b7b26cacb2a92bad2e8b1f098806063898ab42a3bd121d7d45e75" -dependencies = [ - "typed-builder-macro", -] - -[[package]] -name = "typed-builder-macro" -version = "0.20.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "560b82d656506509d43abe30e0ba64c56b1953ab3d4fe7ba5902747a7a3cedd5" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.96", -] - [[package]] name = "typenum" version = "1.17.0" diff --git a/README.md b/README.md index 8cb851e..8300e9e 100644 --- a/README.md +++ b/README.md @@ -1,46 +1,41 @@ # Emumet - - + + -# Keycloak +## Setup + +### Services ```shell -mkdir -p keycloak-data/h2 -podman run --rm -it -v ./keycloak-data/h2:/opt/keycloak/data/h2:Z,U -v ./keycloak-data/import:/opt/keycloak/data/import:Z,U -p 18080:8080 -e KC_BOOTSTRAP_ADMIN_USERNAME=admin -e KC_BOOTSTRAP_ADMIN_PASSWORD=admin --name emumet-keycloak quay.io/keycloak/keycloak:26.1 start-dev --import-realm +podman-compose up -d ``` -> - Url: http://localhost:18080 -> - ユーザー名: admin -> - パスワード: admin -> - Realm: emumet -> - Client: myclient -> - User: testuser -> - UserPass: testuser -> - RealmLoginTest: https://www.keycloak.org/app/ +PostgreSQL, Redis, Ory Kratos, Ory Hydra が起動します。 + +### Auth: Ory Kratos + Hydra -### Update realm data +- **Kratos** (Identity Management): http://localhost:4433 + - Test user: testuser@example.com / testuser +- **Hydra** (OAuth2/OIDC): http://localhost:4444 + +### Environment ```shell -mkdir -p keycloak-data/export -podman run --rm -v ./keycloak-data/h2:/opt/keycloak/data/h2:Z,U -v ./keycloak-data/export:/opt/keycloak/data/export:Z,U quay.io/keycloak/keycloak:latest export --dir /opt/keycloak/data/export --users same_file --realm emumet -sudo cp keycloak-data/export/* keycloak-data/import/ +cp .env.example .env ``` -> keycloak本体を停止してから実行してください - -# DB +## DB -Podman(docker)にて環境構築が可能です +`podman-compose` で PostgreSQL が起動します。手動起動する場合: ```shell podman run --rm --name emumet-postgres -e POSTGRES_PASSWORD=develop -p 5432:5432 docker.io/postgres ``` -> ユーザー名: postgres -> パスワード: develop +> User: postgres / Password: develop -# 語源 +## Etymology -EMU(Extravehicular Mobility Unit=宇宙服)+Helmet +EMU(Extravehicular Mobility Unit) + Helmet diff --git a/compose.yml b/compose.yml new file mode 100644 index 0000000..38320cb --- /dev/null +++ b/compose.yml @@ -0,0 +1,127 @@ +services: + postgres: + image: docker.io/postgres:16 + container_name: emumet-postgres + environment: + POSTGRES_PASSWORD: develop + POSTGRES_USER: postgres + ports: + - "5432:5432" + volumes: + - type: volume + source: postgres-data + target: /var/lib/postgresql/data + - type: bind + source: ./docker-entrypoint-initdb.d + target: /docker-entrypoint-initdb.d + bind: + selinux: z + healthcheck: + test: ["CMD-SHELL", "pg_isready -U postgres"] + interval: 5s + timeout: 5s + retries: 5 + + redis: + image: docker.io/redis:7 + container_name: emumet-redis + ports: + - "6379:6379" + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 5s + timeout: 5s + retries: 5 + + kratos-migrate: + image: docker.io/oryd/kratos:v1.3.1 + container_name: emumet-kratos-migrate + environment: + DSN: postgres://postgres:develop@postgres:5432/kratos?sslmode=disable + volumes: + - type: bind + source: ./ory/kratos + target: /etc/config/kratos + bind: + selinux: z + command: migrate sql -e --yes + depends_on: + postgres: + condition: service_healthy + + kratos: + image: docker.io/oryd/kratos:v1.3.1 + container_name: emumet-kratos + environment: + DSN: postgres://postgres:develop@postgres:5432/kratos?sslmode=disable + volumes: + - type: bind + source: ./ory/kratos + target: /etc/config/kratos + bind: + selinux: z + command: serve -c /etc/config/kratos/kratos.yml --dev --watch-courier + ports: + - "4433:4433" + - "4434:4434" + depends_on: + kratos-migrate: + condition: service_completed_successfully + healthcheck: + test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:4433/health/alive || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + + kratos-import: + image: docker.io/oryd/kratos:v1.3.1 + container_name: emumet-kratos-import + environment: + DSN: postgres://postgres:develop@postgres:5432/kratos?sslmode=disable + volumes: + - type: bind + source: ./ory/kratos + target: /etc/config/kratos + bind: + selinux: z + command: import identities /etc/config/kratos/seed-users.json -e http://kratos:4434 + depends_on: + kratos: + condition: service_healthy + + hydra-migrate: + image: docker.io/oryd/hydra:v2.3.0 + container_name: emumet-hydra-migrate + environment: + DSN: postgres://postgres:develop@postgres:5432/hydra?sslmode=disable + command: migrate sql -e --yes + depends_on: + postgres: + condition: service_healthy + + hydra: + image: docker.io/oryd/hydra:v2.3.0 + container_name: emumet-hydra + environment: + DSN: postgres://postgres:develop@postgres:5432/hydra?sslmode=disable + volumes: + - type: bind + source: ./ory/hydra + target: /etc/config/hydra + bind: + selinux: z + command: serve all -c /etc/config/hydra/hydra.yml --dev + ports: + - "4444:4444" + - "4445:4445" + depends_on: + hydra-migrate: + condition: service_completed_successfully + healthcheck: + test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:4444/health/alive || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + +volumes: + postgres-data: diff --git a/docker-entrypoint-initdb.d/01-create-databases.sh b/docker-entrypoint-initdb.d/01-create-databases.sh new file mode 100755 index 0000000..0f01155 --- /dev/null +++ b/docker-entrypoint-initdb.d/01-create-databases.sh @@ -0,0 +1,7 @@ +#!/bin/bash +set -e + +psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL + CREATE DATABASE kratos; + CREATE DATABASE hydra; +EOSQL diff --git a/keycloak-data/import/emumet-realm.json b/keycloak-data/import/emumet-realm.json deleted file mode 100644 index ac5e401..0000000 --- a/keycloak-data/import/emumet-realm.json +++ /dev/null @@ -1,2046 +0,0 @@ -{ - "id" : "20fdc959-26f7-4a13-92ea-5fcd3380d354", - "realm" : "emumet", - "notBefore" : 0, - "defaultSignatureAlgorithm" : "RS256", - "revokeRefreshToken" : false, - "refreshTokenMaxReuse" : 0, - "accessTokenLifespan" : 300, - "accessTokenLifespanForImplicitFlow" : 900, - "ssoSessionIdleTimeout" : 1800, - "ssoSessionMaxLifespan" : 36000, - "ssoSessionIdleTimeoutRememberMe" : 0, - "ssoSessionMaxLifespanRememberMe" : 0, - "offlineSessionIdleTimeout" : 2592000, - "offlineSessionMaxLifespanEnabled" : false, - "offlineSessionMaxLifespan" : 5184000, - "clientSessionIdleTimeout" : 0, - "clientSessionMaxLifespan" : 0, - "clientOfflineSessionIdleTimeout" : 0, - "clientOfflineSessionMaxLifespan" : 0, - "accessCodeLifespan" : 60, - "accessCodeLifespanUserAction" : 300, - "accessCodeLifespanLogin" : 1800, - "actionTokenGeneratedByAdminLifespan" : 43200, - "actionTokenGeneratedByUserLifespan" : 300, - "oauth2DeviceCodeLifespan" : 600, - "oauth2DevicePollingInterval" : 5, - "enabled" : true, - "sslRequired" : "external", - "registrationAllowed" : false, - "registrationEmailAsUsername" : false, - "rememberMe" : false, - "verifyEmail" : false, - "loginWithEmailAllowed" : true, - "duplicateEmailsAllowed" : false, - "resetPasswordAllowed" : false, - "editUsernameAllowed" : false, - "bruteForceProtected" : false, - "permanentLockout" : false, - "maxTemporaryLockouts" : 0, - "bruteForceStrategy" : "MULTIPLE", - "maxFailureWaitSeconds" : 900, - "minimumQuickLoginWaitSeconds" : 60, - "waitIncrementSeconds" : 60, - "quickLoginCheckMilliSeconds" : 1000, - "maxDeltaTimeSeconds" : 43200, - "failureFactor" : 30, - "roles" : { - "realm" : [ { - "id" : "8d81d43e-775d-4db2-b93b-87d4a4b002a8", - "name" : "uma_authorization", - "description" : "${role_uma_authorization}", - "composite" : false, - "clientRole" : false, - "containerId" : "20fdc959-26f7-4a13-92ea-5fcd3380d354", - "attributes" : { } - }, { - "id" : "855612d9-abeb-4121-946b-dfb505977829", - "name" : "offline_access", - "description" : "${role_offline-access}", - "composite" : false, - "clientRole" : false, - "containerId" : "20fdc959-26f7-4a13-92ea-5fcd3380d354", - "attributes" : { } - }, { - "id" : "71555fc5-bcff-47ec-a2ba-9bdb33a813d5", - "name" : "default-roles-emumet", - "description" : "${role_default-roles}", - "composite" : true, - "composites" : { - "realm" : [ "offline_access", "uma_authorization" ], - "client" : { - "account" : [ "manage-account", "view-profile" ] - } - }, - "clientRole" : false, - "containerId" : "20fdc959-26f7-4a13-92ea-5fcd3380d354", - "attributes" : { } - } ], - "client" : { - "myclient" : [ ], - "realm-management" : [ { - "id" : "8166caae-b0a9-4c11-8bff-6fcf4a0e8b7e", - "name" : "manage-authorization", - "description" : "${role_manage-authorization}", - "composite" : false, - "clientRole" : true, - "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", - "attributes" : { } - }, { - "id" : "79ed0831-5266-41ae-86a0-16b70079d649", - "name" : "query-groups", - "description" : "${role_query-groups}", - "composite" : false, - "clientRole" : true, - "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", - "attributes" : { } - }, { - "id" : "5ae5edd9-73e8-4ea4-9837-043b05d59766", - "name" : "view-authorization", - "description" : "${role_view-authorization}", - "composite" : false, - "clientRole" : true, - "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", - "attributes" : { } - }, { - "id" : "a1203f7d-f93b-4ac2-b56a-065f6e3a1844", - "name" : "view-clients", - "description" : "${role_view-clients}", - "composite" : true, - "composites" : { - "client" : { - "realm-management" : [ "query-clients" ] - } - }, - "clientRole" : true, - "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", - "attributes" : { } - }, { - "id" : "012f433b-d622-47c2-895c-3c970209f193", - "name" : "manage-realm", - "description" : "${role_manage-realm}", - "composite" : false, - "clientRole" : true, - "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", - "attributes" : { } - }, { - "id" : "ef2860ce-4b70-46b1-999a-a52252750b48", - "name" : "view-identity-providers", - "description" : "${role_view-identity-providers}", - "composite" : false, - "clientRole" : true, - "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", - "attributes" : { } - }, { - "id" : "e5235e1f-3470-4419-bb81-67f76c91aa23", - "name" : "query-realms", - "description" : "${role_query-realms}", - "composite" : false, - "clientRole" : true, - "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", - "attributes" : { } - }, { - "id" : "a5e183c3-6925-4228-9e9c-00e224a315a8", - "name" : "manage-users", - "description" : "${role_manage-users}", - "composite" : false, - "clientRole" : true, - "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", - "attributes" : { } - }, { - "id" : "f1aa65cd-c595-44cd-90df-142cf5a8d7a5", - "name" : "view-events", - "description" : "${role_view-events}", - "composite" : false, - "clientRole" : true, - "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", - "attributes" : { } - }, { - "id" : "d2db0c5e-f7da-4ae0-9a87-84c0f280c16c", - "name" : "realm-admin", - "description" : "${role_realm-admin}", - "composite" : true, - "composites" : { - "client" : { - "realm-management" : [ "view-authorization", "query-groups", "manage-authorization", "view-clients", "manage-realm", "view-identity-providers", "query-realms", "manage-users", "view-events", "query-users", "manage-events", "manage-clients", "view-users", "view-realm", "impersonation", "create-client", "query-clients", "manage-identity-providers" ] - } - }, - "clientRole" : true, - "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", - "attributes" : { } - }, { - "id" : "eaebdfa1-e6c6-4b44-aedc-819ee3c6c4ec", - "name" : "query-users", - "description" : "${role_query-users}", - "composite" : false, - "clientRole" : true, - "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", - "attributes" : { } - }, { - "id" : "c0d79eeb-9eb7-4e14-b50e-2b0500fc7181", - "name" : "manage-events", - "description" : "${role_manage-events}", - "composite" : false, - "clientRole" : true, - "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", - "attributes" : { } - }, { - "id" : "0180a75e-9022-4a12-842d-295172cccb22", - "name" : "manage-clients", - "description" : "${role_manage-clients}", - "composite" : false, - "clientRole" : true, - "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", - "attributes" : { } - }, { - "id" : "68095bbe-5a69-4dc6-8972-ca96982a92c2", - "name" : "view-users", - "description" : "${role_view-users}", - "composite" : true, - "composites" : { - "client" : { - "realm-management" : [ "query-groups", "query-users" ] - } - }, - "clientRole" : true, - "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", - "attributes" : { } - }, { - "id" : "d92bbdfa-acd5-4e74-b518-9df525cab01f", - "name" : "view-realm", - "description" : "${role_view-realm}", - "composite" : false, - "clientRole" : true, - "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", - "attributes" : { } - }, { - "id" : "f2365fb1-d03f-4dc0-aa83-7d96e4fab676", - "name" : "impersonation", - "description" : "${role_impersonation}", - "composite" : false, - "clientRole" : true, - "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", - "attributes" : { } - }, { - "id" : "d918fc22-2751-4d21-b924-5e769b867e78", - "name" : "create-client", - "description" : "${role_create-client}", - "composite" : false, - "clientRole" : true, - "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", - "attributes" : { } - }, { - "id" : "df7d3e14-4e9f-4b3c-a01a-20d5d25459bf", - "name" : "query-clients", - "description" : "${role_query-clients}", - "composite" : false, - "clientRole" : true, - "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", - "attributes" : { } - }, { - "id" : "7f072c25-1801-4fd4-aedd-94d1964821ba", - "name" : "manage-identity-providers", - "description" : "${role_manage-identity-providers}", - "composite" : false, - "clientRole" : true, - "containerId" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", - "attributes" : { } - } ], - "security-admin-console" : [ ], - "admin-cli" : [ ], - "account-console" : [ ], - "broker" : [ { - "id" : "25c3d9c4-c9d8-4760-a29a-55b91f295530", - "name" : "read-token", - "description" : "${role_read-token}", - "composite" : false, - "clientRole" : true, - "containerId" : "e233a020-34ea-4e9d-9949-f9d839c4eb64", - "attributes" : { } - } ], - "account" : [ { - "id" : "9d39f10a-69d3-419b-b9fa-8328b4717b0b", - "name" : "manage-account", - "description" : "${role_manage-account}", - "composite" : true, - "composites" : { - "client" : { - "account" : [ "manage-account-links" ] - } - }, - "clientRole" : true, - "containerId" : "6ea8dcc8-c870-4012-8382-141d26864418", - "attributes" : { } - }, { - "id" : "7492bf66-f2a2-423f-9995-3cab271300cf", - "name" : "view-groups", - "description" : "${role_view-groups}", - "composite" : false, - "clientRole" : true, - "containerId" : "6ea8dcc8-c870-4012-8382-141d26864418", - "attributes" : { } - }, { - "id" : "2e059bf9-1c7c-4a59-958b-ea5082761c2f", - "name" : "manage-consent", - "description" : "${role_manage-consent}", - "composite" : true, - "composites" : { - "client" : { - "account" : [ "view-consent" ] - } - }, - "clientRole" : true, - "containerId" : "6ea8dcc8-c870-4012-8382-141d26864418", - "attributes" : { } - }, { - "id" : "9582787b-2975-437e-a5fe-29b515901555", - "name" : "manage-account-links", - "description" : "${role_manage-account-links}", - "composite" : false, - "clientRole" : true, - "containerId" : "6ea8dcc8-c870-4012-8382-141d26864418", - "attributes" : { } - }, { - "id" : "3b8dca64-75f9-465b-b55e-a34db591874c", - "name" : "view-applications", - "description" : "${role_view-applications}", - "composite" : false, - "clientRole" : true, - "containerId" : "6ea8dcc8-c870-4012-8382-141d26864418", - "attributes" : { } - }, { - "id" : "de3d32e3-4945-45e9-b141-6632cc166480", - "name" : "delete-account", - "description" : "${role_delete-account}", - "composite" : false, - "clientRole" : true, - "containerId" : "6ea8dcc8-c870-4012-8382-141d26864418", - "attributes" : { } - }, { - "id" : "f9b78a08-22ae-45d8-9246-64140df4b284", - "name" : "view-consent", - "description" : "${role_view-consent}", - "composite" : false, - "clientRole" : true, - "containerId" : "6ea8dcc8-c870-4012-8382-141d26864418", - "attributes" : { } - }, { - "id" : "2cda31da-3997-45b5-aca7-a5b143e284bb", - "name" : "view-profile", - "description" : "${role_view-profile}", - "composite" : false, - "clientRole" : true, - "containerId" : "6ea8dcc8-c870-4012-8382-141d26864418", - "attributes" : { } - } ] - } - }, - "groups" : [ ], - "defaultRole" : { - "id" : "71555fc5-bcff-47ec-a2ba-9bdb33a813d5", - "name" : "default-roles-emumet", - "description" : "${role_default-roles}", - "composite" : true, - "clientRole" : false, - "containerId" : "20fdc959-26f7-4a13-92ea-5fcd3380d354" - }, - "requiredCredentials" : [ "password" ], - "otpPolicyType" : "totp", - "otpPolicyAlgorithm" : "HmacSHA1", - "otpPolicyInitialCounter" : 0, - "otpPolicyDigits" : 6, - "otpPolicyLookAheadWindow" : 1, - "otpPolicyPeriod" : 30, - "otpPolicyCodeReusable" : false, - "otpSupportedApplications" : [ "totpAppFreeOTPName", "totpAppGoogleName", "totpAppMicrosoftAuthenticatorName" ], - "localizationTexts" : { }, - "webAuthnPolicyRpEntityName" : "keycloak", - "webAuthnPolicySignatureAlgorithms" : [ "ES256", "RS256" ], - "webAuthnPolicyRpId" : "", - "webAuthnPolicyAttestationConveyancePreference" : "not specified", - "webAuthnPolicyAuthenticatorAttachment" : "not specified", - "webAuthnPolicyRequireResidentKey" : "not specified", - "webAuthnPolicyUserVerificationRequirement" : "not specified", - "webAuthnPolicyCreateTimeout" : 0, - "webAuthnPolicyAvoidSameAuthenticatorRegister" : false, - "webAuthnPolicyAcceptableAaguids" : [ ], - "webAuthnPolicyExtraOrigins" : [ ], - "webAuthnPolicyPasswordlessRpEntityName" : "keycloak", - "webAuthnPolicyPasswordlessSignatureAlgorithms" : [ "ES256", "RS256" ], - "webAuthnPolicyPasswordlessRpId" : "", - "webAuthnPolicyPasswordlessAttestationConveyancePreference" : "not specified", - "webAuthnPolicyPasswordlessAuthenticatorAttachment" : "not specified", - "webAuthnPolicyPasswordlessRequireResidentKey" : "not specified", - "webAuthnPolicyPasswordlessUserVerificationRequirement" : "not specified", - "webAuthnPolicyPasswordlessCreateTimeout" : 0, - "webAuthnPolicyPasswordlessAvoidSameAuthenticatorRegister" : false, - "webAuthnPolicyPasswordlessAcceptableAaguids" : [ ], - "webAuthnPolicyPasswordlessExtraOrigins" : [ ], - "scopeMappings" : [ { - "clientScope" : "offline_access", - "roles" : [ "offline_access" ] - } ], - "clientScopeMappings" : { - "account" : [ { - "client" : "account-console", - "roles" : [ "manage-account", "view-groups" ] - } ] - }, - "clients" : [ { - "id" : "6ea8dcc8-c870-4012-8382-141d26864418", - "clientId" : "account", - "name" : "${client_account}", - "rootUrl" : "${authBaseUrl}", - "baseUrl" : "/realms/emumet/account/", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ "/realms/emumet/account/*" ], - "webOrigins" : [ ], - "notBefore" : 0, - "bearerOnly" : false, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : false, - "serviceAccountsEnabled" : false, - "publicClient" : true, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { - "realm_client" : "false", - "post.logout.redirect.uris" : "+" - }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "basic", "email" ], - "optionalClientScopes" : [ "address", "phone", "organization", "offline_access", "microprofile-jwt" ] - }, { - "id" : "cdc2ed5e-d827-41cd-aa86-7a92cda4ddc9", - "clientId" : "account-console", - "name" : "${client_account-console}", - "rootUrl" : "${authBaseUrl}", - "baseUrl" : "/realms/emumet/account/", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ "/realms/emumet/account/*" ], - "webOrigins" : [ ], - "notBefore" : 0, - "bearerOnly" : false, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : false, - "serviceAccountsEnabled" : false, - "publicClient" : true, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { - "realm_client" : "false", - "post.logout.redirect.uris" : "+", - "pkce.code.challenge.method" : "S256" - }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "protocolMappers" : [ { - "id" : "13a4a708-b03c-47d9-99c1-5dbb0c678142", - "name" : "audience resolve", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-audience-resolve-mapper", - "consentRequired" : false, - "config" : { } - } ], - "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "basic", "email" ], - "optionalClientScopes" : [ "address", "phone", "organization", "offline_access", "microprofile-jwt" ] - }, { - "id" : "7f2b1299-d123-40eb-8882-5f44eb7b020a", - "clientId" : "admin-cli", - "name" : "${client_admin-cli}", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ ], - "webOrigins" : [ ], - "notBefore" : 0, - "bearerOnly" : false, - "consentRequired" : false, - "standardFlowEnabled" : false, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : true, - "serviceAccountsEnabled" : false, - "publicClient" : true, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { - "realm_client" : "false", - "client.use.lightweight.access.token.enabled" : "true", - "post.logout.redirect.uris" : "+" - }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : true, - "nodeReRegistrationTimeout" : 0, - "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "basic", "email" ], - "optionalClientScopes" : [ "address", "phone", "organization", "offline_access", "microprofile-jwt" ] - }, { - "id" : "e233a020-34ea-4e9d-9949-f9d839c4eb64", - "clientId" : "broker", - "name" : "${client_broker}", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ ], - "webOrigins" : [ ], - "notBefore" : 0, - "bearerOnly" : true, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : false, - "serviceAccountsEnabled" : false, - "publicClient" : false, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { - "realm_client" : "true", - "post.logout.redirect.uris" : "+" - }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "basic", "email" ], - "optionalClientScopes" : [ "address", "phone", "organization", "offline_access", "microprofile-jwt" ] - }, { - "id" : "b4365bfb-8dc3-4956-90c0-ddf520852804", - "clientId" : "myclient", - "name" : "", - "description" : "", - "rootUrl" : "", - "adminUrl" : "", - "baseUrl" : "", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ "https://www.keycloak.org/app/*" ], - "webOrigins" : [ "https://www.keycloak.org" ], - "notBefore" : 0, - "bearerOnly" : false, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : true, - "serviceAccountsEnabled" : false, - "publicClient" : true, - "frontchannelLogout" : true, - "protocol" : "openid-connect", - "attributes" : { - "realm_client" : "false", - "oidc.ciba.grant.enabled" : "false", - "backchannel.logout.session.required" : "true", - "post.logout.redirect.uris" : "+", - "oauth2.device.authorization.grant.enabled" : "false", - "backchannel.logout.revoke.offline.tokens" : "false" - }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : true, - "nodeReRegistrationTimeout" : -1, - "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "basic", "email" ], - "optionalClientScopes" : [ "address", "phone", "organization", "offline_access", "microprofile-jwt" ] - }, { - "id" : "fa6e8bc5-fc2d-4530-ad1b-a34d1a1e828c", - "clientId" : "realm-management", - "name" : "${client_realm-management}", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ ], - "webOrigins" : [ ], - "notBefore" : 0, - "bearerOnly" : true, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : false, - "serviceAccountsEnabled" : false, - "publicClient" : false, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { - "realm_client" : "true", - "post.logout.redirect.uris" : "+" - }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : false, - "nodeReRegistrationTimeout" : 0, - "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "basic", "email" ], - "optionalClientScopes" : [ "address", "phone", "organization", "offline_access", "microprofile-jwt" ] - }, { - "id" : "d759db8d-3c2d-47c1-8ebc-2d3f1116db8d", - "clientId" : "security-admin-console", - "name" : "${client_security-admin-console}", - "rootUrl" : "${authAdminUrl}", - "baseUrl" : "/admin/emumet/console/", - "surrogateAuthRequired" : false, - "enabled" : true, - "alwaysDisplayInConsole" : false, - "clientAuthenticatorType" : "client-secret", - "redirectUris" : [ "/admin/emumet/console/*" ], - "webOrigins" : [ "+" ], - "notBefore" : 0, - "bearerOnly" : false, - "consentRequired" : false, - "standardFlowEnabled" : true, - "implicitFlowEnabled" : false, - "directAccessGrantsEnabled" : false, - "serviceAccountsEnabled" : false, - "publicClient" : true, - "frontchannelLogout" : false, - "protocol" : "openid-connect", - "attributes" : { - "realm_client" : "false", - "client.use.lightweight.access.token.enabled" : "true", - "post.logout.redirect.uris" : "+", - "pkce.code.challenge.method" : "S256" - }, - "authenticationFlowBindingOverrides" : { }, - "fullScopeAllowed" : true, - "nodeReRegistrationTimeout" : 0, - "protocolMappers" : [ { - "id" : "45ee8535-98e7-48c3-96f9-501090eb4dab", - "name" : "locale", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "locale", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "locale", - "jsonType.label" : "String" - } - } ], - "defaultClientScopes" : [ "web-origins", "acr", "roles", "profile", "basic", "email" ], - "optionalClientScopes" : [ "address", "phone", "organization", "offline_access", "microprofile-jwt" ] - } ], - "clientScopes" : [ { - "id" : "8ae530d6-82c4-4fd6-a737-1b3e2be2cb18", - "name" : "roles", - "description" : "OpenID Connect scope for add user roles to the access token", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "false", - "consent.screen.text" : "${rolesScopeConsentText}", - "display.on.consent.screen" : "true" - }, - "protocolMappers" : [ { - "id" : "e40bd2e4-9d87-49d1-9e77-87017a7fe276", - "name" : "audience resolve", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-audience-resolve-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "access.token.claim" : "true" - } - }, { - "id" : "5160d261-4863-4e66-ae7f-2a1a0c827c1e", - "name" : "realm roles", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-realm-role-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "foo", - "introspection.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "realm_access.roles", - "jsonType.label" : "String", - "multivalued" : "true" - } - }, { - "id" : "33ffe511-24c4-4c92-9753-de100289e281", - "name" : "client roles", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-client-role-mapper", - "consentRequired" : false, - "config" : { - "user.attribute" : "foo", - "introspection.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "resource_access.${client_id}.roles", - "jsonType.label" : "String", - "multivalued" : "true" - } - } ] - }, { - "id" : "a6a9f309-bff4-42fc-849a-dfefc92c14a3", - "name" : "role_list", - "description" : "SAML role list", - "protocol" : "saml", - "attributes" : { - "consent.screen.text" : "${samlRoleListScopeConsentText}", - "display.on.consent.screen" : "true" - }, - "protocolMappers" : [ { - "id" : "aff40860-4fcf-4e28-b1a7-e76ae04a8e65", - "name" : "role list", - "protocol" : "saml", - "protocolMapper" : "saml-role-list-mapper", - "consentRequired" : false, - "config" : { - "single" : "false", - "attribute.nameformat" : "Basic", - "attribute.name" : "Role" - } - } ] - }, { - "id" : "006963a9-566d-4d4b-bfd1-bcc8e5b3be97", - "name" : "email", - "description" : "OpenID Connect built-in scope: email", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "true", - "consent.screen.text" : "${emailScopeConsentText}", - "display.on.consent.screen" : "true" - }, - "protocolMappers" : [ { - "id" : "a3e00a31-6f3c-4f8d-ac15-10d9f7d4a34b", - "name" : "email verified", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-property-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "emailVerified", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "email_verified", - "jsonType.label" : "boolean" - } - }, { - "id" : "6567d0f4-8069-47db-bffa-327113cdb35a", - "name" : "email", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "email", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "email", - "jsonType.label" : "String" - } - } ] - }, { - "id" : "91894ed6-98fe-45b5-8245-b8a6a2c8fc15", - "name" : "web-origins", - "description" : "OpenID Connect scope for add allowed web origins to the access token", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "false", - "consent.screen.text" : "", - "display.on.consent.screen" : "false" - }, - "protocolMappers" : [ { - "id" : "2fe3bfc2-0c05-4202-959f-28d5442f2576", - "name" : "allowed web origins", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-allowed-origins-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "access.token.claim" : "true" - } - } ] - }, { - "id" : "12cb62b5-7a31-4aa9-af58-05d58387447c", - "name" : "organization", - "description" : "Additional claims about the organization a subject belongs to", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "true", - "consent.screen.text" : "${organizationScopeConsentText}", - "display.on.consent.screen" : "true" - }, - "protocolMappers" : [ { - "id" : "27d37db0-6df0-46a3-ad1a-9bf7068f1f29", - "name" : "organization", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-organization-membership-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "multivalued" : "true", - "userinfo.token.claim" : "true", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "organization", - "jsonType.label" : "String" - } - } ] - }, { - "id" : "7ab385a7-8fa1-4016-a41b-773db5c7ded8", - "name" : "address", - "description" : "OpenID Connect built-in scope: address", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "true", - "consent.screen.text" : "${addressScopeConsentText}", - "display.on.consent.screen" : "true" - }, - "protocolMappers" : [ { - "id" : "b225fb58-ceec-4bb9-89c4-e074c5a849c2", - "name" : "address", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-address-mapper", - "consentRequired" : false, - "config" : { - "user.attribute.formatted" : "formatted", - "user.attribute.country" : "country", - "introspection.token.claim" : "true", - "user.attribute.postal_code" : "postal_code", - "userinfo.token.claim" : "true", - "user.attribute.street" : "street", - "id.token.claim" : "true", - "user.attribute.region" : "region", - "access.token.claim" : "true", - "user.attribute.locality" : "locality" - } - } ] - }, { - "id" : "4d0d4f4a-1124-465d-88c9-8370febbc667", - "name" : "profile", - "description" : "OpenID Connect built-in scope: profile", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "true", - "consent.screen.text" : "${profileScopeConsentText}", - "display.on.consent.screen" : "true" - }, - "protocolMappers" : [ { - "id" : "5ddba364-ad48-4477-8886-7476f6fdaa31", - "name" : "birthdate", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "birthdate", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "birthdate", - "jsonType.label" : "String" - } - }, { - "id" : "a6cf2b69-4028-4866-8205-376f3cdfe6b3", - "name" : "zoneinfo", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "zoneinfo", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "zoneinfo", - "jsonType.label" : "String" - } - }, { - "id" : "5bb144fd-d7cc-4f33-be28-7fa9785dfce2", - "name" : "given name", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "firstName", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "given_name", - "jsonType.label" : "String" - } - }, { - "id" : "4d821a65-224b-43a7-b37e-4aa2eff1f3e4", - "name" : "profile", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "profile", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "profile", - "jsonType.label" : "String" - } - }, { - "id" : "c68aff47-70b3-48aa-bd4d-9f8b4526849e", - "name" : "picture", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "picture", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "picture", - "jsonType.label" : "String" - } - }, { - "id" : "49a5a153-ff85-4764-92c3-2ba9e64c9985", - "name" : "full name", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-full-name-mapper", - "consentRequired" : false, - "config" : { - "id.token.claim" : "true", - "introspection.token.claim" : "true", - "access.token.claim" : "true", - "userinfo.token.claim" : "true" - } - }, { - "id" : "0de79ae3-e2d9-4ec0-99fc-3b9ca32d8acf", - "name" : "gender", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "gender", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "gender", - "jsonType.label" : "String" - } - }, { - "id" : "ba9c17a3-4785-4ae1-b508-9a57cbdeffe5", - "name" : "family name", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "lastName", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "family_name", - "jsonType.label" : "String" - } - }, { - "id" : "8aa3b2f6-962a-4407-84ee-223a27de80cb", - "name" : "middle name", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "middleName", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "middle_name", - "jsonType.label" : "String" - } - }, { - "id" : "b5414967-f28d-4f27-984e-06f93e6c6107", - "name" : "locale", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "locale", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "locale", - "jsonType.label" : "String" - } - }, { - "id" : "750c86fa-2a62-4b69-9cb2-577b834e386c", - "name" : "website", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "website", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "website", - "jsonType.label" : "String" - } - }, { - "id" : "6131f323-699a-4bb7-97a6-d1f0a0a3c854", - "name" : "username", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "username", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "preferred_username", - "jsonType.label" : "String" - } - }, { - "id" : "9626048b-ead5-45d6-b9e5-aef75d9d0c2b", - "name" : "nickname", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "nickname", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "nickname", - "jsonType.label" : "String" - } - }, { - "id" : "298e2913-a90d-4d10-869c-0c5aff7034e6", - "name" : "updated at", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "updatedAt", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "updated_at", - "jsonType.label" : "long" - } - } ] - }, { - "id" : "8eec08ca-9335-4e7e-8a86-55c60d0df7de", - "name" : "basic", - "description" : "OpenID Connect scope for add all basic claims to the token", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "false", - "display.on.consent.screen" : "false" - }, - "protocolMappers" : [ { - "id" : "9d074af9-43c9-4abf-a8f4-236e388c19cd", - "name" : "auth_time", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usersessionmodel-note-mapper", - "consentRequired" : false, - "config" : { - "user.session.note" : "AUTH_TIME", - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "auth_time", - "jsonType.label" : "long" - } - }, { - "id" : "558fe31c-4ef1-490a-9461-6b8250549c3a", - "name" : "sub", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-sub-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "access.token.claim" : "true" - } - } ] - }, { - "id" : "dce02b5b-e49a-4143-afd8-4df702f427a5", - "name" : "phone", - "description" : "OpenID Connect built-in scope: phone", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "true", - "consent.screen.text" : "${phoneScopeConsentText}", - "display.on.consent.screen" : "true" - }, - "protocolMappers" : [ { - "id" : "640cf110-1a95-4b8a-8c77-f18c55ffc897", - "name" : "phone number", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "phoneNumber", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "phone_number", - "jsonType.label" : "String" - } - }, { - "id" : "aa261d25-9b27-4db2-93cf-a9658b78f2fb", - "name" : "phone number verified", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "phoneNumberVerified", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "phone_number_verified", - "jsonType.label" : "boolean" - } - } ] - }, { - "id" : "2d33305a-1ce2-42c6-ac0b-080b3dc1ad2f", - "name" : "microprofile-jwt", - "description" : "Microprofile - JWT built-in scope", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "true", - "display.on.consent.screen" : "false" - }, - "protocolMappers" : [ { - "id" : "5199c6a2-b77b-42d9-a347-1334a861737a", - "name" : "groups", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-realm-role-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "multivalued" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "foo", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "groups", - "jsonType.label" : "String" - } - }, { - "id" : "06e64f62-2ecc-484c-9eb7-e33758a390ca", - "name" : "upn", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usermodel-attribute-mapper", - "consentRequired" : false, - "config" : { - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "user.attribute" : "username", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "upn", - "jsonType.label" : "String" - } - } ] - }, { - "id" : "c1412c8d-2c8f-4b18-9176-fde822cf836b", - "name" : "acr", - "description" : "OpenID Connect scope for add acr (authentication context class reference) to the token", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "false", - "display.on.consent.screen" : "false" - }, - "protocolMappers" : [ { - "id" : "584060c1-e826-4a77-a24a-f52fab27ada0", - "name" : "acr loa level", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-acr-mapper", - "consentRequired" : false, - "config" : { - "id.token.claim" : "true", - "introspection.token.claim" : "true", - "access.token.claim" : "true", - "userinfo.token.claim" : "true" - } - } ] - }, { - "id" : "aeee216c-ab51-43a7-9e7d-3431061c9821", - "name" : "service_account", - "description" : "Specific scope for a client enabled for service accounts", - "protocol" : "openid-connect", - "attributes" : { - "include.in.token.scope" : "false", - "display.on.consent.screen" : "false" - }, - "protocolMappers" : [ { - "id" : "fe492866-50e2-4872-b30a-2f605892d4f1", - "name" : "Client IP Address", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usersessionmodel-note-mapper", - "consentRequired" : false, - "config" : { - "user.session.note" : "clientAddress", - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "clientAddress", - "jsonType.label" : "String" - } - }, { - "id" : "535bee5a-aa97-40fe-b053-e91581100767", - "name" : "Client Host", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usersessionmodel-note-mapper", - "consentRequired" : false, - "config" : { - "user.session.note" : "clientHost", - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "clientHost", - "jsonType.label" : "String" - } - }, { - "id" : "8177487b-fd50-42dd-a8b6-9a55d8886b2a", - "name" : "Client ID", - "protocol" : "openid-connect", - "protocolMapper" : "oidc-usersessionmodel-note-mapper", - "consentRequired" : false, - "config" : { - "user.session.note" : "client_id", - "introspection.token.claim" : "true", - "userinfo.token.claim" : "true", - "id.token.claim" : "true", - "access.token.claim" : "true", - "claim.name" : "client_id", - "jsonType.label" : "String" - } - } ] - }, { - "id" : "425e9994-1df2-49ff-9987-e2cd616ed41a", - "name" : "offline_access", - "description" : "OpenID Connect built-in scope: offline_access", - "protocol" : "openid-connect", - "attributes" : { - "consent.screen.text" : "${offlineAccessScopeConsentText}", - "display.on.consent.screen" : "true" - } - }, { - "id" : "690650b8-a502-4cc8-a107-8ed4c6096a92", - "name" : "saml_organization", - "description" : "Organization Membership", - "protocol" : "saml", - "attributes" : { - "display.on.consent.screen" : "false" - }, - "protocolMappers" : [ { - "id" : "d6292315-8182-4a9f-98f8-a75af0e1dfe3", - "name" : "organization", - "protocol" : "saml", - "protocolMapper" : "saml-organization-membership-mapper", - "consentRequired" : false, - "config" : { } - } ] - } ], - "defaultDefaultClientScopes" : [ "role_list", "saml_organization", "profile", "email", "roles", "web-origins", "acr", "basic" ], - "defaultOptionalClientScopes" : [ "offline_access", "address", "phone", "microprofile-jwt", "organization" ], - "browserSecurityHeaders" : { - "contentSecurityPolicyReportOnly" : "", - "xContentTypeOptions" : "nosniff", - "referrerPolicy" : "no-referrer", - "xRobotsTag" : "none", - "xFrameOptions" : "SAMEORIGIN", - "contentSecurityPolicy" : "frame-src 'self'; frame-ancestors 'self'; object-src 'none';", - "xXSSProtection" : "1; mode=block", - "strictTransportSecurity" : "max-age=31536000; includeSubDomains" - }, - "smtpServer" : { }, - "eventsEnabled" : false, - "eventsListeners" : [ "jboss-logging" ], - "enabledEventTypes" : [ ], - "adminEventsEnabled" : false, - "adminEventsDetailsEnabled" : false, - "identityProviders" : [ ], - "identityProviderMappers" : [ ], - "components" : { - "org.keycloak.services.clientregistration.policy.ClientRegistrationPolicy" : [ { - "id" : "300fbe82-c8ae-4e9b-bb69-29595ab1af15", - "name" : "Allowed Protocol Mapper Types", - "providerId" : "allowed-protocol-mappers", - "subType" : "anonymous", - "subComponents" : { }, - "config" : { - "allowed-protocol-mapper-types" : [ "saml-user-attribute-mapper", "oidc-usermodel-property-mapper", "saml-user-property-mapper", "oidc-usermodel-attribute-mapper", "oidc-full-name-mapper", "oidc-address-mapper", "oidc-sha256-pairwise-sub-mapper", "saml-role-list-mapper" ] - } - }, { - "id" : "62112051-56c0-493e-85bb-39e51c772d60", - "name" : "Trusted Hosts", - "providerId" : "trusted-hosts", - "subType" : "anonymous", - "subComponents" : { }, - "config" : { - "host-sending-registration-request-must-match" : [ "true" ], - "client-uris-must-match" : [ "true" ] - } - }, { - "id" : "40069870-8ee9-4ba8-ad77-a4901da5cd91", - "name" : "Allowed Protocol Mapper Types", - "providerId" : "allowed-protocol-mappers", - "subType" : "authenticated", - "subComponents" : { }, - "config" : { - "allowed-protocol-mapper-types" : [ "oidc-usermodel-attribute-mapper", "saml-role-list-mapper", "saml-user-property-mapper", "oidc-full-name-mapper", "oidc-sha256-pairwise-sub-mapper", "oidc-usermodel-property-mapper", "saml-user-attribute-mapper", "oidc-address-mapper" ] - } - }, { - "id" : "965d7cea-339e-43e7-9815-458964c34557", - "name" : "Allowed Client Scopes", - "providerId" : "allowed-client-templates", - "subType" : "authenticated", - "subComponents" : { }, - "config" : { - "allow-default-scopes" : [ "true" ] - } - }, { - "id" : "065d1d40-0ca7-432b-8555-00dd4481b887", - "name" : "Allowed Client Scopes", - "providerId" : "allowed-client-templates", - "subType" : "anonymous", - "subComponents" : { }, - "config" : { - "allow-default-scopes" : [ "true" ] - } - }, { - "id" : "2ba17e54-23f1-4eb6-adab-970864799fa0", - "name" : "Max Clients Limit", - "providerId" : "max-clients", - "subType" : "anonymous", - "subComponents" : { }, - "config" : { - "max-clients" : [ "200" ] - } - }, { - "id" : "d6b88488-c61f-417b-bed9-5c01a31966af", - "name" : "Consent Required", - "providerId" : "consent-required", - "subType" : "anonymous", - "subComponents" : { }, - "config" : { } - }, { - "id" : "6d1b1fda-3811-417d-a23e-5bb86ed7b938", - "name" : "Full Scope Disabled", - "providerId" : "scope", - "subType" : "anonymous", - "subComponents" : { }, - "config" : { } - } ], - "org.keycloak.keys.KeyProvider" : [ { - "id" : "8b6d52a5-9538-4e62-a5b9-e770493e3c44", - "name" : "aes-generated", - "providerId" : "aes-generated", - "subComponents" : { }, - "config" : { - "kid" : [ "d8c544dc-de0d-4ac0-a736-64dbddd49f03" ], - "secret" : [ "uewDe_LI6Fv8kdHNpiGBYQ" ], - "priority" : [ "100" ] - } - }, { - "id" : "a23d66ed-50e0-4b96-9e50-3dc3f7aa9063", - "name" : "hmac-generated-hs512", - "providerId" : "hmac-generated", - "subComponents" : { }, - "config" : { - "kid" : [ "6335f7e5-2b49-421e-83ed-56250fc5426a" ], - "secret" : [ "UeQ65_Pp9_p_nYNwlEc682v3TqL0IH4Y6zs_tiVM8PyOAQTlDpavdjn8KU_Du6rfizPRKXPkoKIcJI5LZyUDAnwv9zBpxIRTHqjPArUAbqPNBjlCmXtuHB-5KdLfxghY-Mu2w1gWQvYpTtPVZSRolUhy6TSXC72tf9pHpRwJs74" ], - "priority" : [ "100" ], - "algorithm" : [ "HS512" ] - } - }, { - "id" : "7c333ba3-85a4-4dfe-88e0-dc3ed71b29ac", - "name" : "rsa-enc-generated", - "providerId" : "rsa-enc-generated", - "subComponents" : { }, - "config" : { - "privateKey" : [ "MIIEogIBAAKCAQEApfy3E12+UJx3l55drUkINQOmvhp61x2sYTZd2J2LiJLawsrWA+tdWQ6cODISjHfPIeMlEHeWU1jtnGxhOl5TQ5to+Srnt9c4Y7Nf1mhmHyuCvEMXaio8Hw1uEFnWGZp6QR56M7Ur9jxWwr0R2WAeOx1S3a4hlb87ussPN/8eS7ecjBP9BW/sz/x2c91ZPxnsAB2zl4RHGomi1a0ElEHIZVkzGV4bPsI0HXvDqB/NyutncXrNSbhev32Kq7L1mO/3zJ/9XBnP5h5o+XagoiiT0qX2gc6638MDFiAx1OVNpg9U6gdHTl3UsNcPBQG0uZxkVDtozd9z1f8kLBBpOJEP4wIDAQABAoIBAAkWs92TGfsm/iNmsAFviMwCVax+HbDOtqQiCnR0d/Hz/JeC7MINLrDULHilQT/Axa441kw3CBurOGOCybYc+RkwFsjh8QsvdS58YWiHkePuCXwOfmc5Rc57eUXa7W68dyo+pXlUV8JlXmjOWn5ZFX6uJd3ujXc6H+aj/MLXrMx/fDFEWajk8Qvgbi4/Fb7qh96sMa2OBKyaXPBgpDWT+vnr7EsC4j+eIfgwAtuaEOCv63ez3pqp6CP5gy3uG6w4UIhlix1BMHcgxjlHwND6Gn+J8nSQAl1kAgqavNKKOqeKjR1xBDxlwEKqpVRYkoCIoznCbHReizCaAmx10OREvCUCgYEA18vh+3+z8UvfsF4B+9ny5fJuVLHp06220ha9z6hjurZE9wPAzGMNbKSbagO+K1yw0N+IfcKr9V9hPuvJz8gR3BPeZHjvFDnhLJr6Ihgqu5+3qtGUw0bLoYjPV11Daqx8H3GkUQC/gb7Ykb2phwKzczZw7Pvpkj4gEXVIumOpX70CgYEAxOk/dWUpK8o0YgUSwpbkQ/8y2++4LtLxT1HZ8QbR7ATV8MFQmZ8TlR6iSDERPKW+u4z249/mgkf8roXI3AcoUYgrP97mjZCvbKlk1ql1Mko6GACp5zt8U31+jR20tMyd5gjo9bP0hqLixkJSclXFnE04gkFkYZCm5E9kiERT2B8CgYB3WUWUqR5GN+ZxTqzeM75JOvmWUge2kP7p1rYH4WO24hPmYecBo07LZYam7YcByHPqMZb1pvMf9C5+dD3bcxWdmEeJXfEsSI6m8tegf6kyt7UG/n6+OatpnZa/BM/Ccb78TQfJ3RYNlhWFFVZrWy0QbW2rQ+/8d+uYfDtLCs+kKQKBgE5O8FSwgVoP1RsyJ07JkUfVYpWC1P2SGDNSOtkWvD8fgTF4v6QIVlJUV3dcRB2ZUKvnmHvxHAutszh4rfOKySb7fy+sZoXgB1OwXhDcXWY9jLLk+KyjxIKzgrN+H9JTGWxVGMg148XzWzo7P+yGXcsWDqYGeXQvgZ+ET1e9zJZDAoGABCE+rU9gaT/UL2ftqPbdX2azeAP9++/BtmZ14WwCo0Y0VJaWU0va+oKtWZlDibRJcBJJRDH1OCYovZirqy8bTm4PFLn6hMosfWnVlksvoYZZ3mNV7n8vtyjjtLnERxCv34I3tOc/8+rdTZ2zgztnsGtou/CL+WeYiRXLtyn5FnM=" ], - "certificate" : [ "MIICmzCCAYMCBgGU3jgG7DANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZlbXVtZXQwHhcNMjUwMjA3MDIyMTQxWhcNMzUwMjA3MDIyMzIxWjARMQ8wDQYDVQQDDAZlbXVtZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCl/LcTXb5QnHeXnl2tSQg1A6a+GnrXHaxhNl3YnYuIktrCytYD611ZDpw4MhKMd88h4yUQd5ZTWO2cbGE6XlNDm2j5Kue31zhjs1/WaGYfK4K8QxdqKjwfDW4QWdYZmnpBHnoztSv2PFbCvRHZYB47HVLdriGVvzu6yw83/x5Lt5yME/0Fb+zP/HZz3Vk/GewAHbOXhEcaiaLVrQSUQchlWTMZXhs+wjQde8OoH83K62dxes1JuF6/fYqrsvWY7/fMn/1cGc/mHmj5dqCiKJPSpfaBzrrfwwMWIDHU5U2mD1TqB0dOXdSw1w8FAbS5nGRUO2jN33PV/yQsEGk4kQ/jAgMBAAEwDQYJKoZIhvcNAQELBQADggEBAIR1JWnpHseNHZmNzZHfHndy4lI/MtibamT4wle9NkOxQ/DxoJpSjs9u+X/In16hbp7OQoXC4JPenAy4zpfhHoYoAytfv6N2UwP0IYdJpbqwn7xwfVzBHoCFXg8v7eLE2GZ0qjNtRgsdSHOmqvokWhqW7/xKL+9MAu2nGKtBPOjYJOKWqWa3N1A/S0s4pWYy8vG3OmS8bCDGY/B3jwc4zf3ABfBfVRSJSRhwXymc9216kTjyMe6Lf7p0Mmse4dFuYst2OFn2twHcFui7eeNu9NMu9lxhN3DbUcmNTAl87nEqzDMX+WqUIMf45EjRTHqI69npfI++QalYH+ed/HTZweA=" ], - "priority" : [ "100" ], - "algorithm" : [ "RSA-OAEP" ] - } - }, { - "id" : "2c913445-586f-4591-a661-3bf7cbc55f27", - "name" : "rsa-generated", - "providerId" : "rsa-generated", - "subComponents" : { }, - "config" : { - "privateKey" : [ "MIIEowIBAAKCAQEAos3UsvHode88kEZEXlBPpRqGXYIpvHjDyDrOfW+9wV1PDONQzbb2twEcQ9KsKUqVED48P/+hwq24LA5gSh2j24xX7OuNTP69Fdf1OOnfvya/seAYDreqU5DnmsK9qJFYuijuqoCT4VpdcJy+N0pJ7IKWC6iAjfWGoWT3eyI/VgUn5nEpSbjsZ+Cg3kSrCg6Ss0l0tt5LPIvPoQk7uzP+OmXkLUmliXZXb4OP3JTctciwL6uloSA1CRkct5VsYjgzsCKmtla47WHAIdlC83NHfjkBJMMCbzkVYmzap52URf2hfj2nYsGJ/iKFrbuC/hWtRS/e45Uh5RUAKHdiipwAFQIDAQABAoIBADwCfN1897/I8F0J2ZeeKM1l6pM7MGEtbpU2v/hSoPJOj53jiFxbjbNFMIL7e8Q4npt/JTw94QVefV2X6vxG0qhRofNNnCb+WvpbQSO6aWQPR2esf5GlN55X8lcEY15oPPlZryef/2J4qaqhzCebNYZ9WAtyD/jDwN1q1yJHLGtrIYnS4cEt6iLLsC4SiZ8ofwSzS5k+yklDp0PhDBmv4oiNrWbZfDlh2HWHYPe2TnSsk5YDGu7XWTQ6zOTthuIv4lcijfFuQpO/SJTYgxF16qNRaIxiG6mljWOssE4o03XMDURIbYMR0BHuM8z+VYiy8v7nz50ehaCwLsANr4lpJF0CgYEA5ek+2YIS7Dzffyx0mNkJV9PVqhe4hHOEpCcBTMKatV3ERljmVZZH9T436Eolja7hTAULpRDy+cqC8opghBQCpW33PGb8w/9/TEr9L1N5xQWm3Ifi0lRkCTv34kTBh8vQEk8nCubbivOXFK5a+C/tjQobcP4BcVrfD66VMddyFgcCgYEAtUcsEE0X+3n3krUJkJxDqDQcdKPDjgEOijuDkqkTk1tEbz+tN6bQ5T71DD7swh1DkG+bO+X1jXEix5I6ByhLXAQqaF7tZcJSBJDaRql4U7aIdV3jhozIfT4etUZZ8EgfDQYZ3egdc1710gOgDp1ciHJTZ16bk6OCMXTdy/Ms0gMCgYABr/mPHR5Ib5XwWAIvEQC5jUt3KR9okXR6w/KFfrQl+p8zKPnfzO+QRDmi0dB+vrbWmP7h4kL2RF87qnpU3dS7JBh5cAQQ6DIl/DLpgwJUyNrVqYWnp4jobHFATuLgvUU0rTILKXCZD3qfYzw1sBxdOaLD7IlULKeQdOaRbBRhRwKBgHIOD6lJ+DbfLGd/xD7aMq9X6jdw+g8UlyNeApB6FLj4CXy9YazMJk62Z9OGm8weQW5U6iSrsO2HK0zJsfzi21dPv6bfYxpNQvFgehVPd0ekZwMBSbBUT6iNNyDy3I+TsQWuuwOlkTIPoza51TCczaWD2PoGynf/vmCDmTFDFQYlAoGBANHhlyDwQt4B3bJERpdP1G8kLt1Xk0BNMQ02iiGG8n0/J4hsJ1OUvCl+okDvlfO4RgXxiL6JicfxSd6wDde9iVYGFtcT0Q6hIk7GMrLeRcQ/tlQnsnaA92wg2vamiDhjOZUjRQ45oApBR5WZYRYvGXv14y7aK4ZfJKom9ns+T7f5" ], - "certificate" : [ "MIICmzCCAYMCBgGU3jgHijANBgkqhkiG9w0BAQsFADARMQ8wDQYDVQQDDAZlbXVtZXQwHhcNMjUwMjA3MDIyMTQxWhcNMzUwMjA3MDIyMzIxWjARMQ8wDQYDVQQDDAZlbXVtZXQwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCizdSy8eh17zyQRkReUE+lGoZdgim8eMPIOs59b73BXU8M41DNtva3ARxD0qwpSpUQPjw//6HCrbgsDmBKHaPbjFfs641M/r0V1/U46d+/Jr+x4BgOt6pTkOeawr2okVi6KO6qgJPhWl1wnL43SknsgpYLqICN9YahZPd7Ij9WBSfmcSlJuOxn4KDeRKsKDpKzSXS23ks8i8+hCTu7M/46ZeQtSaWJdldvg4/clNy1yLAvq6WhIDUJGRy3lWxiODOwIqa2VrjtYcAh2ULzc0d+OQEkwwJvORVibNqnnZRF/aF+PadiwYn+IoWtu4L+Fa1FL97jlSHlFQAod2KKnAAVAgMBAAEwDQYJKoZIhvcNAQELBQADggEBADCADOwdBJA3bkb9X6M3mRQA6kH+OeMDNNq/ZVdw3vv+n7DbAkLY+r3s30cShP9GH+CHajyAdFhhUvyhbEcfwY3e5aJnfrwU/uqPmg7pjF60JbYfVFC+9E930KKfBkz5Hx1X9wFvy1tYanojf9LGYfXzC5DcLvbYU5oax/8lsuZEQ9Rgw6HNdiodRm+Fb8OvfvXJlev6xBelCfZ61R1s9HqDJXCkRiOi0/FeMT17t1H9UEnPR0LbhqQe86Ra2u43YNfxx/qof3dfsYQrnW+xMnkdpWoF5oBLJjE03OyI3n2VQW+OdPhNt4/NooKFSM9TJejnwSZ7II3LxCyjA4adfaw=" ], - "priority" : [ "100" ] - } - } ] - }, - "internationalizationEnabled" : false, - "supportedLocales" : [ ], - "authenticationFlows" : [ { - "id" : "ac47d516-4b49-4281-9730-19b44b9a0d7e", - "alias" : "Account verification options", - "description" : "Method with which to verity the existing account", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "idp-email-verification", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "ALTERNATIVE", - "priority" : 20, - "autheticatorFlow" : true, - "flowAlias" : "Verify Existing Account by Re-authentication", - "userSetupAllowed" : false - } ] - }, { - "id" : "afab1457-54b4-4767-a435-2c134a621e5c", - "alias" : "Browser - Conditional OTP", - "description" : "Flow to determine if the OTP is required for the authentication", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "conditional-user-configured", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "auth-otp-form", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "f2674436-a31c-4b9b-aa36-da5f8713f1c2", - "alias" : "Browser - Conditional Organization", - "description" : "Flow to determine if the organization identity-first login is to be used", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "conditional-user-configured", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "organization", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "ec701c44-c425-4c31-9fdb-b0500dc49184", - "alias" : "Direct Grant - Conditional OTP", - "description" : "Flow to determine if the OTP is required for the authentication", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "conditional-user-configured", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "direct-grant-validate-otp", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "68e873db-9952-441d-8c18-ebe26575d3a0", - "alias" : "First Broker Login - Conditional Organization", - "description" : "Flow to determine if the authenticator that adds organization members is to be used", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "conditional-user-configured", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "idp-add-organization-member", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "bb675959-4199-4a6b-81ef-7cac6931a3a2", - "alias" : "First broker login - Conditional OTP", - "description" : "Flow to determine if the OTP is required for the authentication", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "conditional-user-configured", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "auth-otp-form", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "35f32124-f039-4b95-856b-f48889dd0bf8", - "alias" : "Handle Existing Account", - "description" : "Handle what to do if there is existing account with same email/username like authenticated identity provider", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "idp-confirm-link", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : true, - "flowAlias" : "Account verification options", - "userSetupAllowed" : false - } ] - }, { - "id" : "585121a6-1f43-433b-877f-86ad6792d590", - "alias" : "Organization", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticatorFlow" : true, - "requirement" : "CONDITIONAL", - "priority" : 10, - "autheticatorFlow" : true, - "flowAlias" : "Browser - Conditional Organization", - "userSetupAllowed" : false - } ] - }, { - "id" : "3a7b6fba-6b57-42f1-8d64-bf34da9d1768", - "alias" : "Reset - Conditional OTP", - "description" : "Flow to determine if the OTP should be reset or not. Set to REQUIRED to force.", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "conditional-user-configured", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "reset-otp", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "c4a91189-6108-4547-b919-4e4d7ca70cec", - "alias" : "User creation or linking", - "description" : "Flow for the existing/non-existing user alternatives", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticatorConfig" : "create unique user config", - "authenticator" : "idp-create-user-if-unique", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "ALTERNATIVE", - "priority" : 20, - "autheticatorFlow" : true, - "flowAlias" : "Handle Existing Account", - "userSetupAllowed" : false - } ] - }, { - "id" : "a0a95326-920c-47bf-b401-ccfd5c9503fe", - "alias" : "Verify Existing Account by Re-authentication", - "description" : "Reauthentication of existing account", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "idp-username-password-form", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "CONDITIONAL", - "priority" : 20, - "autheticatorFlow" : true, - "flowAlias" : "First broker login - Conditional OTP", - "userSetupAllowed" : false - } ] - }, { - "id" : "b35f76dc-eb0b-4ca0-aa48-6615ad1dc494", - "alias" : "browser", - "description" : "Browser based authentication", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "auth-cookie", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "auth-spnego", - "authenticatorFlow" : false, - "requirement" : "DISABLED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "identity-provider-redirector", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 25, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "ALTERNATIVE", - "priority" : 26, - "autheticatorFlow" : true, - "flowAlias" : "Organization", - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "ALTERNATIVE", - "priority" : 30, - "autheticatorFlow" : true, - "flowAlias" : "forms", - "userSetupAllowed" : false - } ] - }, { - "id" : "1489b0e6-310a-465b-a184-3f77ed84fb99", - "alias" : "clients", - "description" : "Base authentication for clients", - "providerId" : "client-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "client-secret", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "client-jwt", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "client-secret-jwt", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 30, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "client-x509", - "authenticatorFlow" : false, - "requirement" : "ALTERNATIVE", - "priority" : 40, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "b9e4d292-4c18-4584-9698-c9f18b3f701b", - "alias" : "direct grant", - "description" : "OpenID Connect Resource Owner Grant", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "direct-grant-validate-username", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "direct-grant-validate-password", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "CONDITIONAL", - "priority" : 30, - "autheticatorFlow" : true, - "flowAlias" : "Direct Grant - Conditional OTP", - "userSetupAllowed" : false - } ] - }, { - "id" : "8411190c-ec3f-41e6-94d3-418c8ebee96e", - "alias" : "docker auth", - "description" : "Used by Docker clients to authenticate against the IDP", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "docker-http-basic-authenticator", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "56a8da93-3260-446e-8649-40898020aa8a", - "alias" : "first broker login", - "description" : "Actions taken after first broker login with identity provider account, which is not yet linked to any Keycloak account", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticatorConfig" : "review profile config", - "authenticator" : "idp-review-profile", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : true, - "flowAlias" : "User creation or linking", - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "CONDITIONAL", - "priority" : 50, - "autheticatorFlow" : true, - "flowAlias" : "First Broker Login - Conditional Organization", - "userSetupAllowed" : false - } ] - }, { - "id" : "7eedc13c-788d-479e-95ec-bec0ccb6cba8", - "alias" : "forms", - "description" : "Username, password, otp and other auth forms.", - "providerId" : "basic-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "auth-username-password-form", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "CONDITIONAL", - "priority" : 20, - "autheticatorFlow" : true, - "flowAlias" : "Browser - Conditional OTP", - "userSetupAllowed" : false - } ] - }, { - "id" : "60abd1c3-362a-4130-a4a1-b1bde7590a60", - "alias" : "registration", - "description" : "Registration flow", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "registration-page-form", - "authenticatorFlow" : true, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : true, - "flowAlias" : "registration form", - "userSetupAllowed" : false - } ] - }, { - "id" : "336780bc-8dad-4fab-afb1-6fceb4b61ddf", - "alias" : "registration form", - "description" : "Registration form", - "providerId" : "form-flow", - "topLevel" : false, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "registration-user-creation", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "registration-password-action", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 50, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "registration-recaptcha-action", - "authenticatorFlow" : false, - "requirement" : "DISABLED", - "priority" : 60, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "registration-terms-and-conditions", - "authenticatorFlow" : false, - "requirement" : "DISABLED", - "priority" : 70, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - }, { - "id" : "de415055-a2d6-4318-853b-f3b7789bd3db", - "alias" : "reset credentials", - "description" : "Reset credentials for a user if they forgot their password or something", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "reset-credentials-choose-user", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "reset-credential-email", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 20, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticator" : "reset-password", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 30, - "autheticatorFlow" : false, - "userSetupAllowed" : false - }, { - "authenticatorFlow" : true, - "requirement" : "CONDITIONAL", - "priority" : 40, - "autheticatorFlow" : true, - "flowAlias" : "Reset - Conditional OTP", - "userSetupAllowed" : false - } ] - }, { - "id" : "80c9305b-4d24-4e26-bb97-23073d551228", - "alias" : "saml ecp", - "description" : "SAML ECP Profile Authentication Flow", - "providerId" : "basic-flow", - "topLevel" : true, - "builtIn" : true, - "authenticationExecutions" : [ { - "authenticator" : "http-basic-authenticator", - "authenticatorFlow" : false, - "requirement" : "REQUIRED", - "priority" : 10, - "autheticatorFlow" : false, - "userSetupAllowed" : false - } ] - } ], - "authenticatorConfig" : [ { - "id" : "b88fa7f2-e24a-4168-b00b-bc1b8d4b4ef1", - "alias" : "create unique user config", - "config" : { - "require.password.update.after.registration" : "false" - } - }, { - "id" : "8c26db6a-22ae-4a6b-8544-e74525911ce0", - "alias" : "review profile config", - "config" : { - "update.profile.on.first.login" : "missing" - } - } ], - "requiredActions" : [ { - "alias" : "CONFIGURE_TOTP", - "name" : "Configure OTP", - "providerId" : "CONFIGURE_TOTP", - "enabled" : true, - "defaultAction" : false, - "priority" : 10, - "config" : { } - }, { - "alias" : "TERMS_AND_CONDITIONS", - "name" : "Terms and Conditions", - "providerId" : "TERMS_AND_CONDITIONS", - "enabled" : false, - "defaultAction" : false, - "priority" : 20, - "config" : { } - }, { - "alias" : "UPDATE_PASSWORD", - "name" : "Update Password", - "providerId" : "UPDATE_PASSWORD", - "enabled" : true, - "defaultAction" : false, - "priority" : 30, - "config" : { } - }, { - "alias" : "UPDATE_PROFILE", - "name" : "Update Profile", - "providerId" : "UPDATE_PROFILE", - "enabled" : true, - "defaultAction" : false, - "priority" : 40, - "config" : { } - }, { - "alias" : "VERIFY_EMAIL", - "name" : "Verify Email", - "providerId" : "VERIFY_EMAIL", - "enabled" : true, - "defaultAction" : false, - "priority" : 50, - "config" : { } - }, { - "alias" : "delete_account", - "name" : "Delete Account", - "providerId" : "delete_account", - "enabled" : false, - "defaultAction" : false, - "priority" : 60, - "config" : { } - }, { - "alias" : "webauthn-register", - "name" : "Webauthn Register", - "providerId" : "webauthn-register", - "enabled" : true, - "defaultAction" : false, - "priority" : 70, - "config" : { } - }, { - "alias" : "webauthn-register-passwordless", - "name" : "Webauthn Register Passwordless", - "providerId" : "webauthn-register-passwordless", - "enabled" : true, - "defaultAction" : false, - "priority" : 80, - "config" : { } - }, { - "alias" : "VERIFY_PROFILE", - "name" : "Verify Profile", - "providerId" : "VERIFY_PROFILE", - "enabled" : true, - "defaultAction" : false, - "priority" : 90, - "config" : { } - }, { - "alias" : "delete_credential", - "name" : "Delete Credential", - "providerId" : "delete_credential", - "enabled" : true, - "defaultAction" : false, - "priority" : 100, - "config" : { } - }, { - "alias" : "update_user_locale", - "name" : "Update User Locale", - "providerId" : "update_user_locale", - "enabled" : true, - "defaultAction" : false, - "priority" : 1000, - "config" : { } - } ], - "browserFlow" : "browser", - "registrationFlow" : "registration", - "directGrantFlow" : "direct grant", - "resetCredentialsFlow" : "reset credentials", - "clientAuthenticationFlow" : "clients", - "dockerAuthenticationFlow" : "docker auth", - "firstBrokerLoginFlow" : "first broker login", - "attributes" : { - "cibaBackchannelTokenDeliveryMode" : "poll", - "cibaExpiresIn" : "120", - "cibaAuthRequestedUserHint" : "login_hint", - "oauth2DeviceCodeLifespan" : "600", - "clientOfflineSessionMaxLifespan" : "0", - "oauth2DevicePollingInterval" : "5", - "clientSessionIdleTimeout" : "0", - "parRequestUriLifespan" : "60", - "clientSessionMaxLifespan" : "0", - "clientOfflineSessionIdleTimeout" : "0", - "cibaInterval" : "5", - "realmReusableOtpCode" : "false" - }, - "keycloakVersion" : "26.1.1", - "userManagedAccessAllowed" : false, - "organizationsEnabled" : false, - "verifiableCredentialsEnabled" : false, - "adminPermissionsEnabled" : false, - "clientProfiles" : { - "profiles" : [ ] - }, - "clientPolicies" : { - "policies" : [ ] - } -} \ No newline at end of file diff --git a/keycloak-data/import/emumet-users-0.json b/keycloak-data/import/emumet-users-0.json deleted file mode 100644 index 6200e61..0000000 --- a/keycloak-data/import/emumet-users-0.json +++ /dev/null @@ -1,27 +0,0 @@ -{ - "realm" : "emumet", - "users" : [ { - "id" : "0e2cb7da-8a19-4343-87e3-082b9032a652", - "username" : "testuser", - "firstName" : "test", - "lastName" : "user", - "email" : "testuser@example.com", - "emailVerified" : false, - "createdTimestamp" : 1738895015441, - "enabled" : true, - "totp" : false, - "credentials" : [ { - "id" : "3ae9520c-e3c9-43a6-b5ad-36760a384498", - "type" : "password", - "userLabel" : "My password", - "createdDate" : 1738895132371, - "secretData" : "{\"value\":\"HQ/zyli30BXNZ9a9M/6FZKhjn60z7DlEwgxkN7mroUU=\",\"salt\":\"Tk1Fi0u2kPuLiME14cA33Q==\",\"additionalParameters\":{}}", - "credentialData" : "{\"hashIterations\":5,\"algorithm\":\"argon2\",\"additionalParameters\":{\"hashLength\":[\"32\"],\"memory\":[\"7168\"],\"type\":[\"id\"],\"version\":[\"1.3\"],\"parallelism\":[\"1\"]}}" - } ], - "disableableCredentialTypes" : [ ], - "requiredActions" : [ ], - "realmRoles" : [ "default-roles-emumet" ], - "notBefore" : 0, - "groups" : [ ] - } ] -} \ No newline at end of file diff --git a/ory/hydra/hydra.yml b/ory/hydra/hydra.yml new file mode 100644 index 0000000..cb224c2 --- /dev/null +++ b/ory/hydra/hydra.yml @@ -0,0 +1,31 @@ +serve: + public: + port: 4444 + admin: + port: 4445 + +urls: + self: + issuer: http://localhost:4444/ + login: http://localhost:8080/oauth2/login + consent: http://localhost:8080/oauth2/consent + post_logout_redirect: http://localhost:3000/ + +secrets: + system: + - "dev-secret-do-not-use-in-production-32ch" + +strategies: + access_token: jwt + +oidc: + subject_identifiers: + supported_types: + - public + +log: + level: debug + +ttl: + access_token: 1h + refresh_token: 720h diff --git a/ory/kratos/identity.schema.json b/ory/kratos/identity.schema.json new file mode 100644 index 0000000..2cc2952 --- /dev/null +++ b/ory/kratos/identity.schema.json @@ -0,0 +1,28 @@ +{ + "$id": "https://emumet.shuttlepub.example/identity.schema.json", + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "Identity", + "type": "object", + "properties": { + "traits": { + "type": "object", + "properties": { + "email": { + "type": "string", + "format": "email", + "title": "Email", + "ory.sh/kratos": { + "credentials": { + "password": { + "identifier": true + } + } + } + } + }, + "required": ["email"], + "additionalProperties": false + } + }, + "additionalProperties": false +} diff --git a/ory/kratos/kratos.yml b/ory/kratos/kratos.yml new file mode 100644 index 0000000..d2ba802 --- /dev/null +++ b/ory/kratos/kratos.yml @@ -0,0 +1,33 @@ +serve: + public: + base_url: http://localhost:4433/ + admin: + base_url: http://localhost:4434/ + +selfservice: + default_browser_return_url: http://localhost:3000/ + + flows: + registration: + enabled: true + ui_url: http://localhost:3000/registration + + login: + ui_url: http://localhost:3000/login + + methods: + password: + enabled: true + +identity: + default_schema_id: default + schemas: + - id: default + url: file:///etc/config/kratos/identity.schema.json + +log: + level: debug + +courier: + smtp: + connection_uri: smtp://localhost:1025/?skip_ssl_verify=true diff --git a/ory/kratos/seed-users.json b/ory/kratos/seed-users.json new file mode 100644 index 0000000..0b4a467 --- /dev/null +++ b/ory/kratos/seed-users.json @@ -0,0 +1,14 @@ +{ + "schema_id": "default", + "state": "active", + "traits": { + "email": "testuser@example.com" + }, + "credentials": { + "password": { + "config": { + "password": "testuser" + } + } + } +} diff --git a/server/Cargo.toml b/server/Cargo.toml index 24bf60b..675d5e0 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -9,9 +9,6 @@ authors.workspace = true [dependencies] time = { workspace = true } uuid = { workspace = true, features = ["v4"] } -rand = "0.8" -rand_chacha = "0.3" -sha2 = "0.10" url = { version = "2.5.4", features = [] } dotenvy = { workspace = true} @@ -20,11 +17,14 @@ tracing-appender = "0.2.3" tracing-subscriber = { workspace = true } axum = { version = "0.7.4", features = ["json", "tracing"] } -axum-extra = { version = "0.9.2", features = ["typed-header", "query"] } -axum-keycloak-auth = "0.6.0" +tower = { version = "0.4", features = ["util"] } tower-http = { version = "0.5.1", features = ["tokio", "cors"] } tokio = { workspace = true, features = ["rt", "rt-multi-thread", "macros"] } +jsonwebtoken = "9" +reqwest = { version = "0.12", features = ["json"] } +serde_json = "1" + rikka-mq = { version = "0.1.3", features = ["redis", "tracing"] } error-stack = { workspace = true } @@ -36,4 +36,8 @@ adapter = { path = "../adapter" } application = { path = "../application" } driver = { path = "../driver" } kernel = { path = "../kernel" } -log = "0.4.22" + +[dev-dependencies] +rand = "0.8" +rsa = { version = "0.9" } +base64 = "0.22" diff --git a/server/src/auth.rs b/server/src/auth.rs new file mode 100644 index 0000000..b6dec2d --- /dev/null +++ b/server/src/auth.rs @@ -0,0 +1,645 @@ +use axum::body::Body; +use axum::extract::State; +use axum::http::{Request, StatusCode}; +use axum::middleware::Next; +use axum::response::Response; +use jsonwebtoken::jwk::KeyAlgorithm; +use jsonwebtoken::{decode, decode_header, Algorithm, DecodingKey, Validation}; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use std::time::{Duration, Instant}; +use tokio::sync::{Mutex, RwLock}; + +// --------------------------------------------------------------------------- +// OidcConfig +// --------------------------------------------------------------------------- + +pub struct OidcConfig { + pub issuer_url: String, + pub expected_audience: String, + /// Minimum interval between JWKS re-fetches. Set to 0 in tests. + pub jwks_refetch_interval_secs: u64, +} + +impl OidcConfig { + /// Initialize from environment variables `HYDRA_ISSUER_URL` and `EXPECTED_AUDIENCE`. + pub fn from_env() -> Self { + let issuer_url = dotenvy::var("HYDRA_ISSUER_URL").unwrap_or_else(|_| { + let default = "http://localhost:4444".to_string(); + tracing::warn!("HYDRA_ISSUER_URL not set, using default: {default}"); + default + }); + let expected_audience = dotenvy::var("EXPECTED_AUDIENCE").unwrap_or_else(|_| { + let default = "emumet".to_string(); + tracing::warn!("EXPECTED_AUDIENCE not set, using default: {default}"); + default + }); + Self { + issuer_url, + expected_audience, + jwks_refetch_interval_secs: 300, + } + } +} + +// --------------------------------------------------------------------------- +// AuthClaims +// --------------------------------------------------------------------------- + +/// JWT claims issued by Hydra. +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +pub struct AuthClaims { + pub iss: String, + pub sub: String, + /// `aud` may be a single string or an array of strings. + pub aud: OneOrMany, + pub exp: u64, +} + +/// Represents a JSON value that can be either a single string or a list of strings. +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq)] +#[serde(untagged)] +pub enum OneOrMany { + One(String), + Many(Vec), +} + +impl OneOrMany { + pub fn contains(&self, value: &str) -> bool { + match self { + OneOrMany::One(s) => s == value, + OneOrMany::Many(v) => v.iter().any(|s| s == value), + } + } +} + +// --------------------------------------------------------------------------- +// OidcAuthInfo +// --------------------------------------------------------------------------- + +/// Extracted auth info consumed by `resolve_auth_account_id` (task 5.1). +pub struct OidcAuthInfo { + /// Hydra issuer URL → used as `AuthHost.url` + pub issuer: String, + /// Kratos identity UUID → used as `AuthAccount.client_id` + pub subject: String, +} + +impl From for OidcAuthInfo { + fn from(claims: AuthClaims) -> Self { + Self { + issuer: claims.iss, + subject: claims.sub, + } + } +} + +// --------------------------------------------------------------------------- +// OIDC Discovery response +// --------------------------------------------------------------------------- + +#[derive(Deserialize)] +struct OidcDiscovery { + jwks_uri: String, +} + +// --------------------------------------------------------------------------- +// JwkSet wrapper (re-export from jsonwebtoken) +// --------------------------------------------------------------------------- + +pub use jsonwebtoken::jwk::JwkSet; + +// --------------------------------------------------------------------------- +// JwksCache +// --------------------------------------------------------------------------- + +struct JwksCacheInner { + jwks: Option, + jwks_uri: Option, + last_fetch: Instant, +} + +pub struct JwksCache { + inner: RwLock, + /// Serialises refresh attempts to prevent thundering herd. + refresh_mutex: Mutex<()>, + issuer_url: String, + http_client: reqwest::Client, + min_refetch_interval: Duration, +} + +impl JwksCache { + pub fn new(issuer_url: String, min_refetch_interval: Duration) -> Self { + let initial_last_fetch = Instant::now() + .checked_sub(min_refetch_interval + Duration::from_secs(1)) + .unwrap_or_else(Instant::now); + + Self { + inner: RwLock::new(JwksCacheInner { + jwks: None, + jwks_uri: None, + last_fetch: initial_last_fetch, + }), + refresh_mutex: Mutex::new(()), + issuer_url, + http_client: reqwest::Client::new(), + min_refetch_interval, + } + } + + /// Construct a `JwksCache` with a pre-populated `JwkSet` (test helper). + /// The refetch interval is set to zero so re-fetches are always eligible. + pub fn new_with_jwks(issuer_url: String, jwks: JwkSet) -> Self { + let past = Instant::now() + .checked_sub(Duration::from_secs(1)) + .unwrap_or_else(Instant::now); + Self { + inner: RwLock::new(JwksCacheInner { + jwks: Some(jwks), + jwks_uri: None, + last_fetch: past, + }), + refresh_mutex: Mutex::new(()), + issuer_url, + http_client: reqwest::Client::new(), + min_refetch_interval: Duration::from_secs(0), + } + } + + /// Attempt to initialise the cache by performing OIDC Discovery and fetching + /// the JWKS. Failures are logged but do not panic (lazy initialisation). + pub async fn try_init(&self) { + let discovery_url = format!( + "{}/.well-known/openid-configuration", + self.issuer_url.trim_end_matches('/') + ); + + let discovery: OidcDiscovery = match self.http_client.get(&discovery_url).send().await { + Ok(resp) => match resp.json().await { + Ok(d) => d, + Err(e) => { + tracing::warn!("JwksCache: failed to parse OIDC discovery: {e}"); + return; + } + }, + Err(e) => { + tracing::warn!("JwksCache: OIDC discovery request failed ({discovery_url}): {e}"); + return; + } + }; + + let jwks_uri = discovery.jwks_uri.clone(); + self.fetch_jwks_inner(&jwks_uri).await; + // Update jwks_uri after successful discovery. + self.inner.write().await.jwks_uri = Some(jwks_uri); + } + + /// Fetch JWKS from the given URI and update the cache atomically. + async fn fetch_jwks_inner(&self, jwks_uri: &str) { + match self.http_client.get(jwks_uri).send().await { + Ok(resp) => match resp.json::().await { + Ok(jwks) => { + let mut inner = self.inner.write().await; + inner.jwks = Some(jwks); + inner.last_fetch = Instant::now(); + tracing::info!("JwksCache: JWKS refreshed from {jwks_uri}"); + } + Err(e) => { + tracing::warn!("JwksCache: failed to parse JWKS response: {e}"); + } + }, + Err(e) => { + tracing::warn!("JwksCache: JWKS fetch failed ({jwks_uri}): {e}"); + } + } + } + + /// Look up a decoding key by `kid`. + /// + /// If the key is not found **and** the minimum refetch interval has elapsed, + /// the JWKS is re-fetched once before returning. A mutex ensures only one + /// concurrent refresh. + pub async fn get_key(&self, kid: &str) -> Option { + // First attempt: check current cache. + if let Some(key) = self.key_from_cache(kid).await { + return Some(key); + } + + // Acquire refresh mutex to prevent thundering herd. + let _guard = self.refresh_mutex.lock().await; + + // Re-check cache: another task may have refreshed while we waited. + if let Some(key) = self.key_from_cache(kid).await { + return Some(key); + } + + // Check if we are allowed to re-fetch. + let elapsed = self.inner.read().await.last_fetch.elapsed(); + + if elapsed >= self.min_refetch_interval { + tracing::info!("JwksCache: kid '{kid}' not found – re-fetching JWKS"); + + let jwks_uri = self.inner.read().await.jwks_uri.clone(); + + if let Some(uri) = jwks_uri { + self.fetch_jwks_inner(&uri).await; + } else { + self.try_init().await; + } + + // Second attempt after re-fetch. + return self.key_from_cache(kid).await; + } + + None + } + + /// Extract a `DecodingKey` for the given `kid` from the current in-memory cache. + /// Rejects JWKs whose `alg` field does not match RS256. + async fn key_from_cache(&self, kid: &str) -> Option { + let inner = self.inner.read().await; + let jwks = inner.jwks.as_ref()?; + let jwk = jwks.find(kid)?; + if let Some(alg) = &jwk.common.key_algorithm { + if *alg != KeyAlgorithm::RS256 { + tracing::warn!("JwksCache: JWK kid={kid} has unexpected algorithm {alg:?}"); + return None; + } + } + DecodingKey::from_jwk(jwk).ok() + } +} + +// --------------------------------------------------------------------------- +// auth_middleware (axum middleware layer) +// --------------------------------------------------------------------------- + +/// Axum middleware that validates Bearer JWTs and inserts +/// [`Extension`] into the request. +/// +/// Usage with router: +/// ```ignore +/// let state = (config, jwks_cache); +/// router.layer(axum::middleware::from_fn_with_state(state, auth_middleware)) +/// ``` +pub async fn auth_middleware( + State((config, jwks_cache)): State<(Arc, Arc)>, + mut request: Request, + next: Next, +) -> Result { + auth_middleware_core(config, jwks_cache, &mut request).await?; + Ok(next.run(request).await) +} + +/// Core middleware logic: extracts and validates the Bearer token, then +/// inserts `Extension` into the request extensions. +async fn auth_middleware_core( + config: Arc, + jwks_cache: Arc, + request: &mut Request, +) -> Result<(), StatusCode> { + // 1. Extract the Bearer token from the Authorization header. + let token = extract_bearer_token(request)?; + + // 2. Decode the JWT header to obtain the `kid`. + let header = decode_header(token).map_err(|e| { + tracing::warn!("auth_middleware: failed to decode JWT header: {e}"); + StatusCode::UNAUTHORIZED + })?; + + let kid = header.kid.ok_or_else(|| { + tracing::warn!("auth_middleware: JWT header missing 'kid'"); + StatusCode::UNAUTHORIZED + })?; + + // 3. Fetch the public key from JWKS cache. + let decoding_key = jwks_cache.get_key(&kid).await.ok_or_else(|| { + tracing::warn!("auth_middleware: no JWKS key found for kid='{kid}'"); + StatusCode::UNAUTHORIZED + })?; + + // 4. Validate the JWT (RS256, iss, aud, exp). + let mut validation = Validation::new(Algorithm::RS256); + validation.set_audience(&[&config.expected_audience]); + validation.set_issuer(&[&config.issuer_url]); + + let token_data = decode::(token, &decoding_key, &validation).map_err(|e| { + tracing::warn!("auth_middleware: JWT validation failed: {e}"); + StatusCode::UNAUTHORIZED + })?; + + // 5. Insert claims as request extension. + request.extensions_mut().insert(token_data.claims); + + Ok(()) +} + +// --------------------------------------------------------------------------- +// resolve_auth_account_id +// --------------------------------------------------------------------------- + +use crate::handler::AppModule; +use adapter::processor::auth_account::{ + AuthAccountCommandProcessor, AuthAccountQueryProcessor, DependOnAuthAccountCommandProcessor, + DependOnAuthAccountQueryProcessor, +}; +use kernel::interfaces::database::{DatabaseConnection, DependOnDatabaseConnection}; +use kernel::interfaces::repository::{AuthHostRepository, DependOnAuthHostRepository}; +use kernel::prelude::entity::{ + AuthAccountClientId, AuthAccountId, AuthHost, AuthHostId, AuthHostUrl, +}; +use kernel::KernelError; + +pub async fn resolve_auth_account_id( + app: &AppModule, + auth_info: OidcAuthInfo, +) -> error_stack::Result { + let client_id = AuthAccountClientId::new(auth_info.subject); + let mut executor = app.database_connection().begin_transaction().await?; + let auth_account = app + .auth_account_query_processor() + .find_by_client_id(&mut executor, &client_id) + .await?; + let auth_account = if let Some(auth_account) = auth_account { + auth_account + } else { + let url = AuthHostUrl::new(auth_info.issuer); + let auth_host = app + .auth_host_repository() + .find_by_url(&mut executor, &url) + .await?; + let auth_host = if let Some(auth_host) = auth_host { + auth_host + } else { + let auth_host = AuthHost::new(AuthHostId::default(), url); + app.auth_host_repository() + .create(&mut executor, &auth_host) + .await?; + auth_host + }; + let host_id = auth_host.into_destruct().id; + app.auth_account_command_processor() + .create(&mut executor, host_id, client_id) + .await? + }; + Ok(auth_account.id().clone()) +} + +/// Extract the raw Bearer token string from the `Authorization` header. +fn extract_bearer_token<'a>(request: &'a Request) -> Result<&'a str, StatusCode> { + let header_value = request + .headers() + .get(axum::http::header::AUTHORIZATION) + .ok_or_else(|| { + tracing::warn!("auth_middleware: missing Authorization header"); + StatusCode::UNAUTHORIZED + })?; + + let header_str = header_value.to_str().map_err(|_| { + tracing::warn!("auth_middleware: Authorization header is not valid UTF-8"); + StatusCode::UNAUTHORIZED + })?; + + let token = header_str.strip_prefix("Bearer ").ok_or_else(|| { + tracing::warn!("auth_middleware: Authorization header does not start with 'Bearer '"); + StatusCode::UNAUTHORIZED + })?; + + Ok(token) +} + +// --------------------------------------------------------------------------- +// Tests +// --------------------------------------------------------------------------- + +#[cfg(test)] +mod tests { + use super::*; + use jsonwebtoken::jwk::{ + AlgorithmParameters, CommonParameters, Jwk, JwkSet, KeyAlgorithm, PublicKeyUse, + RSAKeyParameters, + }; + use jsonwebtoken::{encode, EncodingKey, Header}; + use rsa::pkcs1::EncodeRsaPrivateKey; + use rsa::RsaPrivateKey; + use std::time::{SystemTime, UNIX_EPOCH}; + + // ----------------------------------------------------------------------- + // Test helpers + // ----------------------------------------------------------------------- + + fn unix_now() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .unwrap() + .as_secs() + } + + struct TestKeys { + encoding_key: EncodingKey, + jwk_set: JwkSet, + kid: String, + } + + /// Generate a fresh 2048-bit RSA key pair and wrap it as a `JwkSet`. + fn generate_test_keys() -> TestKeys { + use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine}; + use rsa::traits::PublicKeyParts; + + let mut rng = rand::thread_rng(); + let private_key = RsaPrivateKey::new(&mut rng, 2048).expect("generate RSA key"); + + // PEM → EncodingKey + let pem = private_key + .to_pkcs1_pem(rsa::pkcs8::LineEnding::LF) + .expect("encode pkcs1 pem"); + let encoding_key = + EncodingKey::from_rsa_pem(pem.as_bytes()).expect("parse EncodingKey from PEM"); + + // Build JWK from RSA public key components. + let pub_key = private_key.to_public_key(); + let n = URL_SAFE_NO_PAD.encode(pub_key.n().to_bytes_be()); + let e = URL_SAFE_NO_PAD.encode(pub_key.e().to_bytes_be()); + + let kid = "test-key-1".to_string(); + let jwk = Jwk { + common: CommonParameters { + public_key_use: Some(PublicKeyUse::Signature), + key_id: Some(kid.clone()), + key_algorithm: Some(KeyAlgorithm::RS256), + ..Default::default() + }, + algorithm: AlgorithmParameters::RSA(RSAKeyParameters { + n, + e, + ..Default::default() + }), + }; + let jwk_set = JwkSet { keys: vec![jwk] }; + + TestKeys { + encoding_key, + jwk_set, + kid, + } + } + + fn make_claims(iss: &str, aud: &str, sub: &str, exp_offset_secs: i64) -> AuthClaims { + let exp = (unix_now() as i64 + exp_offset_secs) as u64; + AuthClaims { + iss: iss.to_string(), + sub: sub.to_string(), + aud: OneOrMany::One(aud.to_string()), + exp, + } + } + + fn encode_jwt(claims: &AuthClaims, encoding_key: &EncodingKey, kid: &str) -> String { + let mut header = Header::new(Algorithm::RS256); + header.kid = Some(kid.to_string()); + encode(&header, claims, encoding_key).expect("encode JWT") + } + + fn make_config(issuer: &str, audience: &str) -> Arc { + Arc::new(OidcConfig { + issuer_url: issuer.to_string(), + expected_audience: audience.to_string(), + jwks_refetch_interval_secs: 0, + }) + } + + async fn validate( + config: Arc, + cache: Arc, + token: &str, + ) -> Result { + let mut req: Request = Request::builder() + .header("Authorization", format!("Bearer {token}")) + .body(Body::empty()) + .unwrap(); + auth_middleware_core(config, cache, &mut req).await?; + Ok(req.extensions().get::().unwrap().clone()) + } + + // ----------------------------------------------------------------------- + // Test cases + // ----------------------------------------------------------------------- + + #[tokio::test] + async fn valid_jwt_succeeds() { + let keys = generate_test_keys(); + let issuer = "https://hydra.example.com"; + let audience = "emumet"; + let config = make_config(issuer, audience); + let cache = Arc::new(JwksCache::new_with_jwks(issuer.to_string(), keys.jwk_set)); + + let claims = make_claims(issuer, audience, "kratos-uuid-123", 3600); + let token = encode_jwt(&claims, &keys.encoding_key, &keys.kid); + + let result = validate(config, cache, &token).await; + assert!(result.is_ok(), "expected Ok, got {result:?}"); + let decoded = result.unwrap(); + assert_eq!(decoded.sub, "kratos-uuid-123"); + assert_eq!(decoded.iss, issuer); + } + + #[tokio::test] + async fn expired_jwt_fails() { + let keys = generate_test_keys(); + let issuer = "https://hydra.example.com"; + let audience = "emumet"; + let config = make_config(issuer, audience); + let cache = Arc::new(JwksCache::new_with_jwks(issuer.to_string(), keys.jwk_set)); + + // exp well in the past (beyond default 60s leeway) + let claims = make_claims(issuer, audience, "sub", -120); + let token = encode_jwt(&claims, &keys.encoding_key, &keys.kid); + + let result = validate(config, cache, &token).await; + assert_eq!(result, Err(StatusCode::UNAUTHORIZED)); + } + + #[tokio::test] + async fn wrong_audience_fails() { + let keys = generate_test_keys(); + let issuer = "https://hydra.example.com"; + let config = make_config(issuer, "emumet"); + let cache = Arc::new(JwksCache::new_with_jwks(issuer.to_string(), keys.jwk_set)); + + let claims = make_claims(issuer, "wrong-audience", "sub", 3600); + let token = encode_jwt(&claims, &keys.encoding_key, &keys.kid); + + let result = validate(config, cache, &token).await; + assert_eq!(result, Err(StatusCode::UNAUTHORIZED)); + } + + #[tokio::test] + async fn wrong_issuer_fails() { + let keys = generate_test_keys(); + let issuer = "https://hydra.example.com"; + let audience = "emumet"; + let config = make_config(issuer, audience); + let cache = Arc::new(JwksCache::new_with_jwks(issuer.to_string(), keys.jwk_set)); + + let claims = make_claims("https://evil-issuer.example.com", audience, "sub", 3600); + let token = encode_jwt(&claims, &keys.encoding_key, &keys.kid); + + let result = validate(config, cache, &token).await; + assert_eq!(result, Err(StatusCode::UNAUTHORIZED)); + } + + #[tokio::test] + async fn missing_authorization_header_fails() { + let keys = generate_test_keys(); + let issuer = "https://hydra.example.com"; + let config = make_config(issuer, "emumet"); + let cache = Arc::new(JwksCache::new_with_jwks(issuer.to_string(), keys.jwk_set)); + + let mut req: Request = Request::builder().body(Body::empty()).unwrap(); + let result = auth_middleware_core(config, cache, &mut req).await; + assert_eq!(result, Err(StatusCode::UNAUTHORIZED)); + } + + #[tokio::test] + async fn wrong_signing_key_fails() { + let keys = generate_test_keys(); + let wrong_keys = generate_test_keys(); // different RSA key pair + let issuer = "https://hydra.example.com"; + let audience = "emumet"; + let config = make_config(issuer, audience); + // Cache has `keys.jwk_set`, but token is signed with `wrong_keys` + let cache = Arc::new(JwksCache::new_with_jwks(issuer.to_string(), keys.jwk_set)); + + let claims = make_claims(issuer, audience, "sub", 3600); + // Sign with wrong key but use the kid from the cached keyset + let token = encode_jwt(&claims, &wrong_keys.encoding_key, &keys.kid); + + let result = validate(config, cache, &token).await; + assert_eq!(result, Err(StatusCode::UNAUTHORIZED)); + } + + #[tokio::test] + async fn oidc_auth_info_from_claims() { + let claims = AuthClaims { + iss: "https://hydra.example.com".to_string(), + sub: "kratos-uuid-abc".to_string(), + aud: OneOrMany::One("emumet".to_string()), + exp: unix_now() + 3600, + }; + let info: OidcAuthInfo = claims.into(); + assert_eq!(info.issuer, "https://hydra.example.com"); + assert_eq!(info.subject, "kratos-uuid-abc"); + } + + #[test] + fn one_or_many_variants() { + let one = OneOrMany::One("emumet".to_string()); + assert!(one.contains("emumet")); + assert!(!one.contains("other")); + + let many = OneOrMany::Many(vec!["emumet".to_string(), "other".to_string()]); + assert!(many.contains("emumet")); + assert!(many.contains("other")); + assert!(!many.contains("missing")); + } +} diff --git a/server/src/handler.rs b/server/src/handler.rs index 1e8fc61..6dd9873 100644 --- a/server/src/handler.rs +++ b/server/src/handler.rs @@ -1,4 +1,6 @@ use crate::applier::ApplierContainer; +use crate::hydra::HydraAdminClient; +use crate::kratos::KratosClient; use adapter::processor::account::DependOnAccountSignal; use adapter::processor::auth_account::DependOnAuthAccountSignal; use adapter::processor::metadata::DependOnMetadataSignal; @@ -31,6 +33,14 @@ impl AppModule { applier_container, }) } + + pub fn hydra_admin_client(&self) -> &HydraAdminClient { + &self.handler.hydra_admin_client + } + + pub fn kratos_client(&self) -> &KratosClient { + &self.handler.kratos_client + } } // --- DependOn* implementations for AppModule (delegate to handler/applier_container) --- @@ -217,6 +227,9 @@ pub struct Handler { key_encryptor: Argon2Encryptor, signer: Rsa2048Signer, verifier: Rsa2048Verifier, + // Ory clients + pub(crate) hydra_admin_client: HydraAdminClient, + pub(crate) kratos_client: KratosClient, } impl Handler { @@ -224,6 +237,11 @@ impl Handler { let pgpool = PostgresDatabase::new().await?; let redis = RedisDatabase::new()?; + let hydra_admin_url = + dotenvy::var("HYDRA_ADMIN_URL").unwrap_or_else(|_| "http://localhost:4445".to_string()); + let kratos_public_url = dotenvy::var("KRATOS_PUBLIC_URL") + .unwrap_or_else(|_| "http://localhost:4433".to_string()); + Ok(Self { pgpool, redis, @@ -232,6 +250,8 @@ impl Handler { key_encryptor: Argon2Encryptor::default(), signer: Rsa2048Signer, verifier: Rsa2048Verifier, + hydra_admin_client: HydraAdminClient::new(hydra_admin_url), + kratos_client: KratosClient::new(kratos_public_url), }) } } diff --git a/server/src/hydra.rs b/server/src/hydra.rs new file mode 100644 index 0000000..e9bdd3b --- /dev/null +++ b/server/src/hydra.rs @@ -0,0 +1,212 @@ +use reqwest::Client; +use serde::{Deserialize, Serialize}; +use url::Url; + +pub struct HydraAdminClient { + admin_url: String, + http_client: Client, +} + +impl HydraAdminClient { + /// Create a new HydraAdminClient. Panics if `admin_url` is not a valid URL. + pub fn new(admin_url: String) -> Self { + let admin_url = admin_url.trim_end_matches('/').to_string(); + // Validate URL at construction time to fail fast. + Url::parse(&admin_url) + .unwrap_or_else(|e| panic!("HYDRA_ADMIN_URL is not a valid URL ({admin_url}): {e}")); + Self { + admin_url, + http_client: Client::new(), + } + } + + /// Build a URL with a properly encoded challenge query parameter. + /// The base URL was validated in `new()`, so `parse_with_params` will not fail. + fn build_url(&self, path: &str, param_name: &str, challenge: &str) -> Url { + let base = format!("{}{}", self.admin_url, path); + Url::parse_with_params(&base, &[(param_name, challenge)]) + .expect("base URL was validated at construction time") + } + + pub async fn get_login_request(&self, challenge: &str) -> Result { + let url = self.build_url( + "/admin/oauth2/auth/requests/login", + "login_challenge", + challenge, + ); + self.http_client + .get(url) + .send() + .await? + .error_for_status()? + .json::() + .await + } + + pub async fn accept_login( + &self, + challenge: &str, + body: &AcceptLoginRequest, + ) -> Result { + let url = self.build_url( + "/admin/oauth2/auth/requests/login/accept", + "login_challenge", + challenge, + ); + self.http_client + .put(url) + .json(body) + .send() + .await? + .error_for_status()? + .json::() + .await + } + + pub async fn reject_login( + &self, + challenge: &str, + body: &RejectRequest, + ) -> Result { + let url = self.build_url( + "/admin/oauth2/auth/requests/login/reject", + "login_challenge", + challenge, + ); + self.http_client + .put(url) + .json(body) + .send() + .await? + .error_for_status()? + .json::() + .await + } + + pub async fn get_consent_request( + &self, + challenge: &str, + ) -> Result { + let url = self.build_url( + "/admin/oauth2/auth/requests/consent", + "consent_challenge", + challenge, + ); + self.http_client + .get(url) + .send() + .await? + .error_for_status()? + .json::() + .await + } + + pub async fn accept_consent( + &self, + challenge: &str, + body: &AcceptConsentRequest, + ) -> Result { + let url = self.build_url( + "/admin/oauth2/auth/requests/consent/accept", + "consent_challenge", + challenge, + ); + self.http_client + .put(url) + .json(body) + .send() + .await? + .error_for_status()? + .json::() + .await + } + + pub async fn reject_consent( + &self, + challenge: &str, + body: &RejectRequest, + ) -> Result { + let url = self.build_url( + "/admin/oauth2/auth/requests/consent/reject", + "consent_challenge", + challenge, + ); + self.http_client + .put(url) + .json(body) + .send() + .await? + .error_for_status()? + .json::() + .await + } +} + +#[derive(Debug, Deserialize)] +pub struct LoginRequest { + pub challenge: String, + pub skip: bool, + pub subject: String, + pub client: Option, + pub requested_scope: Vec, + pub requested_access_token_audience: Vec, + pub request_url: String, +} + +#[derive(Debug, Deserialize)] +pub struct OAuth2Client { + pub client_id: Option, + pub client_name: Option, + pub skip_consent: Option, +} + +#[derive(Debug, Serialize)] +pub struct AcceptLoginRequest { + pub subject: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub remember: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub remember_for: Option, +} + +#[derive(Debug, Deserialize)] +pub struct ConsentRequest { + pub challenge: String, + pub skip: bool, + pub subject: String, + pub client: Option, + pub requested_scope: Vec, + pub requested_access_token_audience: Vec, +} + +#[derive(Debug, Serialize)] +pub struct AcceptConsentRequest { + pub grant_scope: Vec, + pub grant_access_token_audience: Vec, + #[serde(skip_serializing_if = "Option::is_none")] + pub remember: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub remember_for: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub session: Option, +} + +#[derive(Debug, Serialize)] +pub struct ConsentSession { + #[serde(skip_serializing_if = "Option::is_none")] + pub access_token: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub id_token: Option, +} + +#[derive(Debug, Serialize)] +pub struct RejectRequest { + pub error: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub error_description: Option, +} + +#[derive(Debug, Deserialize)] +pub struct RedirectResponse { + pub redirect_to: String, +} diff --git a/server/src/keycloak.rs b/server/src/keycloak.rs deleted file mode 100644 index 1544adc..0000000 --- a/server/src/keycloak.rs +++ /dev/null @@ -1,105 +0,0 @@ -use crate::handler::AppModule; -use adapter::processor::auth_account::{ - AuthAccountCommandProcessor, AuthAccountQueryProcessor, DependOnAuthAccountCommandProcessor, - DependOnAuthAccountQueryProcessor, -}; -use axum_keycloak_auth::decode::KeycloakToken; -use axum_keycloak_auth::instance::{KeycloakAuthInstance, KeycloakConfig}; -use axum_keycloak_auth::role::Role; -use kernel::interfaces::database::{DatabaseConnection, DependOnDatabaseConnection}; -use kernel::interfaces::repository::{AuthHostRepository, DependOnAuthHostRepository}; -use kernel::prelude::entity::{ - AuthAccountClientId, AuthAccountId, AuthHost, AuthHostId, AuthHostUrl, -}; -use kernel::KernelError; -use std::sync::LazyLock; - -const KEYCLOAK_SERVER_KEY: &str = "KEYCLOAK_SERVER"; -static SERVER_URL: LazyLock = LazyLock::new(|| { - dotenvy::var(KEYCLOAK_SERVER_KEY).unwrap_or_else(|_| "http://localhost:18080/".to_string()) -}); - -const KEYCLOAK_REALM_KEY: &str = "KEYCLOAK_REALM"; - -pub fn create_keycloak_instance() -> KeycloakAuthInstance { - let server = &*SERVER_URL; - let realm = dotenvy::var(KEYCLOAK_REALM_KEY).unwrap_or_else(|_| "MyRealm".to_string()); - log::info!("Keycloak info: server={server}, realm={realm}"); - KeycloakAuthInstance::new( - KeycloakConfig::builder() - .server(server.parse().unwrap()) - .realm(realm) - .build(), - ) -} - -#[derive(Debug)] -pub struct KeycloakAuthAccount { - host_url: String, - client_id: String, -} - -impl From> for KeycloakAuthAccount { - fn from(token: KeycloakToken) -> Self { - Self { - host_url: SERVER_URL.clone(), - client_id: token.subject, - } - } -} - -pub async fn resolve_auth_account_id( - app: &AppModule, - auth_info: KeycloakAuthAccount, -) -> error_stack::Result { - let client_id = AuthAccountClientId::new(auth_info.client_id); - let mut executor = app.database_connection().begin_transaction().await?; - let auth_account = app - .auth_account_query_processor() - .find_by_client_id(&mut executor, &client_id) - .await?; - let auth_account = if let Some(auth_account) = auth_account { - auth_account - } else { - let url = AuthHostUrl::new(auth_info.host_url); - let auth_host = app - .auth_host_repository() - .find_by_url(&mut executor, &url) - .await?; - let auth_host = if let Some(auth_host) = auth_host { - auth_host - } else { - let auth_host = AuthHost::new(AuthHostId::default(), url); - app.auth_host_repository() - .create(&mut executor, &auth_host) - .await?; - auth_host - }; - let host_id = auth_host.into_destruct().id; - app.auth_account_command_processor() - .create(&mut executor, host_id, client_id) - .await? - }; - Ok(auth_account.id().clone()) -} - -#[macro_export] -macro_rules! expect_role { - ($token: expr, $uri: expr, $method: expr) => { - let expected_roles = - $crate::route::to_permission_strings(&$uri.to_string(), $method.as_str()); - let role_result = expected_roles - .iter() - .map(|role| axum_keycloak_auth::role::ExpectRoles::expect_roles($token, &[role])) - .collect::>>(); - if !role_result.iter().any(|r| r.is_ok()) { - return Err($crate::error::ErrorStatus::from(( - axum::http::StatusCode::FORBIDDEN, - format!( - "Permission denied: required roles(any) = {:?}", - expected_roles - ), - ))); - } - }; -} diff --git a/server/src/kratos.rs b/server/src/kratos.rs new file mode 100644 index 0000000..ed883a5 --- /dev/null +++ b/server/src/kratos.rs @@ -0,0 +1,70 @@ +use reqwest::Client; +use serde::Deserialize; +use url::Url; + +pub struct KratosClient { + public_url: String, + http_client: Client, +} + +impl KratosClient { + /// Create a new KratosClient. Panics if `public_url` is not a valid URL. + pub fn new(public_url: String) -> Self { + let public_url = public_url.trim_end_matches('/').to_string(); + Url::parse(&public_url) + .unwrap_or_else(|e| panic!("KRATOS_PUBLIC_URL is not a valid URL ({public_url}): {e}")); + Self { + public_url, + http_client: Client::new(), + } + } + + /// Kratos の /sessions/whoami エンドポイントを呼び出し、 + /// 有効なセッションがあれば KratosSession を返す。 + /// セッションがない場合(401)は None を返す。 + pub async fn whoami(&self, cookie: &str) -> Result, reqwest::Error> { + let url = format!("{}/sessions/whoami", self.public_url); + tracing::debug!("Calling Kratos whoami: url={url}"); + + let response = self + .http_client + .get(&url) + .header("cookie", cookie) + .send() + .await?; + + let status = response.status(); + tracing::debug!("Kratos whoami response: status={status}"); + + if status == reqwest::StatusCode::UNAUTHORIZED { + return Ok(None); + } + + let session = response.error_for_status()?.json::().await?; + tracing::debug!( + "Kratos whoami session: id={}, active={}", + session.id, + session.active + ); + if !session.active { + tracing::debug!("Kratos whoami: session is not active, treating as unauthenticated"); + return Ok(None); + } + Ok(Some(session)) + } +} + +#[derive(Debug, Clone, Deserialize)] +pub struct KratosSession { + pub id: String, + #[serde(default)] + pub active: bool, + pub identity: KratosIdentity, +} + +#[derive(Debug, Clone, Deserialize)] +pub struct KratosIdentity { + pub id: String, + #[serde(default)] + pub traits: serde_json::Value, +} diff --git a/server/src/main.rs b/server/src/main.rs index ff83d4b..b2a3d5e 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -1,20 +1,24 @@ mod applier; +mod auth; mod error; mod handler; -mod keycloak; +mod hydra; +mod kratos; mod permission; mod route; +use crate::auth::{JwksCache, OidcConfig}; use crate::error::StackTrace; use crate::handler::AppModule; -use crate::keycloak::create_keycloak_instance; use crate::route::account::AccountRouter; use crate::route::metadata::MetadataRouter; +use crate::route::oauth2::OAuth2Router; use crate::route::profile::ProfileRouter; use error_stack::ResultExt; use kernel::KernelError; use std::net::SocketAddr; use std::sync::Arc; +use std::time::Duration; use tokio::net::TcpListener; use tower_http::cors::CorsLayer; use tracing_subscriber::layer::SubscriberExt; @@ -43,13 +47,33 @@ async fn main() -> Result<(), StackTrace> { ) .init(); - let keycloak_auth_instance = Arc::new(create_keycloak_instance()); + // OIDC / JWT auth setup + let oidc_config = OidcConfig::from_env(); + let jwks_cache = Arc::new(JwksCache::new( + oidc_config.issuer_url.clone(), + Duration::from_secs(oidc_config.jwks_refetch_interval_secs), + )); + // Attempt eager JWKS init (non-fatal if Hydra is not yet available). + jwks_cache.try_init().await; + let oidc_config = Arc::new(oidc_config); + let app = AppModule::new().await?; - let router = axum::Router::new() - .route_account(keycloak_auth_instance.clone()) - .route_profile(keycloak_auth_instance.clone()) - .route_metadata(keycloak_auth_instance) + // Routes that require JWT auth + let authed_routes = axum::Router::new() + .route_account() + .route_profile() + .route_metadata() + .layer(axum::middleware::from_fn_with_state( + (oidc_config, jwks_cache), + auth::auth_middleware, + )); + + // Routes that do NOT require JWT auth (OAuth2 Login/Consent Provider) + let public_routes = axum::Router::new().route_oauth2(); + + let router = authed_routes + .merge(public_routes) .layer(CorsLayer::new()) .with_state(app); diff --git a/server/src/route.rs b/server/src/route.rs index 708cf58..4c1e69a 100644 --- a/server/src/route.rs +++ b/server/src/route.rs @@ -4,6 +4,7 @@ use axum::http::StatusCode; pub mod account; pub mod metadata; +pub mod oauth2; pub mod profile; trait DirectionConverter { @@ -21,30 +22,3 @@ impl DirectionConverter for Option { } } } - -/// ("/a/b/c", "GET") -> `["/:GET", "/a:GET", "/a/b:GET", "/a/b/c:GET"]` -pub(super) fn to_permission_strings(path: &str, method: &str) -> Vec { - path.split('/') - .scan(String::new(), |state, part| { - if !state.is_empty() { - state.push('/'); - } - state.push_str(part); - Some(format!("/{state}:{method}")) - }) - .collect::>() -} - -#[cfg(test)] -mod test { - use super::*; - - #[test] - fn test_to_permission_strings() { - let path = "/a/b/c"; - let method = "GET"; - let result = to_permission_strings(path, method); - let expected = vec!["/:GET", "/a:GET", "/a/b:GET", "/a/b/c:GET"]; - assert_eq!(result, expected); - } -} diff --git a/server/src/route/account.rs b/server/src/route/account.rs index 4f62091..81c37a7 100644 --- a/server/src/route/account.rs +++ b/server/src/route/account.rs @@ -1,7 +1,6 @@ +use crate::auth::{resolve_auth_account_id, AuthClaims, OidcAuthInfo}; use crate::error::ErrorStatus; -use crate::expect_role; use crate::handler::AppModule; -use crate::keycloak::{resolve_auth_account_id, KeycloakAuthAccount}; use crate::route::DirectionConverter; use application::service::account::{ CreateAccountUseCase, DeleteAccountUseCase, EditAccountUseCase, GetAccountUseCase, @@ -9,15 +8,9 @@ use application::service::account::{ use application::transfer::pagination::Pagination; use axum::extract::{Path, Query, State}; use axum::http::StatusCode; -use axum::http::{Method, Uri}; use axum::routing::{delete, get, post, put}; use axum::{Extension, Json, Router}; -use axum_keycloak_auth::decode::KeycloakToken; -use axum_keycloak_auth::instance::KeycloakAuthInstance; -use axum_keycloak_auth::layer::KeycloakAuthLayer; -use axum_keycloak_auth::PassthroughMode; use serde::{Deserialize, Serialize}; -use std::sync::Arc; use time::OffsetDateTime; #[derive(Debug, Deserialize)] @@ -55,22 +48,19 @@ struct AccountsResponse { } pub trait AccountRouter { - fn route_account(self, instance: Arc) -> Self; + fn route_account(self) -> Self; } async fn get_accounts( - Extension(token): Extension>, + Extension(claims): Extension, State(module): State, - method: Method, - uri: Uri, Query(GetAllAccountQuery { direction, limit, cursor, }): Query, ) -> Result, ErrorStatus> { - expect_role!(&token, uri, method); - let auth_info = KeycloakAuthAccount::from(token); + let auth_info = OidcAuthInfo::from(claims); let auth_account_id = resolve_auth_account_id(&module, auth_info) .await .map_err(ErrorStatus::from)?; @@ -103,16 +93,12 @@ async fn get_accounts( } async fn create_account( - Extension(token): Extension>, + Extension(claims): Extension, State(module): State, - method: Method, - uri: Uri, Json(request): Json, ) -> Result, ErrorStatus> { - expect_role!(&token, uri, method); - let auth_info = KeycloakAuthAccount::from(token); + let auth_info = OidcAuthInfo::from(claims); - // バリデーション if request.name.trim().is_empty() { return Err(ErrorStatus::from(( StatusCode::BAD_REQUEST, @@ -141,14 +127,11 @@ async fn create_account( } async fn get_account_by_id( - Extension(token): Extension>, + Extension(claims): Extension, State(module): State, - method: Method, - uri: Uri, Path(id): Path, ) -> Result, ErrorStatus> { - expect_role!(&token, uri, method); - let auth_info = KeycloakAuthAccount::from(token); + let auth_info = OidcAuthInfo::from(claims); if id.trim().is_empty() { return Err(ErrorStatus::from(( @@ -178,15 +161,12 @@ async fn get_account_by_id( } async fn update_account_by_id( - Extension(token): Extension>, + Extension(claims): Extension, State(module): State, - method: Method, - uri: Uri, Path(id): Path, Json(request): Json, ) -> Result { - expect_role!(&token, uri, method); - let auth_info = KeycloakAuthAccount::from(token); + let auth_info = OidcAuthInfo::from(claims); if id.trim().is_empty() { return Err(ErrorStatus::from(( @@ -208,14 +188,11 @@ async fn update_account_by_id( } async fn delete_account_by_id( - Extension(token): Extension>, + Extension(claims): Extension, State(module): State, - method: Method, - uri: Uri, Path(id): Path, ) -> Result { - expect_role!(&token, uri, method); - let auth_info = KeycloakAuthAccount::from(token); + let auth_info = OidcAuthInfo::from(claims); if id.trim().is_empty() { return Err(ErrorStatus::from(( @@ -237,18 +214,11 @@ async fn delete_account_by_id( } impl AccountRouter for Router { - fn route_account(self, instance: Arc) -> Self { + fn route_account(self) -> Self { self.route("/accounts", get(get_accounts)) .route("/accounts", post(create_account)) .route("/accounts/:id", get(get_account_by_id)) .route("/accounts/:id", put(update_account_by_id)) .route("/accounts/:id", delete(delete_account_by_id)) - .layer( - KeycloakAuthLayer::::builder() - .instance(instance) - .passthrough_mode(PassthroughMode::Block) - .expected_audiences(vec![String::from("account")]) - .build(), - ) } } diff --git a/server/src/route/metadata.rs b/server/src/route/metadata.rs index 82be9e7..15f896f 100644 --- a/server/src/route/metadata.rs +++ b/server/src/route/metadata.rs @@ -1,21 +1,14 @@ +use crate::auth::{resolve_auth_account_id, AuthClaims, OidcAuthInfo}; use crate::error::ErrorStatus; -use crate::expect_role; use crate::handler::AppModule; -use crate::keycloak::{resolve_auth_account_id, KeycloakAuthAccount}; use application::service::metadata::{ CreateMetadataUseCase, DeleteMetadataUseCase, EditMetadataUseCase, GetMetadataUseCase, }; use axum::extract::{Path, State}; use axum::http::StatusCode; -use axum::http::{Method, Uri}; use axum::routing::{get, put}; use axum::{Extension, Json, Router}; -use axum_keycloak_auth::decode::KeycloakToken; -use axum_keycloak_auth::instance::KeycloakAuthInstance; -use axum_keycloak_auth::layer::KeycloakAuthLayer; -use axum_keycloak_auth::PassthroughMode; use serde::{Deserialize, Serialize}; -use std::sync::Arc; #[derive(Debug, Deserialize)] struct CreateMetadataRequest { @@ -47,18 +40,15 @@ impl From for MetadataResponse { } pub trait MetadataRouter { - fn route_metadata(self, keycloak: Arc) -> Self; + fn route_metadata(self) -> Self; } async fn get_metadata( - Extension(token): Extension>, + Extension(claims): Extension, State(module): State, - method: Method, - uri: Uri, Path(account_id): Path, ) -> Result>, ErrorStatus> { - expect_role!(&token, uri, method); - let auth_info = KeycloakAuthAccount::from(token); + let auth_info = OidcAuthInfo::from(claims); if account_id.trim().is_empty() { return Err(ErrorStatus::from(( @@ -85,15 +75,12 @@ async fn get_metadata( } async fn create_metadata( - Extension(token): Extension>, + Extension(claims): Extension, State(module): State, - method: Method, - uri: Uri, Path(account_id): Path, Json(body): Json, ) -> Result<(StatusCode, Json), ErrorStatus> { - expect_role!(&token, uri, method); - let auth_info = KeycloakAuthAccount::from(token); + let auth_info = OidcAuthInfo::from(claims); if account_id.trim().is_empty() { return Err(ErrorStatus::from(( @@ -115,15 +102,12 @@ async fn create_metadata( } async fn update_metadata( - Extension(token): Extension>, + Extension(claims): Extension, State(module): State, - method: Method, - uri: Uri, Path((account_id, metadata_id)): Path<(String, String)>, Json(body): Json, ) -> Result { - expect_role!(&token, uri, method); - let auth_info = KeycloakAuthAccount::from(token); + let auth_info = OidcAuthInfo::from(claims); if account_id.trim().is_empty() { return Err(ErrorStatus::from(( @@ -158,14 +142,11 @@ async fn update_metadata( } async fn delete_metadata( - Extension(token): Extension>, + Extension(claims): Extension, State(module): State, - method: Method, - uri: Uri, Path((account_id, metadata_id)): Path<(String, String)>, ) -> Result { - expect_role!(&token, uri, method); - let auth_info = KeycloakAuthAccount::from(token); + let auth_info = OidcAuthInfo::from(claims); if account_id.trim().is_empty() { return Err(ErrorStatus::from(( @@ -194,7 +175,7 @@ async fn delete_metadata( } impl MetadataRouter for Router { - fn route_metadata(self, keycloak: Arc) -> Self { + fn route_metadata(self) -> Self { self.route( "/accounts/:account_id/metadata", get(get_metadata).post(create_metadata), @@ -203,13 +184,6 @@ impl MetadataRouter for Router { "/accounts/:account_id/metadata/:id", put(update_metadata).delete(delete_metadata), ) - .layer( - KeycloakAuthLayer::::builder() - .instance(keycloak) - .passthrough_mode(PassthroughMode::Block) - .expected_audiences(vec![String::from("account")]) - .build(), - ) } } diff --git a/server/src/route/oauth2.rs b/server/src/route/oauth2.rs new file mode 100644 index 0000000..5ec6154 --- /dev/null +++ b/server/src/route/oauth2.rs @@ -0,0 +1,319 @@ +use crate::handler::AppModule; +use crate::hydra::{AcceptConsentRequest, AcceptLoginRequest, RejectRequest}; +use crate::kratos::KratosClient; +use axum::extract::{Query, State}; +use axum::http::StatusCode; +use axum::routing::{get, post}; +use axum::{Json, Router}; +use serde::{Deserialize, Serialize}; +use std::collections::HashSet; + +const REMEMBER_FOR_SECS: i64 = 3600; + +// --------------------------------------------------------------------------- +// Query parameters +// --------------------------------------------------------------------------- + +#[derive(Debug, Deserialize)] +struct LoginQuery { + login_challenge: String, +} + +#[derive(Debug, Deserialize)] +struct ConsentQuery { + consent_challenge: String, +} + +// --------------------------------------------------------------------------- +// Response types +// --------------------------------------------------------------------------- + +#[derive(Debug, Serialize)] +#[serde(tag = "action")] +enum OAuth2Response { + #[serde(rename = "redirect")] + Redirect { redirect_to: String }, + #[serde(rename = "show_consent")] + ShowConsent { + consent_challenge: String, + client_name: Option, + requested_scope: Vec, + }, +} + +#[derive(Debug, Deserialize)] +struct ConsentDecision { + consent_challenge: String, + accept: bool, + grant_scope: Option>, +} + +// --------------------------------------------------------------------------- +// GET /oauth2/login +// --------------------------------------------------------------------------- + +async fn login( + State(module): State, + Query(LoginQuery { login_challenge }): Query, + headers: axum::http::HeaderMap, +) -> Result, StatusCode> { + let hydra = module.hydra_admin_client(); + let kratos = module.kratos_client(); + + // 1. Fetch login request from Hydra. + let login_request = hydra + .get_login_request(&login_challenge) + .await + .map_err(|e| { + tracing::error!("Failed to get login request from Hydra: {e}"); + StatusCode::BAD_GATEWAY + })?; + + // 2. If Hydra says skip (already authenticated), accept immediately. + if login_request.skip { + let redirect = hydra + .accept_login( + &login_challenge, + &AcceptLoginRequest { + subject: login_request.subject.clone(), + remember: Some(true), + remember_for: Some(REMEMBER_FOR_SECS), + }, + ) + .await + .map_err(|e| { + tracing::error!("Failed to accept login at Hydra: {e}"); + StatusCode::BAD_GATEWAY + })?; + + return Ok(Json(OAuth2Response::Redirect { + redirect_to: redirect.redirect_to, + })); + } + + // 3. Verify user has a valid Kratos session via cookie. + let cookie = headers + .get(axum::http::header::COOKIE) + .and_then(|v| v.to_str().ok()) + .unwrap_or(""); + + let kratos_session = verify_kratos_session(kratos, cookie).await?; + + // 4. Accept login with Kratos identity UUID as subject. + let redirect = hydra + .accept_login( + &login_challenge, + &AcceptLoginRequest { + subject: kratos_session.identity_id, + remember: Some(true), + remember_for: Some(REMEMBER_FOR_SECS), + }, + ) + .await + .map_err(|e| { + tracing::error!("Failed to accept login at Hydra: {e}"); + StatusCode::BAD_GATEWAY + })?; + + Ok(Json(OAuth2Response::Redirect { + redirect_to: redirect.redirect_to, + })) +} + +struct VerifiedSession { + identity_id: String, +} + +/// Verify Kratos session via cookie, returning the identity ID on success. +async fn verify_kratos_session( + kratos: &KratosClient, + cookie: &str, +) -> Result { + if cookie.is_empty() { + tracing::warn!("oauth2/login: no cookie header, cannot verify Kratos session"); + return Err(StatusCode::UNAUTHORIZED); + } + + // Extract only the Kratos session cookie to avoid leaking other cookies. + let kratos_cookie = cookie + .split(';') + .map(|c| c.trim()) + .find(|c| c.starts_with("ory_kratos_session=")) + .unwrap_or(""); + + if kratos_cookie.is_empty() { + tracing::warn!("oauth2/login: no ory_kratos_session cookie found"); + return Err(StatusCode::UNAUTHORIZED); + } + + let session = kratos.whoami(kratos_cookie).await.map_err(|e| { + tracing::error!("Kratos whoami request failed: {e}"); + StatusCode::BAD_GATEWAY + })?; + + match session { + Some(s) => Ok(VerifiedSession { + identity_id: s.identity.id, + }), + None => { + tracing::warn!("oauth2/login: Kratos session invalid or expired"); + Err(StatusCode::UNAUTHORIZED) + } + } +} + +// --------------------------------------------------------------------------- +// GET /oauth2/consent +// --------------------------------------------------------------------------- + +async fn get_consent( + State(module): State, + Query(ConsentQuery { consent_challenge }): Query, +) -> Result, StatusCode> { + let hydra = module.hydra_admin_client(); + + let consent_request = hydra + .get_consent_request(&consent_challenge) + .await + .map_err(|e| { + tracing::error!("Failed to get consent request from Hydra: {e}"); + StatusCode::BAD_GATEWAY + })?; + + // If the client is configured to skip consent, accept automatically. + let skip_consent = consent_request + .client + .as_ref() + .and_then(|c| c.skip_consent) + .unwrap_or(false); + + if consent_request.skip || skip_consent { + let redirect = hydra + .accept_consent( + &consent_challenge, + &AcceptConsentRequest { + grant_scope: consent_request.requested_scope.clone(), + grant_access_token_audience: consent_request + .requested_access_token_audience + .clone(), + remember: Some(true), + remember_for: Some(REMEMBER_FOR_SECS), + session: None, + }, + ) + .await + .map_err(|e| { + tracing::error!("Failed to accept consent at Hydra: {e}"); + StatusCode::BAD_GATEWAY + })?; + + return Ok(Json(OAuth2Response::Redirect { + redirect_to: redirect.redirect_to, + })); + } + + // Non-skip: return consent details for frontend to display. + let client_name = consent_request + .client + .as_ref() + .and_then(|c| c.client_name.clone()); + + Ok(Json(OAuth2Response::ShowConsent { + consent_challenge, + client_name, + requested_scope: consent_request.requested_scope, + })) +} + +// --------------------------------------------------------------------------- +// POST /oauth2/consent +// --------------------------------------------------------------------------- + +async fn post_consent( + State(module): State, + Json(decision): Json, +) -> Result, StatusCode> { + let hydra = module.hydra_admin_client(); + + if decision.accept { + let grant_scope = decision.grant_scope.unwrap_or_default(); + + // Re-fetch consent request to get requested_access_token_audience + // and to validate that granted scopes are a subset of requested scopes. + let consent_request = hydra + .get_consent_request(&decision.consent_challenge) + .await + .map_err(|e| { + tracing::error!("Failed to get consent request from Hydra: {e}"); + StatusCode::BAD_GATEWAY + })?; + + // Validate: grant_scope must be a subset of requested_scope. + let requested: HashSet<&str> = consent_request + .requested_scope + .iter() + .map(|s| s.as_str()) + .collect(); + for scope in &grant_scope { + if !requested.contains(scope.as_str()) { + tracing::warn!("Client attempted to grant unrequested scope: {scope}"); + return Err(StatusCode::BAD_REQUEST); + } + } + + let redirect = hydra + .accept_consent( + &decision.consent_challenge, + &AcceptConsentRequest { + grant_scope, + grant_access_token_audience: consent_request.requested_access_token_audience, + remember: Some(true), + remember_for: Some(REMEMBER_FOR_SECS), + session: None, + }, + ) + .await + .map_err(|e| { + tracing::error!("Failed to accept consent at Hydra: {e}"); + StatusCode::BAD_GATEWAY + })?; + + Ok(Json(OAuth2Response::Redirect { + redirect_to: redirect.redirect_to, + })) + } else { + let redirect = hydra + .reject_consent( + &decision.consent_challenge, + &RejectRequest { + error: "consent_denied".to_string(), + error_description: Some("The user denied the consent request.".to_string()), + }, + ) + .await + .map_err(|e| { + tracing::error!("Failed to reject consent at Hydra: {e}"); + StatusCode::BAD_GATEWAY + })?; + + Ok(Json(OAuth2Response::Redirect { + redirect_to: redirect.redirect_to, + })) + } +} + +// --------------------------------------------------------------------------- +// Router +// --------------------------------------------------------------------------- + +pub trait OAuth2Router { + fn route_oauth2(self) -> Self; +} + +impl OAuth2Router for Router { + fn route_oauth2(self) -> Self { + self.route("/oauth2/login", get(login)) + .route("/oauth2/consent", get(get_consent)) + .route("/oauth2/consent", post(post_consent)) + } +} diff --git a/server/src/route/profile.rs b/server/src/route/profile.rs index c076598..d2e09b7 100644 --- a/server/src/route/profile.rs +++ b/server/src/route/profile.rs @@ -1,22 +1,15 @@ +use crate::auth::{resolve_auth_account_id, AuthClaims, OidcAuthInfo}; use crate::error::ErrorStatus; -use crate::expect_role; use crate::handler::AppModule; -use crate::keycloak::{resolve_auth_account_id, KeycloakAuthAccount}; use application::service::profile::{ CreateProfileUseCase, DeleteProfileUseCase, EditProfileUseCase, GetProfileUseCase, }; use axum::extract::{Path, State}; use axum::http::StatusCode; -use axum::http::{Method, Uri}; -use axum::routing::{delete, get, post, put}; +use axum::routing::get; use axum::{Extension, Json, Router}; -use axum_keycloak_auth::decode::KeycloakToken; -use axum_keycloak_auth::instance::KeycloakAuthInstance; -use axum_keycloak_auth::layer::KeycloakAuthLayer; -use axum_keycloak_auth::PassthroughMode; use kernel::prelude::entity::ImageId; use serde::{Deserialize, Serialize}; -use std::sync::Arc; use uuid::Uuid; #[derive(Debug, Deserialize)] @@ -57,18 +50,15 @@ impl From for ProfileResponse { } pub trait ProfileRouter { - fn route_profile(self, keycloak: Arc) -> Self; + fn route_profile(self) -> Self; } async fn get_profile( - Extension(token): Extension>, + Extension(claims): Extension, State(module): State, - method: Method, - uri: Uri, Path(account_id): Path, ) -> Result, ErrorStatus> { - expect_role!(&token, uri, method); - let auth_info = KeycloakAuthAccount::from(token); + let auth_info = OidcAuthInfo::from(claims); if account_id.trim().is_empty() { return Err(ErrorStatus::from(( @@ -90,15 +80,12 @@ async fn get_profile( } async fn create_profile( - Extension(token): Extension>, + Extension(claims): Extension, State(module): State, - method: Method, - uri: Uri, Path(account_id): Path, Json(body): Json, ) -> Result<(StatusCode, Json), ErrorStatus> { - expect_role!(&token, uri, method); - let auth_info = KeycloakAuthAccount::from(token); + let auth_info = OidcAuthInfo::from(claims); if account_id.trim().is_empty() { return Err(ErrorStatus::from(( @@ -130,15 +117,12 @@ async fn create_profile( } async fn update_profile( - Extension(token): Extension>, + Extension(claims): Extension, State(module): State, - method: Method, - uri: Uri, Path(account_id): Path, Json(body): Json, ) -> Result { - expect_role!(&token, uri, method); - let auth_info = KeycloakAuthAccount::from(token); + let auth_info = OidcAuthInfo::from(claims); if account_id.trim().is_empty() { return Err(ErrorStatus::from(( @@ -170,14 +154,11 @@ async fn update_profile( } async fn delete_profile( - Extension(token): Extension>, + Extension(claims): Extension, State(module): State, - method: Method, - uri: Uri, Path(account_id): Path, ) -> Result { - expect_role!(&token, uri, method); - let auth_info = KeycloakAuthAccount::from(token); + let auth_info = OidcAuthInfo::from(claims); if account_id.trim().is_empty() { return Err(ErrorStatus::from(( @@ -199,7 +180,7 @@ async fn delete_profile( } impl ProfileRouter for Router { - fn route_profile(self, keycloak: Arc) -> Self { + fn route_profile(self) -> Self { self.route( "/accounts/:account_id/profile", get(get_profile) @@ -207,13 +188,6 @@ impl ProfileRouter for Router { .put(update_profile) .delete(delete_profile), ) - .layer( - KeycloakAuthLayer::::builder() - .instance(keycloak) - .passthrough_mode(PassthroughMode::Block) - .expected_audiences(vec![String::from("account")]) - .build(), - ) } } From ebeb9acf7deef51bf103cd8e392f80cee244c3f3 Mon Sep 17 00:00:00 2001 From: turtton Date: Thu, 12 Mar 2026 11:00:25 +0900 Subject: [PATCH 54/59] :white_check_mark: Add OAuth2 Login/Consent integration tests with wiremock MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit wiremockでHydra/Kratosをモックし、OAuth2エンドポイントの統合テスト10件を追加。 - GET /oauth2/login: skip/session有効/cookie無し/session無効の4ケース - GET /oauth2/consent: skip/client skip/通常表示の3ケース - POST /oauth2/consent: accept/scope不正/rejectの3ケース その他の変更: - tower 0.4→0.5にアップグレード(axum 0.7互換) - Handler::init_with_urls()を抽出しコンストラクタの重複を解消 --- Cargo.lock | 119 ++++++--- server/Cargo.toml | 4 +- server/src/handler.rs | 26 +- server/src/route/oauth2.rs | 493 +++++++++++++++++++++++++++++++++++++ 4 files changed, 599 insertions(+), 43 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 7fb82a3..bd97675 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -164,6 +164,16 @@ version = "0.7.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" +[[package]] +name = "assert-json-diff" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47e4f2b81832e72834d7518d8487a0396a28cc408186a2e8854c0f98011faf12" +dependencies = [ + "serde", + "serde_json", +] + [[package]] name = "async-trait" version = "0.1.81" @@ -224,7 +234,7 @@ dependencies = [ "serde_urlencoded", "sync_wrapper", "tokio", - "tower 0.5.2", + "tower", "tower-layer", "tower-service", "tracing", @@ -568,6 +578,18 @@ dependencies = [ "cipher", ] +[[package]] +name = "deadpool" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb84100978c1c7b37f09ed3ce3e5f843af02c2a2c431bae5b19230dad2c1b490" +dependencies = [ + "async-trait", + "deadpool-runtime", + "num_cpus", + "tokio", +] + [[package]] name = "deadpool" version = "0.12.1" @@ -585,7 +607,7 @@ version = "0.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c136f185b3ca9d1f4e4e19c11570e1002f4bfdd592d589053e225716d613851f" dependencies = [ - "deadpool", + "deadpool 0.12.1", "redis", ] @@ -804,6 +826,21 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" +[[package]] +name = "futures" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + [[package]] name = "futures-channel" version = "0.3.31" @@ -848,6 +885,17 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" +[[package]] +name = "futures-macro" +version = "0.3.31" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.96", +] + [[package]] name = "futures-sink" version = "0.3.31" @@ -866,8 +914,10 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ + "futures-channel", "futures-core", "futures-io", + "futures-macro", "futures-sink", "futures-task", "memchr", @@ -1751,26 +1801,6 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" -[[package]] -name = "pin-project" -version = "1.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1749c7ed4bcaf4c3d0a3efc28538844fb29bcdd7d2b67b2be7e20ba861ff517" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9b20ed30f105399776b9c883e68e536ef602a16ae6f596d2c473591d6ad64c6" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.96", -] - [[package]] name = "pin-project-lite" version = "0.2.14" @@ -2101,7 +2131,7 @@ dependencies = [ "system-configuration", "tokio", "tokio-native-tls", - "tower 0.5.2", + "tower", "tower-service", "url", "wasm-bindgen", @@ -2401,6 +2431,7 @@ dependencies = [ "dotenvy", "driver", "error-stack", + "http-body-util", "jsonwebtoken", "kernel", "rand", @@ -2411,7 +2442,7 @@ dependencies = [ "serde_json", "time", "tokio", - "tower 0.4.13", + "tower", "tower-http", "tracing", "tracing-appender", @@ -2419,6 +2450,7 @@ dependencies = [ "url", "uuid", "vodca", + "wiremock", ] [[package]] @@ -3081,21 +3113,6 @@ dependencies = [ "winnow", ] -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "futures-core", - "futures-util", - "pin-project", - "pin-project-lite", - "tower-layer", - "tower-service", - "tracing", -] - [[package]] name = "tower" version = "0.5.2" @@ -3767,6 +3784,30 @@ version = "0.0.19" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904" +[[package]] +name = "wiremock" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2b8b99d4cdbf36b239a9532e31fe4fb8acc38d1897c1761e161550a7dc78e6a" +dependencies = [ + "assert-json-diff", + "async-trait", + "base64 0.22.1", + "deadpool 0.10.0", + "futures", + "http", + "http-body-util", + "hyper", + "hyper-util", + "log", + "once_cell", + "regex", + "serde", + "serde_json", + "tokio", + "url", +] + [[package]] name = "write16" version = "1.0.0" diff --git a/server/Cargo.toml b/server/Cargo.toml index 675d5e0..890873e 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -17,7 +17,7 @@ tracing-appender = "0.2.3" tracing-subscriber = { workspace = true } axum = { version = "0.7.4", features = ["json", "tracing"] } -tower = { version = "0.4", features = ["util"] } +tower = { version = "0.5", features = ["util"] } tower-http = { version = "0.5.1", features = ["tokio", "cors"] } tokio = { workspace = true, features = ["rt", "rt-multi-thread", "macros"] } @@ -41,3 +41,5 @@ kernel = { path = "../kernel" } rand = "0.8" rsa = { version = "0.9" } base64 = "0.22" +wiremock = "0.6" +http-body-util = "0.1" diff --git a/server/src/handler.rs b/server/src/handler.rs index 6dd9873..2443cd7 100644 --- a/server/src/handler.rs +++ b/server/src/handler.rs @@ -234,14 +234,19 @@ pub struct Handler { impl Handler { pub async fn init() -> error_stack::Result { - let pgpool = PostgresDatabase::new().await?; - let redis = RedisDatabase::new()?; - let hydra_admin_url = dotenvy::var("HYDRA_ADMIN_URL").unwrap_or_else(|_| "http://localhost:4445".to_string()); let kratos_public_url = dotenvy::var("KRATOS_PUBLIC_URL") .unwrap_or_else(|_| "http://localhost:4433".to_string()); + Self::init_with_urls(hydra_admin_url, kratos_public_url).await + } + async fn init_with_urls( + hydra_admin_url: String, + kratos_public_url: String, + ) -> error_stack::Result { + let pgpool = PostgresDatabase::new().await?; + let redis = RedisDatabase::new()?; Ok(Self { pgpool, redis, @@ -256,6 +261,21 @@ impl Handler { } } +#[cfg(test)] +impl AppModule { + pub(crate) async fn new_for_oauth2_test( + hydra_admin_url: String, + kratos_public_url: String, + ) -> error_stack::Result { + let handler = Arc::new(Handler::init_with_urls(hydra_admin_url, kratos_public_url).await?); + let applier_container = Arc::new(ApplierContainer::new(handler.clone())); + Ok(Self { + handler, + applier_container, + }) + } +} + // --- Database DI implementations (via macro) --- kernel::impl_database_delegation!(Handler, pgpool, PostgresDatabase); diff --git a/server/src/route/oauth2.rs b/server/src/route/oauth2.rs index 5ec6154..e785c1a 100644 --- a/server/src/route/oauth2.rs +++ b/server/src/route/oauth2.rs @@ -317,3 +317,496 @@ impl OAuth2Router for Router { .route("/oauth2/consent", post(post_consent)) } } + +#[cfg(test)] +mod tests { + use super::*; + use axum::body::Body; + use axum::http::Request; + use http_body_util::BodyExt; + use tower::ServiceExt; + use wiremock::matchers::{method, path, query_param}; + use wiremock::{Mock, MockServer, ResponseTemplate}; + + async fn build_app(hydra_url: &str, kratos_url: &str) -> Router { + let app = AppModule::new_for_oauth2_test(hydra_url.into(), kratos_url.into()) + .await + .unwrap(); + Router::new().route_oauth2().with_state(app) + } + + async fn response_json(resp: axum::http::Response) -> serde_json::Value { + let body = resp.into_body().collect().await.unwrap().to_bytes(); + serde_json::from_slice(&body).unwrap_or_else(|e| { + panic!( + "Failed to parse response as JSON: {e}\nBody: {}", + String::from_utf8_lossy(&body) + ) + }) + } + + // ----------------------------------------------------------------------- + // GET /oauth2/login + // ----------------------------------------------------------------------- + + #[tokio::test] + async fn login_skip_returns_redirect() { + let hydra_mock = MockServer::start().await; + let kratos_mock = MockServer::start().await; + + Mock::given(method("GET")) + .and(path("/admin/oauth2/auth/requests/login")) + .and(query_param("login_challenge", "test-challenge")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "challenge": "test-challenge", + "skip": true, + "subject": "user-uuid", + "client": null, + "requested_scope": ["openid"], + "requested_access_token_audience": ["account"], + "request_url": "http://example.com" + }))) + .mount(&hydra_mock) + .await; + + Mock::given(method("PUT")) + .and(path("/admin/oauth2/auth/requests/login/accept")) + .and(query_param("login_challenge", "test-challenge")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "redirect_to": "http://example.com/callback" + }))) + .mount(&hydra_mock) + .await; + + let app = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await; + + let resp = app + .oneshot( + Request::get("/oauth2/login?login_challenge=test-challenge") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(resp.status(), StatusCode::OK); + let json = response_json(resp).await; + assert_eq!(json["action"], "redirect"); + assert_eq!(json["redirect_to"], "http://example.com/callback"); + } + + #[tokio::test] + async fn login_valid_kratos_session_returns_redirect() { + let hydra_mock = MockServer::start().await; + let kratos_mock = MockServer::start().await; + + Mock::given(method("GET")) + .and(path("/admin/oauth2/auth/requests/login")) + .and(query_param("login_challenge", "challenge-2")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "challenge": "challenge-2", + "skip": false, + "subject": "", + "client": null, + "requested_scope": ["openid"], + "requested_access_token_audience": ["account"], + "request_url": "http://example.com" + }))) + .mount(&hydra_mock) + .await; + + Mock::given(method("GET")) + .and(path("/sessions/whoami")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "id": "session-id", + "active": true, + "identity": { + "id": "identity-uuid", + "traits": {} + } + }))) + .mount(&kratos_mock) + .await; + + Mock::given(method("PUT")) + .and(path("/admin/oauth2/auth/requests/login/accept")) + .and(query_param("login_challenge", "challenge-2")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "redirect_to": "http://example.com/consent" + }))) + .mount(&hydra_mock) + .await; + + let app = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await; + + let resp = app + .oneshot( + Request::get("/oauth2/login?login_challenge=challenge-2") + .header("cookie", "ory_kratos_session=test-session-token") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(resp.status(), StatusCode::OK); + let json = response_json(resp).await; + assert_eq!(json["action"], "redirect"); + assert_eq!(json["redirect_to"], "http://example.com/consent"); + } + + #[tokio::test] + async fn login_no_cookie_returns_401() { + let hydra_mock = MockServer::start().await; + let kratos_mock = MockServer::start().await; + + Mock::given(method("GET")) + .and(path("/admin/oauth2/auth/requests/login")) + .and(query_param("login_challenge", "challenge-3")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "challenge": "challenge-3", + "skip": false, + "subject": "", + "client": null, + "requested_scope": ["openid"], + "requested_access_token_audience": ["account"], + "request_url": "http://example.com" + }))) + .mount(&hydra_mock) + .await; + + let app = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await; + + let resp = app + .oneshot( + Request::get("/oauth2/login?login_challenge=challenge-3") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); + } + + #[tokio::test] + async fn login_invalid_kratos_session_returns_401() { + let hydra_mock = MockServer::start().await; + let kratos_mock = MockServer::start().await; + + Mock::given(method("GET")) + .and(path("/admin/oauth2/auth/requests/login")) + .and(query_param("login_challenge", "challenge-4")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "challenge": "challenge-4", + "skip": false, + "subject": "", + "client": null, + "requested_scope": ["openid"], + "requested_access_token_audience": ["account"], + "request_url": "http://example.com" + }))) + .mount(&hydra_mock) + .await; + + Mock::given(method("GET")) + .and(path("/sessions/whoami")) + .respond_with(ResponseTemplate::new(401)) + .mount(&kratos_mock) + .await; + + let app = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await; + + let resp = app + .oneshot( + Request::get("/oauth2/login?login_challenge=challenge-4") + .header("cookie", "ory_kratos_session=expired-token") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); + } + + // ----------------------------------------------------------------------- + // GET /oauth2/consent + // ----------------------------------------------------------------------- + + #[tokio::test] + async fn consent_skip_returns_redirect() { + let hydra_mock = MockServer::start().await; + let kratos_mock = MockServer::start().await; + + Mock::given(method("GET")) + .and(path("/admin/oauth2/auth/requests/consent")) + .and(query_param("consent_challenge", "consent-1")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "challenge": "consent-1", + "skip": true, + "subject": "user-uuid", + "client": null, + "requested_scope": ["openid"], + "requested_access_token_audience": ["account"] + }))) + .mount(&hydra_mock) + .await; + + Mock::given(method("PUT")) + .and(path("/admin/oauth2/auth/requests/consent/accept")) + .and(query_param("consent_challenge", "consent-1")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "redirect_to": "http://example.com/token" + }))) + .mount(&hydra_mock) + .await; + + let app = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await; + + let resp = app + .oneshot( + Request::get("/oauth2/consent?consent_challenge=consent-1") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(resp.status(), StatusCode::OK); + let json = response_json(resp).await; + assert_eq!(json["action"], "redirect"); + assert_eq!(json["redirect_to"], "http://example.com/token"); + } + + #[tokio::test] + async fn consent_client_skip_consent_returns_redirect() { + let hydra_mock = MockServer::start().await; + let kratos_mock = MockServer::start().await; + + Mock::given(method("GET")) + .and(path("/admin/oauth2/auth/requests/consent")) + .and(query_param("consent_challenge", "consent-2")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "challenge": "consent-2", + "skip": false, + "subject": "user-uuid", + "client": { + "client_id": "my-app", + "client_name": "My App", + "skip_consent": true + }, + "requested_scope": ["openid", "offline"], + "requested_access_token_audience": ["account"] + }))) + .mount(&hydra_mock) + .await; + + Mock::given(method("PUT")) + .and(path("/admin/oauth2/auth/requests/consent/accept")) + .and(query_param("consent_challenge", "consent-2")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "redirect_to": "http://example.com/token2" + }))) + .mount(&hydra_mock) + .await; + + let app = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await; + + let resp = app + .oneshot( + Request::get("/oauth2/consent?consent_challenge=consent-2") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(resp.status(), StatusCode::OK); + let json = response_json(resp).await; + assert_eq!(json["action"], "redirect"); + assert_eq!(json["redirect_to"], "http://example.com/token2"); + } + + #[tokio::test] + async fn consent_no_skip_returns_show_consent() { + let hydra_mock = MockServer::start().await; + let kratos_mock = MockServer::start().await; + + Mock::given(method("GET")) + .and(path("/admin/oauth2/auth/requests/consent")) + .and(query_param("consent_challenge", "consent-3")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "challenge": "consent-3", + "skip": false, + "subject": "user-uuid", + "client": { + "client_id": "my-app", + "client_name": "My App", + "skip_consent": false + }, + "requested_scope": ["openid", "profile"], + "requested_access_token_audience": ["account"] + }))) + .mount(&hydra_mock) + .await; + + let app = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await; + + let resp = app + .oneshot( + Request::get("/oauth2/consent?consent_challenge=consent-3") + .body(Body::empty()) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(resp.status(), StatusCode::OK); + let json = response_json(resp).await; + assert_eq!(json["action"], "show_consent"); + assert_eq!(json["consent_challenge"], "consent-3"); + assert_eq!(json["client_name"], "My App"); + assert_eq!( + json["requested_scope"], + serde_json::json!(["openid", "profile"]) + ); + } + + // ----------------------------------------------------------------------- + // POST /oauth2/consent + // ----------------------------------------------------------------------- + + #[tokio::test] + async fn consent_accept_valid_scopes_returns_redirect() { + let hydra_mock = MockServer::start().await; + let kratos_mock = MockServer::start().await; + + Mock::given(method("GET")) + .and(path("/admin/oauth2/auth/requests/consent")) + .and(query_param("consent_challenge", "consent-4")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "challenge": "consent-4", + "skip": false, + "subject": "user-uuid", + "client": null, + "requested_scope": ["openid", "profile"], + "requested_access_token_audience": ["account"] + }))) + .mount(&hydra_mock) + .await; + + Mock::given(method("PUT")) + .and(path("/admin/oauth2/auth/requests/consent/accept")) + .and(query_param("consent_challenge", "consent-4")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "redirect_to": "http://example.com/done" + }))) + .mount(&hydra_mock) + .await; + + let app = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await; + + let resp = app + .oneshot( + Request::post("/oauth2/consent") + .header("content-type", "application/json") + .body(Body::from( + serde_json::to_string(&serde_json::json!({ + "consent_challenge": "consent-4", + "accept": true, + "grant_scope": ["openid"] + })) + .unwrap(), + )) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(resp.status(), StatusCode::OK); + let json = response_json(resp).await; + assert_eq!(json["action"], "redirect"); + assert_eq!(json["redirect_to"], "http://example.com/done"); + } + + #[tokio::test] + async fn consent_accept_invalid_scope_returns_400() { + let hydra_mock = MockServer::start().await; + let kratos_mock = MockServer::start().await; + + Mock::given(method("GET")) + .and(path("/admin/oauth2/auth/requests/consent")) + .and(query_param("consent_challenge", "consent-5")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "challenge": "consent-5", + "skip": false, + "subject": "user-uuid", + "client": null, + "requested_scope": ["openid"], + "requested_access_token_audience": ["account"] + }))) + .mount(&hydra_mock) + .await; + + let app = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await; + + let resp = app + .oneshot( + Request::post("/oauth2/consent") + .header("content-type", "application/json") + .body(Body::from( + serde_json::to_string(&serde_json::json!({ + "consent_challenge": "consent-5", + "accept": true, + "grant_scope": ["openid", "admin"] + })) + .unwrap(), + )) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + } + + #[tokio::test] + async fn consent_reject_returns_redirect() { + let hydra_mock = MockServer::start().await; + let kratos_mock = MockServer::start().await; + + Mock::given(method("PUT")) + .and(path("/admin/oauth2/auth/requests/consent/reject")) + .and(query_param("consent_challenge", "consent-6")) + .respond_with(ResponseTemplate::new(200).set_body_json(serde_json::json!({ + "redirect_to": "http://example.com/denied" + }))) + .mount(&hydra_mock) + .await; + + let app = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await; + + let resp = app + .oneshot( + Request::post("/oauth2/consent") + .header("content-type", "application/json") + .body(Body::from( + serde_json::to_string(&serde_json::json!({ + "consent_challenge": "consent-6", + "accept": false, + "grant_scope": null + })) + .unwrap(), + )) + .unwrap(), + ) + .await + .unwrap(); + + assert_eq!(resp.status(), StatusCode::OK); + let json = response_json(resp).await; + assert_eq!(json["action"], "redirect"); + assert_eq!(json["redirect_to"], "http://example.com/denied"); + } +} From 26d1bbd0a8c3f467e59fb197dc6436f334b64543 Mon Sep 17 00:00:00 2001 From: turtton Date: Thu, 12 Mar 2026 12:21:41 +0900 Subject: [PATCH 55/59] :recycle: Skip OAuth2 tests when DATABASE_URL is unavailable MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dotenvy::varでDATABASE_URLの存在を確認し、未設定時はテストをスキップするように変更。 CIなどDB未起動環境でテストが失敗しないようにする。 --- server/src/route/oauth2.rs | 48 ++++++++++++++++++++++++++++---------- 1 file changed, 36 insertions(+), 12 deletions(-) diff --git a/server/src/route/oauth2.rs b/server/src/route/oauth2.rs index e785c1a..f40f4ce 100644 --- a/server/src/route/oauth2.rs +++ b/server/src/route/oauth2.rs @@ -328,11 +328,15 @@ mod tests { use wiremock::matchers::{method, path, query_param}; use wiremock::{Mock, MockServer, ResponseTemplate}; - async fn build_app(hydra_url: &str, kratos_url: &str) -> Router { + async fn build_app(hydra_url: &str, kratos_url: &str) -> Option { + if dotenvy::var("DATABASE_URL").is_err() { + eprintln!("Skipping: DATABASE_URL not available"); + return None; + } let app = AppModule::new_for_oauth2_test(hydra_url.into(), kratos_url.into()) .await .unwrap(); - Router::new().route_oauth2().with_state(app) + Some(Router::new().route_oauth2().with_state(app)) } async fn response_json(resp: axum::http::Response) -> serde_json::Value { @@ -378,7 +382,9 @@ mod tests { .mount(&hydra_mock) .await; - let app = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await; + let Some(app) = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await else { + return; + }; let resp = app .oneshot( @@ -437,7 +443,9 @@ mod tests { .mount(&hydra_mock) .await; - let app = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await; + let Some(app) = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await else { + return; + }; let resp = app .oneshot( @@ -475,7 +483,9 @@ mod tests { .mount(&hydra_mock) .await; - let app = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await; + let Some(app) = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await else { + return; + }; let resp = app .oneshot( @@ -515,7 +525,9 @@ mod tests { .mount(&kratos_mock) .await; - let app = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await; + let Some(app) = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await else { + return; + }; let resp = app .oneshot( @@ -562,7 +574,9 @@ mod tests { .mount(&hydra_mock) .await; - let app = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await; + let Some(app) = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await else { + return; + }; let resp = app .oneshot( @@ -611,7 +625,9 @@ mod tests { .mount(&hydra_mock) .await; - let app = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await; + let Some(app) = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await else { + return; + }; let resp = app .oneshot( @@ -651,7 +667,9 @@ mod tests { .mount(&hydra_mock) .await; - let app = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await; + let Some(app) = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await else { + return; + }; let resp = app .oneshot( @@ -705,7 +723,9 @@ mod tests { .mount(&hydra_mock) .await; - let app = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await; + let Some(app) = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await else { + return; + }; let resp = app .oneshot( @@ -749,7 +769,9 @@ mod tests { .mount(&hydra_mock) .await; - let app = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await; + let Some(app) = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await else { + return; + }; let resp = app .oneshot( @@ -785,7 +807,9 @@ mod tests { .mount(&hydra_mock) .await; - let app = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await; + let Some(app) = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await else { + return; + }; let resp = app .oneshot( From 06120c269ffaf269afd3833d5f7af86951495a94 Mon Sep 17 00:00:00 2001 From: turtton Date: Thu, 12 Mar 2026 12:26:03 +0900 Subject: [PATCH 56/59] :wrench: Use test_with::env to skip OAuth2 tests when DATABASE_URL is unset MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit test_withのコンパイル時envチェックにより、DATABASE_URL未設定時は テストがignoredとして正しく表示されるようにした。 --- Cargo.lock | 1 + server/Cargo.toml | 1 + server/src/route/oauth2.rs | 59 +++++++++++++++----------------------- 3 files changed, 25 insertions(+), 36 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bd97675..d2b53c7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2440,6 +2440,7 @@ dependencies = [ "rsa", "serde", "serde_json", + "test-with", "time", "tokio", "tower", diff --git a/server/Cargo.toml b/server/Cargo.toml index 890873e..5bd5a66 100644 --- a/server/Cargo.toml +++ b/server/Cargo.toml @@ -43,3 +43,4 @@ rsa = { version = "0.9" } base64 = "0.22" wiremock = "0.6" http-body-util = "0.1" +test-with.workspace = true diff --git a/server/src/route/oauth2.rs b/server/src/route/oauth2.rs index f40f4ce..a558174 100644 --- a/server/src/route/oauth2.rs +++ b/server/src/route/oauth2.rs @@ -328,15 +328,11 @@ mod tests { use wiremock::matchers::{method, path, query_param}; use wiremock::{Mock, MockServer, ResponseTemplate}; - async fn build_app(hydra_url: &str, kratos_url: &str) -> Option { - if dotenvy::var("DATABASE_URL").is_err() { - eprintln!("Skipping: DATABASE_URL not available"); - return None; - } + async fn build_app(hydra_url: &str, kratos_url: &str) -> Router { let app = AppModule::new_for_oauth2_test(hydra_url.into(), kratos_url.into()) .await .unwrap(); - Some(Router::new().route_oauth2().with_state(app)) + Router::new().route_oauth2().with_state(app) } async fn response_json(resp: axum::http::Response) -> serde_json::Value { @@ -353,6 +349,8 @@ mod tests { // GET /oauth2/login // ----------------------------------------------------------------------- + #[test_with::env(DATABASE_URL)] + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn login_skip_returns_redirect() { let hydra_mock = MockServer::start().await; @@ -382,9 +380,7 @@ mod tests { .mount(&hydra_mock) .await; - let Some(app) = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await else { - return; - }; + let app = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await; let resp = app .oneshot( @@ -401,6 +397,7 @@ mod tests { assert_eq!(json["redirect_to"], "http://example.com/callback"); } + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn login_valid_kratos_session_returns_redirect() { let hydra_mock = MockServer::start().await; @@ -443,9 +440,7 @@ mod tests { .mount(&hydra_mock) .await; - let Some(app) = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await else { - return; - }; + let app = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await; let resp = app .oneshot( @@ -463,6 +458,7 @@ mod tests { assert_eq!(json["redirect_to"], "http://example.com/consent"); } + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn login_no_cookie_returns_401() { let hydra_mock = MockServer::start().await; @@ -483,9 +479,7 @@ mod tests { .mount(&hydra_mock) .await; - let Some(app) = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await else { - return; - }; + let app = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await; let resp = app .oneshot( @@ -499,6 +493,7 @@ mod tests { assert_eq!(resp.status(), StatusCode::UNAUTHORIZED); } + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn login_invalid_kratos_session_returns_401() { let hydra_mock = MockServer::start().await; @@ -525,9 +520,7 @@ mod tests { .mount(&kratos_mock) .await; - let Some(app) = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await else { - return; - }; + let app = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await; let resp = app .oneshot( @@ -546,6 +539,7 @@ mod tests { // GET /oauth2/consent // ----------------------------------------------------------------------- + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn consent_skip_returns_redirect() { let hydra_mock = MockServer::start().await; @@ -574,9 +568,7 @@ mod tests { .mount(&hydra_mock) .await; - let Some(app) = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await else { - return; - }; + let app = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await; let resp = app .oneshot( @@ -593,6 +585,7 @@ mod tests { assert_eq!(json["redirect_to"], "http://example.com/token"); } + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn consent_client_skip_consent_returns_redirect() { let hydra_mock = MockServer::start().await; @@ -625,9 +618,7 @@ mod tests { .mount(&hydra_mock) .await; - let Some(app) = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await else { - return; - }; + let app = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await; let resp = app .oneshot( @@ -644,6 +635,7 @@ mod tests { assert_eq!(json["redirect_to"], "http://example.com/token2"); } + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn consent_no_skip_returns_show_consent() { let hydra_mock = MockServer::start().await; @@ -667,9 +659,7 @@ mod tests { .mount(&hydra_mock) .await; - let Some(app) = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await else { - return; - }; + let app = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await; let resp = app .oneshot( @@ -695,6 +685,7 @@ mod tests { // POST /oauth2/consent // ----------------------------------------------------------------------- + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn consent_accept_valid_scopes_returns_redirect() { let hydra_mock = MockServer::start().await; @@ -723,9 +714,7 @@ mod tests { .mount(&hydra_mock) .await; - let Some(app) = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await else { - return; - }; + let app = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await; let resp = app .oneshot( @@ -750,6 +739,7 @@ mod tests { assert_eq!(json["redirect_to"], "http://example.com/done"); } + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn consent_accept_invalid_scope_returns_400() { let hydra_mock = MockServer::start().await; @@ -769,9 +759,7 @@ mod tests { .mount(&hydra_mock) .await; - let Some(app) = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await else { - return; - }; + let app = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await; let resp = app .oneshot( @@ -793,6 +781,7 @@ mod tests { assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } + #[test_with::env(DATABASE_URL)] #[tokio::test] async fn consent_reject_returns_redirect() { let hydra_mock = MockServer::start().await; @@ -807,9 +796,7 @@ mod tests { .mount(&hydra_mock) .await; - let Some(app) = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await else { - return; - }; + let app = build_app(&hydra_mock.uri(), &kratos_mock.uri()).await; let resp = app .oneshot( From 17ec372d5f47509f7397df2facb97b1820079f77 Mon Sep 17 00:00:00 2001 From: turtton Date: Thu, 12 Mar 2026 14:51:59 +0900 Subject: [PATCH 57/59] :sparkles: Introduce Ory Keto permission system with Relation Tuple authorization MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit kernel層にPermission/PermissionReq/Relation/Resource型とPermissionChecker/PermissionWriter traitを定義。 driver層にKetoClient (HTTP API実装)を追加。 application層にポリシー関数(account_view/edit/delete/sign, instance_moderate)とcheck_permissionヘルパーを追加。 全UseCase traitのインライン所有者チェック(find_by_auth_id)をKeto経由のpermissionチェックに置換。 Account作成時にowner tupleを自動作成、削除時に自動削除。 インフラ: compose.ymlにketo-migrate/ketoサービス追加、ory/keto/keto.yml設定ファイル追加。 --- .env.example | 3 + Cargo.lock | 1 + application/src/lib.rs | 1 + application/src/permission.rs | 58 +++++++ application/src/service/account.rs | 94 ++++++----- application/src/service/metadata.rs | 78 ++++------ application/src/service/profile.rs | 66 +++----- compose.yml | 40 +++++ .../01-create-databases.sh | 1 + driver/Cargo.toml | 1 + driver/src/keto.rs | 147 ++++++++++++++++++ driver/src/lib.rs | 1 + kernel/src/lib.rs | 4 + kernel/src/permission.rs | 146 +++++++++++++++++ ory/keto/keto.yml | 17 ++ server/src/handler.rs | 60 ++++++- server/src/main.rs | 1 - server/src/permission.rs | 124 --------------- 18 files changed, 592 insertions(+), 251 deletions(-) create mode 100644 application/src/permission.rs create mode 100644 driver/src/keto.rs create mode 100644 kernel/src/permission.rs create mode 100644 ory/keto/keto.yml delete mode 100644 server/src/permission.rs diff --git a/.env.example b/.env.example index d78956f..09ea00d 100644 --- a/.env.example +++ b/.env.example @@ -11,6 +11,9 @@ HYDRA_ADMIN_URL=http://localhost:4445/ KRATOS_PUBLIC_URL=http://localhost:4433/ EXPECTED_AUDIENCE=account +KETO_READ_URL=http://localhost:4466 +KETO_WRITE_URL=http://localhost:4467 + REDIS_URL=redis://localhost:6379 # or REDIS_HOST=localhost \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index d2b53c7..341c778 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -693,6 +693,7 @@ dependencies = [ "error-stack", "kernel", "rand", + "reqwest", "rsa", "serde", "serde_json", diff --git a/application/src/lib.rs b/application/src/lib.rs index bef0d9e..6936a4d 100644 --- a/application/src/lib.rs +++ b/application/src/lib.rs @@ -1,2 +1,3 @@ +pub mod permission; pub mod service; pub mod transfer; diff --git a/application/src/permission.rs b/application/src/permission.rs new file mode 100644 index 0000000..da7c4de --- /dev/null +++ b/application/src/permission.rs @@ -0,0 +1,58 @@ +use error_stack::Report; +use kernel::interfaces::permission::{ + DependOnPermissionChecker, Permission, PermissionChecker, PermissionReq, Relation, Resource, +}; +use kernel::prelude::entity::{AccountId, AuthAccountId}; +use kernel::KernelError; + +pub fn account_view(account_id: &AccountId) -> Permission { + Permission::new(PermissionReq::new( + Resource::Account(account_id.clone()), + [Relation::Owner, Relation::Editor, Relation::Signer], + )) +} + +pub fn account_edit(account_id: &AccountId) -> Permission { + Permission::new(PermissionReq::new( + Resource::Account(account_id.clone()), + [Relation::Owner, Relation::Editor], + )) +} + +pub fn account_delete(account_id: &AccountId) -> Permission { + Permission::new(PermissionReq::new( + Resource::Account(account_id.clone()), + [Relation::Owner], + )) +} + +pub fn account_sign(account_id: &AccountId) -> Permission { + Permission::new(PermissionReq::new( + Resource::Account(account_id.clone()), + [Relation::Owner, Relation::Signer], + )) +} + +pub fn instance_moderate() -> Permission { + Permission::new(PermissionReq::new( + Resource::Instance, + [Relation::Admin, Relation::Moderator], + )) +} + +pub async fn check_permission( + deps: &T, + subject: &AuthAccountId, + permission: &Permission, +) -> error_stack::Result<(), KernelError> { + if !deps + .permission_checker() + .satisfies(subject, permission) + .await? + { + return Err( + Report::new(KernelError::PermissionDenied).attach_printable("Insufficient permissions") + ); + } + Ok(()) +} diff --git a/application/src/service/account.rs b/application/src/service/account.rs index e3a0f92..63b7bbe 100644 --- a/application/src/service/account.rs +++ b/application/src/service/account.rs @@ -1,3 +1,4 @@ +use crate::permission::{account_delete, account_edit, account_view, check_permission}; use crate::transfer::account::AccountDto; use crate::transfer::pagination::{apply_pagination, Pagination}; use adapter::crypto::{DependOnSigningKeyGenerator, SigningKeyGenerator}; @@ -8,6 +9,9 @@ use adapter::processor::account::{ use error_stack::Report; use kernel::interfaces::crypto::{DependOnPasswordProvider, PasswordProvider}; use kernel::interfaces::database::DatabaseConnection; +use kernel::interfaces::permission::{ + DependOnPermissionChecker, DependOnPermissionWriter, PermissionWriter, Relation, Resource, +}; use kernel::prelude::entity::{ Account, AccountIsBot, AccountName, AccountPrivateKey, AccountPublicKey, AuthAccountId, Nanoid, }; @@ -15,7 +19,9 @@ use kernel::KernelError; use serde_json; use std::future::Future; -pub trait GetAccountUseCase: 'static + Sync + Send + DependOnAccountQueryProcessor { +pub trait GetAccountUseCase: + 'static + Sync + Send + DependOnAccountQueryProcessor + DependOnPermissionChecker +{ fn get_all_accounts( &self, auth_account_id: &AuthAccountId, @@ -65,23 +71,17 @@ pub trait GetAccountUseCase: 'static + Sync + Send + DependOnAccountQueryProcess )) })?; - let accounts = self - .account_query_processor() - .find_by_auth_id(&mut transaction, auth_account_id) - .await?; - - let found = accounts.iter().any(|a| a.id() == account.id()); - if !found { - return Err(Report::new(KernelError::PermissionDenied) - .attach_printable("This account does not belong to the authenticated user")); - } + check_permission(self, auth_account_id, &account_view(account.id())).await?; Ok(AccountDto::from(account)) } } } -impl GetAccountUseCase for T where T: 'static + DependOnAccountQueryProcessor {} +impl GetAccountUseCase for T where + T: 'static + DependOnAccountQueryProcessor + DependOnPermissionChecker +{ +} pub trait CreateAccountUseCase: 'static @@ -90,6 +90,7 @@ pub trait CreateAccountUseCase: + DependOnAccountCommandProcessor + DependOnPasswordProvider + DependOnSigningKeyGenerator + + DependOnPermissionWriter { fn create_account( &self, @@ -123,7 +124,15 @@ pub trait CreateAccountUseCase: private_key, public_key, account_is_bot, - auth_account_id, + auth_account_id.clone(), + ) + .await?; + + self.permission_writer() + .create_relation( + &Resource::Account(account.id().clone()), + Relation::Owner, + &auth_account_id, ) .await?; @@ -137,11 +146,17 @@ impl CreateAccountUseCase for T where + DependOnAccountCommandProcessor + DependOnPasswordProvider + DependOnSigningKeyGenerator + + DependOnPermissionWriter { } pub trait EditAccountUseCase: - 'static + Sync + Send + DependOnAccountCommandProcessor + DependOnAccountQueryProcessor + 'static + + Sync + + Send + + DependOnAccountCommandProcessor + + DependOnAccountQueryProcessor + + DependOnPermissionChecker { fn edit_account( &self, @@ -164,16 +179,7 @@ pub trait EditAccountUseCase: )) })?; - let accounts = self - .account_query_processor() - .find_by_auth_id(&mut transaction, auth_account_id) - .await?; - - let found = accounts.iter().any(|a| a.id() == account.id()); - if !found { - return Err(Report::new(KernelError::PermissionDenied) - .attach_printable("This account does not belong to the authenticated user")); - } + check_permission(self, auth_account_id, &account_edit(account.id())).await?; let account_id = account.id().clone(); let current_version = account.version().clone(); @@ -192,12 +198,21 @@ pub trait EditAccountUseCase: } impl EditAccountUseCase for T where - T: 'static + DependOnAccountCommandProcessor + DependOnAccountQueryProcessor + T: 'static + + DependOnAccountCommandProcessor + + DependOnAccountQueryProcessor + + DependOnPermissionChecker { } pub trait DeleteAccountUseCase: - 'static + Sync + Send + DependOnAccountCommandProcessor + DependOnAccountQueryProcessor + 'static + + Sync + + Send + + DependOnAccountCommandProcessor + + DependOnAccountQueryProcessor + + DependOnPermissionChecker + + DependOnPermissionWriter { fn delete_account( &self, @@ -219,21 +234,20 @@ pub trait DeleteAccountUseCase: )) })?; - let accounts = self - .account_query_processor() - .find_by_auth_id(&mut transaction, auth_account_id) - .await?; - - let found = accounts.iter().any(|a| a.id() == account.id()); - if !found { - return Err(Report::new(KernelError::PermissionDenied) - .attach_printable("This account does not belong to the authenticated user")); - } + check_permission(self, auth_account_id, &account_delete(account.id())).await?; let account_id = account.id().clone(); let current_version = account.version().clone(); self.account_command_processor() - .delete(&mut transaction, account_id, current_version) + .delete(&mut transaction, account_id.clone(), current_version) + .await?; + + self.permission_writer() + .delete_relation( + &Resource::Account(account_id), + Relation::Owner, + auth_account_id, + ) .await?; Ok(()) @@ -242,6 +256,10 @@ pub trait DeleteAccountUseCase: } impl DeleteAccountUseCase for T where - T: 'static + DependOnAccountCommandProcessor + DependOnAccountQueryProcessor + T: 'static + + DependOnAccountCommandProcessor + + DependOnAccountQueryProcessor + + DependOnPermissionChecker + + DependOnPermissionWriter { } diff --git a/application/src/service/metadata.rs b/application/src/service/metadata.rs index d9d718b..cf4e3cc 100644 --- a/application/src/service/metadata.rs +++ b/application/src/service/metadata.rs @@ -1,3 +1,4 @@ +use crate::permission::{account_edit, account_view, check_permission}; use crate::transfer::metadata::MetadataDto; use adapter::processor::account::{AccountQueryProcessor, DependOnAccountQueryProcessor}; use adapter::processor::metadata::{ @@ -8,6 +9,7 @@ use error_stack::Report; use kernel::interfaces::database::{DatabaseConnection, DependOnDatabaseConnection}; use kernel::interfaces::event::EventApplier; use kernel::interfaces::event_store::{DependOnMetadataEventStore, MetadataEventStore}; +use kernel::interfaces::permission::DependOnPermissionChecker; use kernel::interfaces::read_model::{DependOnMetadataReadModel, MetadataReadModel}; use kernel::prelude::entity::{ Account, AuthAccountId, EventId, Metadata, MetadataContent, MetadataId, MetadataLabel, Nanoid, @@ -85,7 +87,12 @@ impl UpdateMetadata for T where } pub trait GetMetadataUseCase: - 'static + Sync + Send + DependOnMetadataQueryProcessor + DependOnAccountQueryProcessor + 'static + + Sync + + Send + + DependOnMetadataQueryProcessor + + DependOnAccountQueryProcessor + + DependOnPermissionChecker { fn get_metadata( &self, @@ -107,16 +114,7 @@ pub trait GetMetadataUseCase: )) })?; - let accounts = self - .account_query_processor() - .find_by_auth_id(&mut transaction, auth_account_id) - .await?; - - let found = accounts.iter().any(|a| a.id() == account.id()); - if !found { - return Err(Report::new(KernelError::PermissionDenied) - .attach_printable("This account does not belong to the authenticated user")); - } + check_permission(self, auth_account_id, &account_view(account.id())).await?; let metadata_list = self .metadata_query_processor() @@ -129,12 +127,22 @@ pub trait GetMetadataUseCase: } impl GetMetadataUseCase for T where - T: 'static + Sync + Send + DependOnMetadataQueryProcessor + DependOnAccountQueryProcessor + T: 'static + + Sync + + Send + + DependOnMetadataQueryProcessor + + DependOnAccountQueryProcessor + + DependOnPermissionChecker { } pub trait CreateMetadataUseCase: - 'static + Sync + Send + DependOnMetadataCommandProcessor + DependOnAccountQueryProcessor + 'static + + Sync + + Send + + DependOnMetadataCommandProcessor + + DependOnAccountQueryProcessor + + DependOnPermissionChecker { fn create_metadata( &self, @@ -158,16 +166,7 @@ pub trait CreateMetadataUseCase: )) })?; - let accounts = self - .account_query_processor() - .find_by_auth_id(&mut transaction, auth_account_id) - .await?; - - let found = accounts.iter().any(|a| a.id() == account.id()); - if !found { - return Err(Report::new(KernelError::PermissionDenied) - .attach_printable("This account does not belong to the authenticated user")); - } + check_permission(self, auth_account_id, &account_edit(account.id())).await?; let account_id = account.id().clone(); let metadata_nanoid = Nanoid::::default(); @@ -188,7 +187,12 @@ pub trait CreateMetadataUseCase: } impl CreateMetadataUseCase for T where - T: 'static + Sync + Send + DependOnMetadataCommandProcessor + DependOnAccountQueryProcessor + T: 'static + + Sync + + Send + + DependOnMetadataCommandProcessor + + DependOnAccountQueryProcessor + + DependOnPermissionChecker { } @@ -199,6 +203,7 @@ pub trait EditMetadataUseCase: + DependOnMetadataCommandProcessor + DependOnMetadataQueryProcessor + DependOnAccountQueryProcessor + + DependOnPermissionChecker { fn edit_metadata( &self, @@ -223,16 +228,7 @@ pub trait EditMetadataUseCase: )) })?; - let accounts = self - .account_query_processor() - .find_by_auth_id(&mut transaction, auth_account_id) - .await?; - - let found = accounts.iter().any(|a| a.id() == account.id()); - if !found { - return Err(Report::new(KernelError::PermissionDenied) - .attach_printable("This account does not belong to the authenticated user")); - } + check_permission(self, auth_account_id, &account_edit(account.id())).await?; let metadata_list = self .metadata_query_processor() @@ -273,6 +269,7 @@ impl EditMetadataUseCase for T where + DependOnMetadataCommandProcessor + DependOnMetadataQueryProcessor + DependOnAccountQueryProcessor + + DependOnPermissionChecker { } @@ -283,6 +280,7 @@ pub trait DeleteMetadataUseCase: + DependOnMetadataCommandProcessor + DependOnMetadataQueryProcessor + DependOnAccountQueryProcessor + + DependOnPermissionChecker { fn delete_metadata( &self, @@ -305,16 +303,7 @@ pub trait DeleteMetadataUseCase: )) })?; - let accounts = self - .account_query_processor() - .find_by_auth_id(&mut transaction, auth_account_id) - .await?; - - let found = accounts.iter().any(|a| a.id() == account.id()); - if !found { - return Err(Report::new(KernelError::PermissionDenied) - .attach_printable("This account does not belong to the authenticated user")); - } + check_permission(self, auth_account_id, &account_edit(account.id())).await?; let metadata_list = self .metadata_query_processor() @@ -349,5 +338,6 @@ impl DeleteMetadataUseCase for T where + DependOnMetadataCommandProcessor + DependOnMetadataQueryProcessor + DependOnAccountQueryProcessor + + DependOnPermissionChecker { } diff --git a/application/src/service/profile.rs b/application/src/service/profile.rs index ce8691b..3a441d7 100644 --- a/application/src/service/profile.rs +++ b/application/src/service/profile.rs @@ -1,3 +1,4 @@ +use crate::permission::{account_edit, account_view, check_permission}; use crate::transfer::profile::ProfileDto; use adapter::processor::account::{AccountQueryProcessor, DependOnAccountQueryProcessor}; use adapter::processor::profile::{ @@ -8,6 +9,7 @@ use error_stack::Report; use kernel::interfaces::database::{DatabaseConnection, DependOnDatabaseConnection}; use kernel::interfaces::event::EventApplier; use kernel::interfaces::event_store::{DependOnProfileEventStore, ProfileEventStore}; +use kernel::interfaces::permission::DependOnPermissionChecker; use kernel::interfaces::read_model::{DependOnProfileReadModel, ProfileReadModel}; use kernel::prelude::entity::{ Account, AuthAccountId, EventId, ImageId, Nanoid, Profile, ProfileDisplayName, ProfileId, @@ -83,7 +85,12 @@ impl UpdateProfile for T where } pub trait GetProfileUseCase: - 'static + Sync + Send + DependOnProfileQueryProcessor + DependOnAccountQueryProcessor + 'static + + Sync + + Send + + DependOnProfileQueryProcessor + + DependOnAccountQueryProcessor + + DependOnPermissionChecker { fn get_profile( &self, @@ -105,16 +112,7 @@ pub trait GetProfileUseCase: )) })?; - let accounts = self - .account_query_processor() - .find_by_auth_id(&mut transaction, auth_account_id) - .await?; - - let found = accounts.iter().any(|a| a.id() == account.id()); - if !found { - return Err(Report::new(KernelError::PermissionDenied) - .attach_printable("This account does not belong to the authenticated user")); - } + check_permission(self, auth_account_id, &account_view(account.id())).await?; let profile = self .profile_query_processor() @@ -131,7 +129,12 @@ pub trait GetProfileUseCase: } impl GetProfileUseCase for T where - T: 'static + Sync + Send + DependOnProfileQueryProcessor + DependOnAccountQueryProcessor + T: 'static + + Sync + + Send + + DependOnProfileQueryProcessor + + DependOnAccountQueryProcessor + + DependOnPermissionChecker { } @@ -142,6 +145,7 @@ pub trait CreateProfileUseCase: + DependOnProfileCommandProcessor + DependOnProfileQueryProcessor + DependOnAccountQueryProcessor + + DependOnPermissionChecker { fn create_profile( &self, @@ -167,16 +171,7 @@ pub trait CreateProfileUseCase: )) })?; - let accounts = self - .account_query_processor() - .find_by_auth_id(&mut transaction, auth_account_id) - .await?; - - let found = accounts.iter().any(|a| a.id() == account.id()); - if !found { - return Err(Report::new(KernelError::PermissionDenied) - .attach_printable("This account does not belong to the authenticated user")); - } + check_permission(self, auth_account_id, &account_edit(account.id())).await?; let existing_profile = self .profile_query_processor() @@ -214,6 +209,7 @@ impl CreateProfileUseCase for T where + DependOnProfileCommandProcessor + DependOnProfileQueryProcessor + DependOnAccountQueryProcessor + + DependOnPermissionChecker { } @@ -224,6 +220,7 @@ pub trait EditProfileUseCase: + DependOnProfileCommandProcessor + DependOnProfileQueryProcessor + DependOnAccountQueryProcessor + + DependOnPermissionChecker { fn edit_profile( &self, @@ -249,16 +246,7 @@ pub trait EditProfileUseCase: )) })?; - let accounts = self - .account_query_processor() - .find_by_auth_id(&mut transaction, auth_account_id) - .await?; - - let found = accounts.iter().any(|a| a.id() == account.id()); - if !found { - return Err(Report::new(KernelError::PermissionDenied) - .attach_printable("This account does not belong to the authenticated user")); - } + check_permission(self, auth_account_id, &account_edit(account.id())).await?; let profile = self .profile_query_processor() @@ -295,6 +283,7 @@ impl EditProfileUseCase for T where + DependOnProfileCommandProcessor + DependOnProfileQueryProcessor + DependOnAccountQueryProcessor + + DependOnPermissionChecker { } @@ -305,6 +294,7 @@ pub trait DeleteProfileUseCase: + DependOnProfileCommandProcessor + DependOnProfileQueryProcessor + DependOnAccountQueryProcessor + + DependOnPermissionChecker { fn delete_profile( &self, @@ -326,16 +316,7 @@ pub trait DeleteProfileUseCase: )) })?; - let accounts = self - .account_query_processor() - .find_by_auth_id(&mut transaction, auth_account_id) - .await?; - - let found = accounts.iter().any(|a| a.id() == account.id()); - if !found { - return Err(Report::new(KernelError::PermissionDenied) - .attach_printable("This account does not belong to the authenticated user")); - } + check_permission(self, auth_account_id, &account_edit(account.id())).await?; let profile = self .profile_query_processor() @@ -364,5 +345,6 @@ impl DeleteProfileUseCase for T where + DependOnProfileCommandProcessor + DependOnProfileQueryProcessor + DependOnAccountQueryProcessor + + DependOnPermissionChecker { } diff --git a/compose.yml b/compose.yml index 38320cb..e4d6035 100644 --- a/compose.yml +++ b/compose.yml @@ -123,5 +123,45 @@ services: timeout: 5s retries: 5 + keto-migrate: + image: docker.io/oryd/keto:v0.12.0 + container_name: emumet-keto-migrate + environment: + DSN: postgres://postgres:develop@postgres:5432/keto?sslmode=disable + volumes: + - type: bind + source: ./ory/keto + target: /etc/config/keto + bind: + selinux: z + command: migrate up -y -c /etc/config/keto/keto.yml + depends_on: + postgres: + condition: service_healthy + + keto: + image: docker.io/oryd/keto:v0.12.0 + container_name: emumet-keto + environment: + DSN: postgres://postgres:develop@postgres:5432/keto?sslmode=disable + volumes: + - type: bind + source: ./ory/keto + target: /etc/config/keto + bind: + selinux: z + command: serve -c /etc/config/keto/keto.yml + ports: + - "4466:4466" + - "4467:4467" + depends_on: + keto-migrate: + condition: service_completed_successfully + healthcheck: + test: ["CMD-SHELL", "wget --no-verbose --tries=1 --spider http://localhost:4466/health/alive || exit 1"] + interval: 10s + timeout: 5s + retries: 5 + volumes: postgres-data: diff --git a/docker-entrypoint-initdb.d/01-create-databases.sh b/docker-entrypoint-initdb.d/01-create-databases.sh index 0f01155..f75355e 100755 --- a/docker-entrypoint-initdb.d/01-create-databases.sh +++ b/docker-entrypoint-initdb.d/01-create-databases.sh @@ -4,4 +4,5 @@ set -e psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" <<-EOSQL CREATE DATABASE kratos; CREATE DATABASE hydra; + CREATE DATABASE keto; EOSQL diff --git a/driver/Cargo.toml b/driver/Cargo.toml index bf3fd9d..4aeabf3 100644 --- a/driver/Cargo.toml +++ b/driver/Cargo.toml @@ -15,6 +15,7 @@ serde = { workspace = true } uuid = { workspace = true } time = { workspace = true } async-trait = "0.1" +reqwest = { version = "0.12", features = ["json"] } vodca.workspace = true error-stack = { workspace = true } diff --git a/driver/src/keto.rs b/driver/src/keto.rs new file mode 100644 index 0000000..3ee3603 --- /dev/null +++ b/driver/src/keto.rs @@ -0,0 +1,147 @@ +use error_stack::{Report, ResultExt}; +use kernel::interfaces::permission::{ + PermissionChecker, PermissionReq, PermissionWriter, Relation, Resource, +}; +use kernel::prelude::entity::AuthAccountId; +use kernel::KernelError; +use reqwest::Client; +use serde::{Deserialize, Serialize}; + +pub struct KetoClient { + read_url: String, + write_url: String, + http_client: Client, +} + +impl KetoClient { + pub fn new(read_url: String, write_url: String) -> Self { + let read_url = read_url.trim_end_matches('/').to_string(); + let write_url = write_url.trim_end_matches('/').to_string(); + Self { + read_url, + write_url, + http_client: Client::new(), + } + } +} + +#[derive(Debug, Serialize)] +struct CheckRequest { + namespace: String, + object: String, + relation: String, + subject_id: String, +} + +#[derive(Debug, Deserialize)] +struct CheckResponse { + allowed: bool, +} + +#[derive(Debug, Serialize)] +struct RelationTuple { + namespace: String, + object: String, + relation: String, + subject_id: String, +} + +impl PermissionChecker for KetoClient { + async fn check( + &self, + subject: &AuthAccountId, + req: &PermissionReq, + ) -> error_stack::Result { + let subject_id = subject.as_ref().to_string(); + + for relation in req.relations() { + let body = CheckRequest { + namespace: req.resource().namespace().to_string(), + object: req.resource().object_id(), + relation: relation.as_str().to_string(), + subject_id: subject_id.clone(), + }; + + let response = self + .http_client + .post(format!("{}/relation-tuples/check", self.read_url)) + .json(&body) + .send() + .await + .change_context_lazy(|| KernelError::Internal) + .attach_printable("Failed to check permission with Keto")?; + + if response.status().is_success() { + let check: CheckResponse = response + .json() + .await + .change_context_lazy(|| KernelError::Internal) + .attach_printable("Failed to parse Keto check response")?; + + if check.allowed { + return Ok(true); + } + } + } + + Ok(false) + } +} + +impl PermissionWriter for KetoClient { + async fn create_relation( + &self, + resource: &Resource, + relation: Relation, + subject: &AuthAccountId, + ) -> error_stack::Result<(), KernelError> { + let tuple = RelationTuple { + namespace: resource.namespace().to_string(), + object: resource.object_id(), + relation: relation.as_str().to_string(), + subject_id: subject.as_ref().to_string(), + }; + + self.http_client + .put(format!("{}/admin/relation-tuples", self.write_url)) + .json(&tuple) + .send() + .await + .change_context_lazy(|| KernelError::Internal) + .attach_printable("Failed to create relation tuple in Keto")? + .error_for_status() + .map_err(|e| { + Report::new(KernelError::Internal) + .attach_printable(format!("Keto write error: {e}")) + })?; + + Ok(()) + } + + async fn delete_relation( + &self, + resource: &Resource, + relation: Relation, + subject: &AuthAccountId, + ) -> error_stack::Result<(), KernelError> { + self.http_client + .delete(format!("{}/admin/relation-tuples", self.write_url)) + .query(&[ + ("namespace", resource.namespace()), + ("object", &resource.object_id()), + ("relation", relation.as_str()), + ("subject_id", &subject.as_ref().to_string()), + ]) + .send() + .await + .change_context_lazy(|| KernelError::Internal) + .attach_printable("Failed to delete relation tuple from Keto")? + .error_for_status() + .map_err(|e| { + Report::new(KernelError::Internal) + .attach_printable(format!("Keto delete error: {e}")) + })?; + + Ok(()) + } +} diff --git a/driver/src/lib.rs b/driver/src/lib.rs index bdf8e69..b400a9e 100644 --- a/driver/src/lib.rs +++ b/driver/src/lib.rs @@ -1,5 +1,6 @@ pub mod crypto; pub mod database; mod error; +pub mod keto; pub use self::error::*; diff --git a/kernel/src/lib.rs b/kernel/src/lib.rs index 0671286..c3e40ea 100644 --- a/kernel/src/lib.rs +++ b/kernel/src/lib.rs @@ -4,6 +4,7 @@ mod entity; mod error; mod event; mod event_store; +mod permission; mod read_model; mod repository; mod signal; @@ -37,6 +38,9 @@ pub mod interfaces { pub mod repository { pub use crate::repository::*; } + pub mod permission { + pub use crate::permission::*; + } pub mod signal { pub use crate::signal::*; } diff --git a/kernel/src/permission.rs b/kernel/src/permission.rs new file mode 100644 index 0000000..c8377f5 --- /dev/null +++ b/kernel/src/permission.rs @@ -0,0 +1,146 @@ +use crate::entity::{AccountId, AuthAccountId}; +use crate::KernelError; +use std::collections::HashSet; +use std::future::Future; +use std::ops::Add; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum Relation { + Owner, + Editor, + Signer, + Admin, + Moderator, +} + +impl Relation { + pub fn as_str(&self) -> &'static str { + match self { + Relation::Owner => "owner", + Relation::Editor => "editor", + Relation::Signer => "signer", + Relation::Admin => "admin", + Relation::Moderator => "moderator", + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum Resource { + Account(AccountId), + Instance, +} + +impl Resource { + pub fn namespace(&self) -> &'static str { + match self { + Resource::Account(_) => "accounts", + Resource::Instance => "instance", + } + } + + pub fn object_id(&self) -> String { + match self { + Resource::Account(id) => id.as_ref().to_string(), + Resource::Instance => "singleton".to_string(), + } + } +} + +#[derive(Debug, Clone)] +pub struct PermissionReq { + resource: Resource, + relations: HashSet, +} + +impl PermissionReq { + pub fn new(resource: Resource, relations: impl IntoIterator) -> Self { + Self { + resource, + relations: relations.into_iter().collect(), + } + } + + pub fn resource(&self) -> &Resource { + &self.resource + } + + pub fn relations(&self) -> &HashSet { + &self.relations + } +} + +#[derive(Debug, Clone)] +pub struct Permission(Vec); + +impl Permission { + pub fn new(req: PermissionReq) -> Self { + Self(vec![req]) + } + + pub fn all(reqs: Vec) -> Self { + Self(reqs) + } + + pub fn requirements(&self) -> &[PermissionReq] { + &self.0 + } +} + +impl Add for Permission { + type Output = Permission; + + fn add(mut self, rhs: Self) -> Self::Output { + self.0.extend(rhs.0); + self + } +} + +pub trait PermissionChecker: Send + Sync + 'static { + fn check( + &self, + subject: &AuthAccountId, + req: &PermissionReq, + ) -> impl Future> + Send; + + fn satisfies( + &self, + subject: &AuthAccountId, + permission: &Permission, + ) -> impl Future> + Send { + async move { + for req in permission.requirements() { + if !self.check(subject, req).await? { + return Ok(false); + } + } + Ok(true) + } + } +} + +pub trait DependOnPermissionChecker: Send + Sync { + type PermissionChecker: PermissionChecker; + fn permission_checker(&self) -> &Self::PermissionChecker; +} + +pub trait PermissionWriter: Send + Sync + 'static { + fn create_relation( + &self, + resource: &Resource, + relation: Relation, + subject: &AuthAccountId, + ) -> impl Future> + Send; + + fn delete_relation( + &self, + resource: &Resource, + relation: Relation, + subject: &AuthAccountId, + ) -> impl Future> + Send; +} + +pub trait DependOnPermissionWriter: Send + Sync { + type PermissionWriter: PermissionWriter; + fn permission_writer(&self) -> &Self::PermissionWriter; +} diff --git a/ory/keto/keto.yml b/ory/keto/keto.yml new file mode 100644 index 0000000..904f09b --- /dev/null +++ b/ory/keto/keto.yml @@ -0,0 +1,17 @@ +version: v0.12.0 + +dsn: postgres://postgres:develop@postgres:5432/keto?sslmode=disable + +namespaces: + - id: 0 + name: accounts + - id: 1 + name: instance + +serve: + read: + host: 0.0.0.0 + port: 4466 + write: + host: 0.0.0.0 + port: 4467 diff --git a/server/src/handler.rs b/server/src/handler.rs index 2443cd7..60b72fb 100644 --- a/server/src/handler.rs +++ b/server/src/handler.rs @@ -9,11 +9,13 @@ use driver::crypto::{ Argon2Encryptor, FilePasswordProvider, Rsa2048RawGenerator, Rsa2048Signer, Rsa2048Verifier, }; use driver::database::{PostgresDatabase, RedisDatabase}; +use driver::keto::KetoClient; use kernel::interfaces::crypto::{ DependOnKeyEncryptor, DependOnPasswordProvider, DependOnRawKeyGenerator, DependOnSignatureVerifier, DependOnSigner, }; use kernel::interfaces::database::DependOnDatabaseConnection; +use kernel::interfaces::permission::{DependOnPermissionChecker, DependOnPermissionWriter}; use kernel::KernelError; use std::sync::Arc; use vodca::References; @@ -213,6 +215,20 @@ impl kernel::interfaces::repository::DependOnImageRepository for AppModule { } } +impl DependOnPermissionChecker for AppModule { + type PermissionChecker = KetoClient; + fn permission_checker(&self) -> &Self::PermissionChecker { + self.handler.as_ref().permission_checker() + } +} + +impl DependOnPermissionWriter for AppModule { + type PermissionWriter = KetoClient; + fn permission_writer(&self) -> &Self::PermissionWriter { + self.handler.as_ref().permission_writer() + } +} + // Note: DependOnSigningKeyGenerator, DependOnAccountCommandProcessor, // DependOnAccountQueryProcessor, and all UseCase traits are provided // automatically via blanket impls in adapter. @@ -230,6 +246,7 @@ pub struct Handler { // Ory clients pub(crate) hydra_admin_client: HydraAdminClient, pub(crate) kratos_client: KratosClient, + keto_client: KetoClient, } impl Handler { @@ -238,12 +255,24 @@ impl Handler { dotenvy::var("HYDRA_ADMIN_URL").unwrap_or_else(|_| "http://localhost:4445".to_string()); let kratos_public_url = dotenvy::var("KRATOS_PUBLIC_URL") .unwrap_or_else(|_| "http://localhost:4433".to_string()); - Self::init_with_urls(hydra_admin_url, kratos_public_url).await + let keto_read_url = + dotenvy::var("KETO_READ_URL").unwrap_or_else(|_| "http://localhost:4466".to_string()); + let keto_write_url = + dotenvy::var("KETO_WRITE_URL").unwrap_or_else(|_| "http://localhost:4467".to_string()); + Self::init_with_urls( + hydra_admin_url, + kratos_public_url, + keto_read_url, + keto_write_url, + ) + .await } async fn init_with_urls( hydra_admin_url: String, kratos_public_url: String, + keto_read_url: String, + keto_write_url: String, ) -> error_stack::Result { let pgpool = PostgresDatabase::new().await?; let redis = RedisDatabase::new()?; @@ -257,6 +286,7 @@ impl Handler { verifier: Rsa2048Verifier, hydra_admin_client: HydraAdminClient::new(hydra_admin_url), kratos_client: KratosClient::new(kratos_public_url), + keto_client: KetoClient::new(keto_read_url, keto_write_url), }) } } @@ -267,7 +297,19 @@ impl AppModule { hydra_admin_url: String, kratos_public_url: String, ) -> error_stack::Result { - let handler = Arc::new(Handler::init_with_urls(hydra_admin_url, kratos_public_url).await?); + let keto_read_url = + dotenvy::var("KETO_READ_URL").unwrap_or_else(|_| "http://localhost:4466".to_string()); + let keto_write_url = + dotenvy::var("KETO_WRITE_URL").unwrap_or_else(|_| "http://localhost:4467".to_string()); + let handler = Arc::new( + Handler::init_with_urls( + hydra_admin_url, + kratos_public_url, + keto_read_url, + keto_write_url, + ) + .await?, + ); let applier_container = Arc::new(ApplierContainer::new(handler.clone())); Ok(Self { handler, @@ -316,3 +358,17 @@ impl DependOnSignatureVerifier for Handler { &self.verifier } } + +impl DependOnPermissionChecker for Handler { + type PermissionChecker = KetoClient; + fn permission_checker(&self) -> &Self::PermissionChecker { + &self.keto_client + } +} + +impl DependOnPermissionWriter for Handler { + type PermissionWriter = KetoClient; + fn permission_writer(&self) -> &Self::PermissionWriter { + &self.keto_client + } +} diff --git a/server/src/main.rs b/server/src/main.rs index b2a3d5e..64ebd5a 100644 --- a/server/src/main.rs +++ b/server/src/main.rs @@ -4,7 +4,6 @@ mod error; mod handler; mod hydra; mod kratos; -mod permission; mod route; use crate::auth::{JwksCache, OidcConfig}; diff --git a/server/src/permission.rs b/server/src/permission.rs deleted file mode 100644 index af3c21e..0000000 --- a/server/src/permission.rs +++ /dev/null @@ -1,124 +0,0 @@ -use std::collections::HashSet; -use std::hash::Hash; -use std::ops::Add; - -struct Request(HashSet); - -impl Request { - pub fn new(objects: HashSet) -> Self { - Request(objects) - } -} - -pub trait ToRequest { - fn to_request(&self, target: &T) -> Request; -} - -#[derive(Debug, Clone)] -pub struct Permission(Vec>); - -impl Permission { - pub fn new(perm: HashSet) -> Self { - Permission(vec![perm]) - } - - pub fn allows(&self, req: &Request) -> bool { - let request = &req.0; - self.0.iter().all(|p| !(p & request).is_empty()) - } -} - -impl Add for Permission { - type Output = Self; - - fn add(self, rhs: Self) -> Self::Output { - let mut perm = self.0; - perm.extend(rhs.0); - Permission(perm) - } -} - -pub trait ToPermission { - fn to_permission(&self, target: T) -> Permission; -} - -#[cfg(test)] -mod test { - use super::*; - use std::collections::HashMap; - - #[derive(Debug, Clone, PartialEq, Eq, Hash)] - enum Role { - Admin, - User, - Guest, - } - - #[derive(Debug, Clone, PartialEq, Eq, Hash)] - struct User { - id: u64, - } - - #[derive(Debug, Clone)] - struct RoleManager(HashMap); - - impl ToRequest for RoleManager { - fn to_request(&self, target: &User) -> Request { - Request(self.0.get(&target.id).map_or_else(HashSet::new, |role| { - vec![role.clone()].into_iter().collect::>() - })) - } - } - - #[derive(Debug, Clone, Hash, PartialEq, Eq)] - enum Action { - Read, - Write, - Delete, - } - - #[derive(Debug, Clone)] - struct ActionRoles(HashMap>); - - impl ToPermission for ActionRoles { - fn to_permission(&self, target: Action) -> Permission { - self.0.get(&target).map_or_else( - || Permission::new(HashSet::new()), - |roles| Permission::new(roles.clone()), - ) - } - } - - #[test] - fn test() { - let mut role_manager = RoleManager(HashMap::new()); - let mut action_roles = ActionRoles(HashMap::new()); - action_roles.0.insert( - Action::Read, - vec![Role::Admin, Role::User, Role::Guest] - .into_iter() - .collect::>(), - ); - action_roles.0.insert( - Action::Write, - vec![Role::Admin, Role::User] - .into_iter() - .collect::>(), - ); - action_roles.0.insert( - Action::Delete, - vec![Role::Admin].into_iter().collect::>(), - ); - - let user = User { id: 1 }; - role_manager.0.insert(user.id, Role::User); - - let request = role_manager.to_request(&user); - let read = action_roles.to_permission(Action::Read); - let write = action_roles.to_permission(Action::Write); - - let new_perm = read + write; - - assert!(new_perm.allows(&request)); - } -} From aa95db1fb42db64030c6e8d47dd63bc2b5110706 Mon Sep 17 00:00:00 2001 From: turtton Date: Thu, 12 Mar 2026 16:06:33 +0900 Subject: [PATCH 58/59] :recycle: Refactor Account deletion to deactivation with cascade cleanup MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Account: Deleted → Deactivated (論理削除、entity は Some のまま deleted_at をセット) - serde alias "deleted" で旧イベント後方互換性を維持 - 二重退会防止ガード追加 - Profile/AuthAccount: 独立した Deleted イベントを除去(ドメイン的に不要) - AccountApplier: 退会時のカスケード処理を追加 - Profile 削除、全 Metadata 削除、全 Follow 削除、auth link 解除 - AccountReadModel: delete() → deactivate() + unlink_all_auth_accounts() 追加 - update() SQL に deleted_at カラムを追加 - DeleteProfileUseCase を完全削除、Profile の DELETE エンドポイントを除去 - DeleteAccountUseCase → DeactivateAccountUseCase にリネーム --- adapter/src/processor/account.rs | 6 +- adapter/src/processor/auth_account.rs | 30 ------ adapter/src/processor/profile.rs | 26 ----- application/src/permission.rs | 2 +- application/src/service/account.rs | 12 +-- application/src/service/auth_account.rs | 4 - application/src/service/profile.rs | 66 ------------- driver/src/database/postgres/account.rs | 34 +++++-- .../database/postgres/account_event_store.rs | 4 +- .../postgres/auth_account_event_store.rs | 28 +----- driver/src/database/postgres/follow.rs | 20 ++-- driver/src/database/postgres/metadata.rs | 10 +- driver/src/database/postgres/profile.rs | 10 +- .../database/postgres/profile_event_store.rs | 34 +------ kernel/src/entity/account.rs | 73 +++++++++++--- kernel/src/entity/auth_account.rs | 43 -------- kernel/src/entity/profile.rs | 65 ------------ kernel/src/read_model/account.rs | 8 +- server/src/applier/account_applier.rs | 98 ++++++++++++++++--- server/src/route/account.rs | 8 +- server/src/route/profile.rs | 35 +------ 21 files changed, 229 insertions(+), 387 deletions(-) diff --git a/adapter/src/processor/account.rs b/adapter/src/processor/account.rs index cc6cbf8..2235eca 100644 --- a/adapter/src/processor/account.rs +++ b/adapter/src/processor/account.rs @@ -41,7 +41,7 @@ pub trait AccountCommandProcessor: Send + Sync + 'static { current_version: kernel::prelude::entity::EventVersion, ) -> impl Future> + Send; - fn delete( + fn deactivate( &self, executor: &mut Self::Executor, account_id: AccountId, @@ -116,13 +116,13 @@ where Ok(()) } - async fn delete( + async fn deactivate( &self, executor: &mut Self::Executor, account_id: AccountId, current_version: kernel::prelude::entity::EventVersion, ) -> error_stack::Result<(), KernelError> { - let command = Account::delete(account_id.clone(), current_version); + let command = Account::deactivate(account_id.clone(), current_version); self.account_event_store() .persist_and_transform(executor, command) diff --git a/adapter/src/processor/auth_account.rs b/adapter/src/processor/auth_account.rs index 4e3a937..e773b2f 100644 --- a/adapter/src/processor/auth_account.rs +++ b/adapter/src/processor/auth_account.rs @@ -26,13 +26,6 @@ pub trait AuthAccountCommandProcessor: Send + Sync + 'static { host: AuthHostId, client_id: AuthAccountClientId, ) -> impl Future> + Send; - - fn delete( - &self, - executor: &mut Self::Executor, - auth_account_id: AuthAccountId, - current_version: kernel::prelude::entity::EventVersion, - ) -> impl Future> + Send; } impl AuthAccountCommandProcessor for T @@ -77,29 +70,6 @@ where Ok(auth_account) } - - async fn delete( - &self, - executor: &mut Self::Executor, - auth_account_id: AuthAccountId, - current_version: kernel::prelude::entity::EventVersion, - ) -> error_stack::Result<(), KernelError> { - let command = AuthAccount::delete(auth_account_id.clone(), current_version); - - self.auth_account_event_store() - .persist_and_transform(executor, command) - .await?; - - self.auth_account_read_model() - .delete(executor, &auth_account_id) - .await?; - - if let Err(e) = self.auth_account_signal().emit(auth_account_id).await { - tracing::warn!("Failed to emit auth account signal: {:?}", e); - } - - Ok(()) - } } pub trait DependOnAuthAccountCommandProcessor: DependOnDatabaseConnection + Send + Sync { diff --git a/adapter/src/processor/profile.rs b/adapter/src/processor/profile.rs index 2304850..968d242 100644 --- a/adapter/src/processor/profile.rs +++ b/adapter/src/processor/profile.rs @@ -44,13 +44,6 @@ pub trait ProfileCommandProcessor: Send + Sync + 'static { banner: Option, current_version: EventVersion, ) -> impl Future> + Send; - - fn delete( - &self, - executor: &mut Self::Executor, - profile_id: ProfileId, - current_version: EventVersion, - ) -> impl Future> + Send; } impl ProfileCommandProcessor for T @@ -129,25 +122,6 @@ where Ok(()) } - - async fn delete( - &self, - executor: &mut Self::Executor, - profile_id: ProfileId, - current_version: EventVersion, - ) -> error_stack::Result<(), KernelError> { - let command = Profile::delete(profile_id.clone(), current_version); - - self.profile_event_store() - .persist_and_transform(executor, command) - .await?; - - if let Err(e) = self.profile_signal().emit(profile_id).await { - tracing::warn!("Failed to emit profile signal: {:?}", e); - } - - Ok(()) - } } pub trait DependOnProfileCommandProcessor: DependOnDatabaseConnection + Send + Sync { diff --git a/application/src/permission.rs b/application/src/permission.rs index da7c4de..5fd9b10 100644 --- a/application/src/permission.rs +++ b/application/src/permission.rs @@ -19,7 +19,7 @@ pub fn account_edit(account_id: &AccountId) -> Permission { )) } -pub fn account_delete(account_id: &AccountId) -> Permission { +pub fn account_deactivate(account_id: &AccountId) -> Permission { Permission::new(PermissionReq::new( Resource::Account(account_id.clone()), [Relation::Owner], diff --git a/application/src/service/account.rs b/application/src/service/account.rs index 63b7bbe..5240212 100644 --- a/application/src/service/account.rs +++ b/application/src/service/account.rs @@ -1,4 +1,4 @@ -use crate::permission::{account_delete, account_edit, account_view, check_permission}; +use crate::permission::{account_deactivate, account_edit, account_view, check_permission}; use crate::transfer::account::AccountDto; use crate::transfer::pagination::{apply_pagination, Pagination}; use adapter::crypto::{DependOnSigningKeyGenerator, SigningKeyGenerator}; @@ -205,7 +205,7 @@ impl EditAccountUseCase for T where { } -pub trait DeleteAccountUseCase: +pub trait DeactivateAccountUseCase: 'static + Sync + Send @@ -214,7 +214,7 @@ pub trait DeleteAccountUseCase: + DependOnPermissionChecker + DependOnPermissionWriter { - fn delete_account( + fn deactivate_account( &self, auth_account_id: &AuthAccountId, account_id: String, @@ -234,12 +234,12 @@ pub trait DeleteAccountUseCase: )) })?; - check_permission(self, auth_account_id, &account_delete(account.id())).await?; + check_permission(self, auth_account_id, &account_deactivate(account.id())).await?; let account_id = account.id().clone(); let current_version = account.version().clone(); self.account_command_processor() - .delete(&mut transaction, account_id.clone(), current_version) + .deactivate(&mut transaction, account_id.clone(), current_version) .await?; self.permission_writer() @@ -255,7 +255,7 @@ pub trait DeleteAccountUseCase: } } -impl DeleteAccountUseCase for T where +impl DeactivateAccountUseCase for T where T: 'static + DependOnAccountCommandProcessor + DependOnAccountQueryProcessor diff --git a/application/src/service/auth_account.rs b/application/src/service/auth_account.rs index 666fea6..111c58a 100644 --- a/application/src/service/auth_account.rs +++ b/application/src/service/auth_account.rs @@ -39,10 +39,6 @@ pub trait UpdateAuthAccount: self.auth_account_read_model() .update(&mut transaction, &auth_account) .await?; - } else { - self.auth_account_read_model() - .delete(&mut transaction, &auth_account_id) - .await?; } } } else { diff --git a/application/src/service/profile.rs b/application/src/service/profile.rs index 3a441d7..ffd665d 100644 --- a/application/src/service/profile.rs +++ b/application/src/service/profile.rs @@ -51,10 +51,6 @@ pub trait UpdateProfile: self.profile_read_model() .update(&mut transaction, &profile) .await?; - } else { - self.profile_read_model() - .delete(&mut transaction, &profile_id) - .await?; } } } else { @@ -286,65 +282,3 @@ impl EditProfileUseCase for T where + DependOnPermissionChecker { } - -pub trait DeleteProfileUseCase: - 'static - + Sync - + Send - + DependOnProfileCommandProcessor - + DependOnProfileQueryProcessor - + DependOnAccountQueryProcessor - + DependOnPermissionChecker -{ - fn delete_profile( - &self, - auth_account_id: &AuthAccountId, - account_nanoid: String, - ) -> impl Future> + Send { - async move { - let mut transaction = self.database_connection().begin_transaction().await?; - - let nanoid = kernel::prelude::entity::Nanoid::::new(account_nanoid); - let account = self - .account_query_processor() - .find_by_nanoid(&mut transaction, &nanoid) - .await? - .ok_or_else(|| { - Report::new(KernelError::NotFound).attach_printable(format!( - "Account not found with nanoid: {}", - nanoid.as_ref() - )) - })?; - - check_permission(self, auth_account_id, &account_edit(account.id())).await?; - - let profile = self - .profile_query_processor() - .find_by_account_id(&mut transaction, account.id()) - .await? - .ok_or_else(|| { - Report::new(KernelError::NotFound) - .attach_printable("Profile not found for this account") - })?; - - let profile_id = profile.id().clone(); - let current_version = profile.version().clone(); - self.profile_command_processor() - .delete(&mut transaction, profile_id, current_version) - .await?; - - Ok(()) - } - } -} - -impl DeleteProfileUseCase for T where - T: 'static - + Sync - + Send - + DependOnProfileCommandProcessor - + DependOnProfileQueryProcessor - + DependOnAccountQueryProcessor - + DependOnPermissionChecker -{ -} diff --git a/driver/src/database/postgres/account.rs b/driver/src/database/postgres/account.rs index fd96611..3a1704c 100644 --- a/driver/src/database/postgres/account.rs +++ b/driver/src/database/postgres/account.rs @@ -166,7 +166,7 @@ impl AccountReadModel for PostgresAccountReadModel { //language=postgresql r#" UPDATE accounts - SET name = $2, private_key = $3, public_key = $4, is_bot = $5, version = $6 + SET name = $2, private_key = $3, public_key = $4, is_bot = $5, version = $6, deleted_at = $7 WHERE id = $1 "#, ) @@ -176,13 +176,14 @@ impl AccountReadModel for PostgresAccountReadModel { .bind(account.public_key().as_ref()) .bind(account.is_bot().as_ref()) .bind(account.version().as_ref()) + .bind(account.deleted_at().as_ref().map(|d| d.as_ref())) .execute(con) .await .convert_error()?; Ok(()) } - async fn delete( + async fn deactivate( &self, executor: &mut Self::Executor, account_id: &AccountId, @@ -203,6 +204,25 @@ impl AccountReadModel for PostgresAccountReadModel { Ok(()) } + async fn unlink_all_auth_accounts( + &self, + executor: &mut Self::Executor, + account_id: &AccountId, + ) -> error_stack::Result<(), KernelError> { + let con: &mut PgConnection = executor; + sqlx::query( + //language=postgresql + r#" + DELETE FROM auth_emumet_accounts WHERE emumet_id = $1 + "#, + ) + .bind(account_id.as_ref()) + .execute(con) + .await + .convert_error()?; + Ok(()) + } + async fn link_auth_account( &self, executor: &mut Self::Executor, @@ -323,7 +343,7 @@ mod test { assert_eq!(result.as_ref().map(Account::id), Some(account.id())); database .account_read_model() - .delete(&mut transaction, account.id()) + .deactivate(&mut transaction, account.id()) .await .unwrap(); } @@ -360,7 +380,7 @@ mod test { assert_eq!(result.as_ref().map(Account::id), Some(account.id())); database .account_read_model() - .delete(&mut transaction, account.id()) + .deactivate(&mut transaction, account.id()) .await .unwrap(); } @@ -444,7 +464,7 @@ mod test { #[test_with::env(DATABASE_URL)] #[tokio::test] - async fn delete() { + async fn deactivate() { let database = PostgresDatabase::new().await.unwrap(); let mut transaction = database.begin_transaction().await.unwrap(); @@ -467,7 +487,7 @@ mod test { database .account_read_model() - .delete(&mut transaction, account.id()) + .deactivate(&mut transaction, account.id()) .await .unwrap(); let result = database @@ -497,7 +517,7 @@ mod test { database .account_read_model() - .delete(&mut transaction, account.id()) + .deactivate(&mut transaction, account.id()) .await .unwrap(); let result = database diff --git a/driver/src/database/postgres/account_event_store.rs b/driver/src/database/postgres/account_event_store.rs index a8598b2..d6865a0 100644 --- a/driver/src/database/postgres/account_event_store.rs +++ b/driver/src/database/postgres/account_event_store.rs @@ -247,7 +247,7 @@ mod test { update_event, None, ); - let delete_event = AccountEvent::Deleted; + let delete_event = AccountEvent::Deactivated; let deleted_account = CommandEnvelope::new( EventId::from(account_id.clone()), delete_event.name(), @@ -296,7 +296,7 @@ mod test { update_event, None, ); - let delete_event = AccountEvent::Deleted; + let delete_event = AccountEvent::Deactivated; let deleted_account = CommandEnvelope::new( EventId::from(account_id.clone()), delete_event.name(), diff --git a/driver/src/database/postgres/auth_account_event_store.rs b/driver/src/database/postgres/auth_account_event_store.rs index 4a1a03a..d005732 100644 --- a/driver/src/database/postgres/auth_account_event_store.rs +++ b/driver/src/database/postgres/auth_account_event_store.rs @@ -235,14 +235,8 @@ mod test { assert_eq!(events.len(), 0); let created = create_auth_account_command(id.clone()); - let create_envelope = db - .auth_account_event_store() - .persist_and_transform(&mut transaction, created.clone()) - .await - .unwrap(); - let deleted = AuthAccount::delete(id.clone(), create_envelope.version); db.auth_account_event_store() - .persist(&mut transaction, &deleted) + .persist_and_transform(&mut transaction, created.clone()) .await .unwrap(); @@ -251,9 +245,8 @@ mod test { .find_by_id(&mut transaction, &event_id, None) .await .unwrap(); - assert_eq!(events.len(), 2); + assert_eq!(events.len(), 1); assert_eq!(&events[0].event, created.event()); - assert_eq!(&events[1].event, deleted.event()); } #[test_with::env(DATABASE_URL)] @@ -270,30 +263,17 @@ mod test { .persist_and_transform(&mut transaction, created.clone()) .await .unwrap(); - let deleted = AuthAccount::delete(id.clone(), create_envelope.version); - db.auth_account_event_store() - .persist(&mut transaction, &deleted) - .await - .unwrap(); let all_events = db .auth_account_event_store() .find_by_id(&mut transaction, &event_id, None) .await .unwrap(); - assert_eq!(all_events.len(), 2); - - let since_events = db - .auth_account_event_store() - .find_by_id(&mut transaction, &event_id, Some(&all_events[0].version)) - .await - .unwrap(); - assert_eq!(since_events.len(), 1); - assert_eq!(&since_events[0].event, deleted.event()); + assert_eq!(all_events.len(), 1); let no_events = db .auth_account_event_store() - .find_by_id(&mut transaction, &event_id, Some(&all_events[1].version)) + .find_by_id(&mut transaction, &event_id, Some(&create_envelope.version)) .await .unwrap(); assert_eq!(no_events.len(), 0); diff --git a/driver/src/database/postgres/follow.rs b/driver/src/database/postgres/follow.rs index 86c44f2..985eae6 100644 --- a/driver/src/database/postgres/follow.rs +++ b/driver/src/database/postgres/follow.rs @@ -304,12 +304,12 @@ mod test { .unwrap(); database .account_read_model() - .delete(&mut transaction, follower_account.id()) + .deactivate(&mut transaction, follower_account.id()) .await .unwrap(); database .account_read_model() - .delete(&mut transaction, followee_account.id()) + .deactivate(&mut transaction, followee_account.id()) .await .unwrap(); } @@ -387,12 +387,12 @@ mod test { .unwrap(); database .account_read_model() - .delete(&mut transaction, follower_account.id()) + .deactivate(&mut transaction, follower_account.id()) .await .unwrap(); database .account_read_model() - .delete(&mut transaction, followee_account.id()) + .deactivate(&mut transaction, followee_account.id()) .await .unwrap(); } @@ -468,12 +468,12 @@ mod test { .unwrap(); database .account_read_model() - .delete(&mut transaction, follower_account.id()) + .deactivate(&mut transaction, follower_account.id()) .await .unwrap(); database .account_read_model() - .delete(&mut transaction, followee_account.id()) + .deactivate(&mut transaction, followee_account.id()) .await .unwrap(); } @@ -565,12 +565,12 @@ mod test { .unwrap(); database .account_read_model() - .delete(&mut transaction, follower_account.id()) + .deactivate(&mut transaction, follower_account.id()) .await .unwrap(); database .account_read_model() - .delete(&mut transaction, followee_account.id()) + .deactivate(&mut transaction, followee_account.id()) .await .unwrap(); } @@ -642,12 +642,12 @@ mod test { assert!(following.is_empty()); database .account_read_model() - .delete(&mut transaction, follower_account.id()) + .deactivate(&mut transaction, follower_account.id()) .await .unwrap(); database .account_read_model() - .delete(&mut transaction, followee_account.id()) + .deactivate(&mut transaction, followee_account.id()) .await .unwrap(); } diff --git a/driver/src/database/postgres/metadata.rs b/driver/src/database/postgres/metadata.rs index d8d24fe..805bdc6 100644 --- a/driver/src/database/postgres/metadata.rs +++ b/driver/src/database/postgres/metadata.rs @@ -235,7 +235,7 @@ mod test { database .account_read_model() - .delete(&mut transaction, account.id()) + .deactivate(&mut transaction, account.id()) .await .unwrap(); } @@ -295,7 +295,7 @@ mod test { database .account_read_model() - .delete(&mut transaction, account.id()) + .deactivate(&mut transaction, account.id()) .await .unwrap(); } @@ -334,7 +334,7 @@ mod test { database .account_read_model() - .delete(&mut transaction, account.id()) + .deactivate(&mut transaction, account.id()) .await .unwrap(); } @@ -387,7 +387,7 @@ mod test { database .account_read_model() - .delete(&mut transaction, account.id()) + .deactivate(&mut transaction, account.id()) .await .unwrap(); } @@ -429,7 +429,7 @@ mod test { database .account_read_model() - .delete(&mut transaction, account.id()) + .deactivate(&mut transaction, account.id()) .await .unwrap(); } diff --git a/driver/src/database/postgres/profile.rs b/driver/src/database/postgres/profile.rs index 3c21f3c..6728355 100644 --- a/driver/src/database/postgres/profile.rs +++ b/driver/src/database/postgres/profile.rs @@ -248,7 +248,7 @@ mod test { database .account_read_model() - .delete(&mut transaction, account.id()) + .deactivate(&mut transaction, account.id()) .await .unwrap(); } @@ -292,7 +292,7 @@ mod test { database .account_read_model() - .delete(&mut transaction, account.id()) + .deactivate(&mut transaction, account.id()) .await .unwrap(); } @@ -329,7 +329,7 @@ mod test { database .account_read_model() - .delete(&mut transaction, account.id()) + .deactivate(&mut transaction, account.id()) .await .unwrap(); } @@ -384,7 +384,7 @@ mod test { database .account_read_model() - .delete(&mut transaction, account.id()) + .deactivate(&mut transaction, account.id()) .await .unwrap(); } @@ -426,7 +426,7 @@ mod test { database .account_read_model() - .delete(&mut transaction, account.id()) + .deactivate(&mut transaction, account.id()) .await .unwrap(); } diff --git a/driver/src/database/postgres/profile_event_store.rs b/driver/src/database/postgres/profile_event_store.rs index 4a2f2c8..f00f09e 100644 --- a/driver/src/database/postgres/profile_event_store.rs +++ b/driver/src/database/postgres/profile_event_store.rs @@ -250,13 +250,6 @@ mod test { update_event, None, ); - let delete_event = ProfileEvent::Deleted; - let deleted_profile = CommandEnvelope::new( - EventId::from(profile_id.clone()), - delete_event.name(), - delete_event, - None, - ); db.profile_event_store() .persist(&mut transaction, &created_profile) @@ -266,19 +259,14 @@ mod test { .persist(&mut transaction, &updated_profile) .await .unwrap(); - db.profile_event_store() - .persist(&mut transaction, &deleted_profile) - .await - .unwrap(); let events = db .profile_event_store() .find_by_id(&mut transaction, &event_id, None) .await .unwrap(); - assert_eq!(events.len(), 3); + assert_eq!(events.len(), 2); assert_eq!(&events[0].event, created_profile.event()); assert_eq!(&events[1].event, updated_profile.event()); - assert_eq!(&events[2].event, deleted_profile.event()); } #[test_with::env(DATABASE_URL)] @@ -302,13 +290,6 @@ mod test { update_event, None, ); - let delete_event = ProfileEvent::Deleted; - let deleted_profile = CommandEnvelope::new( - EventId::from(profile_id.clone()), - delete_event.name(), - delete_event, - None, - ); db.profile_event_store() .persist(&mut transaction, &created_profile) @@ -318,10 +299,6 @@ mod test { .persist(&mut transaction, &updated_profile) .await .unwrap(); - db.profile_event_store() - .persist(&mut transaction, &deleted_profile) - .await - .unwrap(); // Get all events to obtain the first version let all_events = db @@ -329,22 +306,21 @@ mod test { .find_by_id(&mut transaction, &event_id, None) .await .unwrap(); - assert_eq!(all_events.len(), 3); + assert_eq!(all_events.len(), 2); - // Query since the first event's version — should return the 2nd and 3rd events + // Query since the first event's version — should return the 2nd event let since_events = db .profile_event_store() .find_by_id(&mut transaction, &event_id, Some(&all_events[0].version)) .await .unwrap(); - assert_eq!(since_events.len(), 2); + assert_eq!(since_events.len(), 1); assert_eq!(&since_events[0].event, updated_profile.event()); - assert_eq!(&since_events[1].event, deleted_profile.event()); // Query since the last event's version — should return no events let no_events = db .profile_event_store() - .find_by_id(&mut transaction, &event_id, Some(&all_events[2].version)) + .find_by_id(&mut transaction, &event_id, Some(&all_events[1].version)) .await .unwrap(); assert_eq!(no_events.len(), 0); diff --git a/kernel/src/entity/account.rs b/kernel/src/entity/account.rs index 9809947..3649933 100644 --- a/kernel/src/entity/account.rs +++ b/kernel/src/entity/account.rs @@ -53,7 +53,8 @@ pub enum AccountEvent { Updated { is_bot: AccountIsBot, }, - Deleted, + #[serde(alias = "deleted")] + Deactivated, } impl Account { @@ -96,11 +97,11 @@ impl Account { ) } - pub fn delete( + pub fn deactivate( id: AccountId, current_version: EventVersion, ) -> CommandEnvelope { - let event = AccountEvent::Deleted; + let event = AccountEvent::Deactivated; CommandEnvelope::new( EventId::from(id), event.name(), @@ -169,9 +170,14 @@ impl EventApplier for Account { .attach_printable(Self::not_exists(event.id.as_ref()))); } } - AccountEvent::Deleted => { - if entity.is_some() { - *entity = None; + AccountEvent::Deactivated => { + if let Some(account) = entity { + if account.deleted_at.is_some() { + return Err(Report::new(KernelError::Internal) + .attach_printable("Account is already deactivated")); + } + account.deleted_at = Some(DeletedAt::now()); + account.version = event.version; } else { return Err(Report::new(KernelError::Internal) .attach_printable(Self::not_exists(event.id.as_ref()))); @@ -312,7 +318,7 @@ mod test { } #[test] - fn delete_account() { + fn deactivate_account() { let id = AccountId::new(Uuid::now_v7()); let name = AccountName::new("test"); let private_key = AccountPrivateKey::new("private_key".to_string()); @@ -331,7 +337,7 @@ mod test { CreatedAt::now(), ); let version = account.version().clone(); - let event = Account::delete(id.clone(), version); + let event = Account::deactivate(id.clone(), version); let envelope = EventEnvelope::new( event.id().clone(), event.event().clone(), @@ -339,14 +345,59 @@ mod test { ); let mut account = Some(account); Account::apply(&mut account, envelope.clone()).unwrap(); - assert!(account.is_none()); + assert!(account.is_some()); + let account = account.unwrap(); + assert!(account.deleted_at().is_some()); + } + + #[test] + fn deactivate_already_deactivated_account() { + let id = AccountId::new(Uuid::now_v7()); + let name = AccountName::new("test"); + let private_key = AccountPrivateKey::new("private_key".to_string()); + let public_key = AccountPublicKey::new("public_key".to_string()); + let is_bot = AccountIsBot::new(false); + let nano_id = Nanoid::default(); + let account = Account::new( + id.clone(), + name.clone(), + private_key.clone(), + public_key.clone(), + is_bot.clone(), + None, + EventVersion::new(Uuid::now_v7()), + nano_id.clone(), + CreatedAt::now(), + ); + let version = account.version().clone(); + let event = Account::deactivate(id.clone(), version); + let envelope = EventEnvelope::new( + event.id().clone(), + event.event().clone(), + EventVersion::new(Uuid::now_v7()), + ); + let mut account = Some(account); + Account::apply(&mut account, envelope).unwrap(); + assert!(account.is_some()); + assert!(account.as_ref().unwrap().deleted_at().is_some()); + + // Second deactivation should fail + let version2 = account.as_ref().unwrap().version().clone(); + let event2 = Account::deactivate(id.clone(), version2); + let envelope2 = EventEnvelope::new( + event2.id().clone(), + event2.event().clone(), + EventVersion::new(Uuid::now_v7()), + ); + assert!(Account::apply(&mut account, envelope2) + .is_err_and(|e| e.current_context() == &KernelError::Internal)); } #[test] - fn delete_not_exist_account() { + fn deactivate_not_exist_account() { let id = AccountId::new(Uuid::now_v7()); let version = EventVersion::new(Uuid::now_v7()); - let event = Account::delete(id.clone(), version); + let event = Account::deactivate(id.clone(), version); let envelope = EventEnvelope::new( event.id().clone(), event.event().clone(), diff --git a/kernel/src/entity/auth_account.rs b/kernel/src/entity/auth_account.rs index 7ceca96..6bb8765 100644 --- a/kernel/src/entity/auth_account.rs +++ b/kernel/src/entity/auth_account.rs @@ -32,7 +32,6 @@ pub enum AuthAccountEvent { host: AuthHostId, client_id: AuthAccountClientId, }, - Deleted, } impl AuthAccount { @@ -49,19 +48,6 @@ impl AuthAccount { Some(KnownEventVersion::Nothing), ) } - - pub fn delete( - id: AuthAccountId, - current_version: EventVersion, - ) -> CommandEnvelope { - let event = AuthAccountEvent::Deleted; - CommandEnvelope::new( - EventId::from(id), - event.name(), - event, - Some(KnownEventVersion::Prev(current_version)), - ) - } } impl EventApplier for AuthAccount { @@ -88,13 +74,6 @@ impl EventApplier for AuthAccount { version: event.version, }); } - AuthAccountEvent::Deleted => { - if entity.is_none() { - return Err(Report::new(KernelError::Internal) - .attach_printable(Self::not_exists(event.id.as_ref()))); - } - *entity = None; - } } Ok(()) } @@ -127,26 +106,4 @@ mod test { assert_eq!(account.host(), &host); assert_eq!(account.client_id(), &client_id); } - - #[test] - fn delete_auth_account() { - let id = AuthAccountId::new(Uuid::now_v7()); - let host = AuthHostId::new(Uuid::now_v7()); - let client_id = AuthAccountClientId::new(Uuid::now_v7()); - let account = AuthAccount::new( - id.clone(), - host.clone(), - client_id.clone(), - EventVersion::new(Uuid::now_v7()), - ); - let delete_account = AuthAccount::delete(id.clone(), account.version().clone()); - let envelope = EventEnvelope::new( - delete_account.id().clone(), - delete_account.event().clone(), - EventVersion::new(Uuid::now_v7()), - ); - let mut account = Some(account); - AuthAccount::apply(&mut account, envelope).unwrap(); - assert!(account.is_none()); - } } diff --git a/kernel/src/entity/profile.rs b/kernel/src/entity/profile.rs index 8c65845..4eaa879 100644 --- a/kernel/src/entity/profile.rs +++ b/kernel/src/entity/profile.rs @@ -49,7 +49,6 @@ pub enum ProfileEvent { icon: Option, banner: Option, }, - Deleted, } impl Profile { @@ -99,19 +98,6 @@ impl Profile { Some(KnownEventVersion::Prev(current_version)), ) } - - pub fn delete( - id: ProfileId, - current_version: EventVersion, - ) -> CommandEnvelope { - let event = ProfileEvent::Deleted; - CommandEnvelope::new( - EventId::from(id), - event.name(), - event, - Some(KnownEventVersion::Prev(current_version)), - ) - } } impl EventApplier for Profile { @@ -171,14 +157,6 @@ impl EventApplier for Profile { .attach_printable(Self::not_exists(event.id.as_ref()))); } } - ProfileEvent::Deleted => { - if entity.is_some() { - *entity = None; - } else { - return Err(Report::new(KernelError::Internal) - .attach_printable(Self::not_exists(event.id.as_ref()))); - } - } } Ok(()) } @@ -191,7 +169,6 @@ mod test { ProfileId, ProfileSummary, }; use crate::event::EventApplier; - use crate::KernelError; use uuid::Uuid; #[test] @@ -273,46 +250,4 @@ mod test { assert_eq!(profile.version(), &version); assert_eq!(profile.nanoid(), &nano_id); } - - #[test] - fn delete_profile() { - let account_id = AccountId::new(Uuid::now_v7()); - let id = ProfileId::new(Uuid::now_v7()); - let nano_id = Nanoid::default(); - let profile = Profile::new( - id.clone(), - account_id.clone(), - None, - None, - None, - None, - EventVersion::new(Uuid::now_v7()), - nano_id.clone(), - ); - let version = profile.version().clone(); - let event = Profile::delete(id.clone(), version); - let envelope = EventEnvelope::new( - event.id().clone(), - event.event().clone(), - EventVersion::new(Uuid::now_v7()), - ); - let mut profile = Some(profile); - Profile::apply(&mut profile, envelope).unwrap(); - assert!(profile.is_none()); - } - - #[test] - fn delete_not_exist_profile() { - let id = ProfileId::new(Uuid::now_v7()); - let version = EventVersion::new(Uuid::now_v7()); - let event = Profile::delete(id.clone(), version); - let envelope = EventEnvelope::new( - event.id().clone(), - event.event().clone(), - EventVersion::new(Uuid::now_v7()), - ); - let mut profile = None; - assert!(Profile::apply(&mut profile, envelope) - .is_err_and(|e| e.current_context() == &KernelError::Internal)); - } } diff --git a/kernel/src/read_model/account.rs b/kernel/src/read_model/account.rs index edd82d0..58cc4d4 100644 --- a/kernel/src/read_model/account.rs +++ b/kernel/src/read_model/account.rs @@ -44,7 +44,13 @@ pub trait AccountReadModel: Sync + Send + 'static { account: &Account, ) -> impl Future> + Send; - fn delete( + fn deactivate( + &self, + executor: &mut Self::Executor, + account_id: &AccountId, + ) -> impl Future> + Send; + + fn unlink_all_auth_accounts( &self, executor: &mut Self::Executor, account_id: &AccountId, diff --git a/server/src/applier/account_applier.rs b/server/src/applier/account_applier.rs index c249248..97ff87a 100644 --- a/server/src/applier/account_applier.rs +++ b/server/src/applier/account_applier.rs @@ -3,9 +3,13 @@ use error_stack::ResultExt; use kernel::interfaces::database::{DatabaseConnection, DependOnDatabaseConnection}; use kernel::interfaces::event::EventApplier; use kernel::interfaces::event_store::{AccountEventStore, DependOnAccountEventStore}; -use kernel::interfaces::read_model::{AccountReadModel, DependOnAccountReadModel}; +use kernel::interfaces::read_model::{ + AccountReadModel, DependOnAccountReadModel, DependOnMetadataReadModel, + DependOnProfileReadModel, MetadataReadModel, ProfileReadModel, +}; +use kernel::interfaces::repository::{DependOnFollowRepository, FollowRepository}; use kernel::interfaces::signal::Signal; -use kernel::prelude::entity::{Account, AccountEvent, AccountId, EventId}; +use kernel::prelude::entity::{Account, AccountEvent, AccountId, EventId, FollowTargetId}; use kernel::KernelError; use rikka_mq::config::MQConfig; use rikka_mq::define::redis::mq::RedisMessageQueue; @@ -86,18 +90,88 @@ impl AccountApplier { } } (Some(account), Some(_)) => { - handler - .account_read_model() - .update(&mut tx, account) - .await - .map_err(|e| ErrorOperation::Delay(format!("{:?}", e)))?; + if account.deleted_at().is_some() { + // Account deactivated: cascade delete related data + handler + .account_read_model() + .deactivate(&mut tx, &id) + .await + .map_err(|e| ErrorOperation::Delay(format!("{:?}", e)))?; + + // Delete profile + if let Some(profile) = handler + .profile_read_model() + .find_by_account_id(&mut tx, &id) + .await + .map_err(|e| ErrorOperation::Delay(format!("{:?}", e)))? + { + handler + .profile_read_model() + .delete(&mut tx, profile.id()) + .await + .map_err(|e| ErrorOperation::Delay(format!("{:?}", e)))?; + } + + // Delete all metadata + let metadata_list = handler + .metadata_read_model() + .find_by_account_id(&mut tx, &id) + .await + .map_err(|e| ErrorOperation::Delay(format!("{:?}", e)))?; + for metadata in &metadata_list { + handler + .metadata_read_model() + .delete(&mut tx, metadata.id()) + .await + .map_err(|e| ErrorOperation::Delay(format!("{:?}", e)))?; + } + + // Delete all follow relationships (as follower and followee) + let target_id = FollowTargetId::from(id.clone()); + let followings = handler + .follow_repository() + .find_followings(&mut tx, &target_id) + .await + .map_err(|e| ErrorOperation::Delay(format!("{:?}", e)))?; + for follow in &followings { + handler + .follow_repository() + .delete(&mut tx, follow.id()) + .await + .map_err(|e| ErrorOperation::Delay(format!("{:?}", e)))?; + } + let followers = handler + .follow_repository() + .find_followers(&mut tx, &target_id) + .await + .map_err(|e| ErrorOperation::Delay(format!("{:?}", e)))?; + for follow in &followers { + handler + .follow_repository() + .delete(&mut tx, follow.id()) + .await + .map_err(|e| ErrorOperation::Delay(format!("{:?}", e)))?; + } + + // Unlink all auth accounts + handler + .account_read_model() + .unlink_all_auth_accounts(&mut tx, &id) + .await + .map_err(|e| ErrorOperation::Delay(format!("{:?}", e)))?; + } else { + handler + .account_read_model() + .update(&mut tx, account) + .await + .map_err(|e| ErrorOperation::Delay(format!("{:?}", e)))?; + } } (None, Some(_)) => { - handler - .account_read_model() - .delete(&mut tx, &id) - .await - .map_err(|e| ErrorOperation::Delay(format!("{:?}", e)))?; + tracing::warn!( + "Account applier: entity became None with existing projection for id {:?} — this should not happen after Deactivated migration", + id + ); } (None, None) => { tracing::warn!( diff --git a/server/src/route/account.rs b/server/src/route/account.rs index 81c37a7..acd543d 100644 --- a/server/src/route/account.rs +++ b/server/src/route/account.rs @@ -3,7 +3,7 @@ use crate::error::ErrorStatus; use crate::handler::AppModule; use crate::route::DirectionConverter; use application::service::account::{ - CreateAccountUseCase, DeleteAccountUseCase, EditAccountUseCase, GetAccountUseCase, + CreateAccountUseCase, DeactivateAccountUseCase, EditAccountUseCase, GetAccountUseCase, }; use application::transfer::pagination::Pagination; use axum::extract::{Path, Query, State}; @@ -187,7 +187,7 @@ async fn update_account_by_id( Ok(StatusCode::NO_CONTENT) } -async fn delete_account_by_id( +async fn deactivate_account_by_id( Extension(claims): Extension, State(module): State, Path(id): Path, @@ -206,7 +206,7 @@ async fn delete_account_by_id( .map_err(ErrorStatus::from)?; module - .delete_account(&auth_account_id, id) + .deactivate_account(&auth_account_id, id) .await .map_err(ErrorStatus::from)?; @@ -219,6 +219,6 @@ impl AccountRouter for Router { .route("/accounts", post(create_account)) .route("/accounts/:id", get(get_account_by_id)) .route("/accounts/:id", put(update_account_by_id)) - .route("/accounts/:id", delete(delete_account_by_id)) + .route("/accounts/:id", delete(deactivate_account_by_id)) } } diff --git a/server/src/route/profile.rs b/server/src/route/profile.rs index d2e09b7..24a59b2 100644 --- a/server/src/route/profile.rs +++ b/server/src/route/profile.rs @@ -1,9 +1,7 @@ use crate::auth::{resolve_auth_account_id, AuthClaims, OidcAuthInfo}; use crate::error::ErrorStatus; use crate::handler::AppModule; -use application::service::profile::{ - CreateProfileUseCase, DeleteProfileUseCase, EditProfileUseCase, GetProfileUseCase, -}; +use application::service::profile::{CreateProfileUseCase, EditProfileUseCase, GetProfileUseCase}; use axum::extract::{Path, State}; use axum::http::StatusCode; use axum::routing::get; @@ -153,40 +151,11 @@ async fn update_profile( Ok(StatusCode::NO_CONTENT) } -async fn delete_profile( - Extension(claims): Extension, - State(module): State, - Path(account_id): Path, -) -> Result { - let auth_info = OidcAuthInfo::from(claims); - - if account_id.trim().is_empty() { - return Err(ErrorStatus::from(( - StatusCode::BAD_REQUEST, - "Account ID cannot be empty".to_string(), - ))); - } - - let auth_account_id = resolve_auth_account_id(&module, auth_info) - .await - .map_err(ErrorStatus::from)?; - - module - .delete_profile(&auth_account_id, account_id) - .await - .map_err(ErrorStatus::from)?; - - Ok(StatusCode::NO_CONTENT) -} - impl ProfileRouter for Router { fn route_profile(self) -> Self { self.route( "/accounts/:account_id/profile", - get(get_profile) - .post(create_profile) - .put(update_profile) - .delete(delete_profile), + get(get_profile).post(create_profile).put(update_profile), ) } } From 6ed0d4e5d34da9437e49694dde27156ca2133cc3 Mon Sep 17 00:00:00 2001 From: turtton Date: Thu, 12 Mar 2026 22:38:36 +0900 Subject: [PATCH 59/59] :sparkles: Replace individual GET endpoints with batch fetch APIs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 個別ID取得エンドポイントをバッチ取得APIに統一し、N+1問題を解消。 - GET /accounts?ids=a,b,c でアカウントのバッチ取得(既存のページネーション一覧と共存) - GET /profiles?account_ids=a,b,c でプロフィールのバッチ取得 - GET /metadata?account_ids=a,b,c でメタデータのバッチ取得 - GET /accounts/:id, GET /accounts/:account_id/profile(GET), GET /accounts/:account_id/metadata(GET) を削除 - CUD routes (POST/PUT/DELETE) は変更なし 各層の変更: - kernel: ReadModelにfind_by_nanoids/find_by_account_idsトレイトメソッド追加 - driver: WHERE ... = ANY($1) によるPostgreSQL実装 - adapter: QueryProcessorにバッチメソッド追加(blanket impl) - application: DTOにaccount_nanoidフィールド追加、バッチUseCaseメソッド追加 - server: バッチハンドラ追加、バッチサイズ上限(100)、空文字列フィルタ、ids+pagination排他チェック 権限チェックはアカウント単位でイテレートし、権限のないアカウントは結果から除外。 --- adapter/src/processor/account.rs | 16 ++++++ adapter/src/processor/metadata.rs | 16 ++++++ adapter/src/processor/profile.rs | 16 ++++++ application/src/service/account.rs | 33 ++++++------ application/src/service/metadata.rs | 54 +++++++++++++------ application/src/service/profile.rs | 64 ++++++++++++++-------- application/src/transfer/metadata.rs | 12 +++-- application/src/transfer/profile.rs | 18 ++++--- driver/src/database/postgres/account.rs | 22 ++++++++ driver/src/database/postgres/metadata.rs | 22 ++++++++ driver/src/database/postgres/profile.rs | 21 ++++++++ kernel/src/read_model/account.rs | 6 +++ kernel/src/read_model/metadata.rs | 6 +++ kernel/src/read_model/profile.rs | 6 +++ server/src/route.rs | 23 ++++++++ server/src/route/account.rs | 68 +++++++++--------------- server/src/route/metadata.rs | 41 +++++++------- server/src/route/profile.rs | 41 ++++++++------ 18 files changed, 343 insertions(+), 142 deletions(-) diff --git a/adapter/src/processor/account.rs b/adapter/src/processor/account.rs index 2235eca..0386667 100644 --- a/adapter/src/processor/account.rs +++ b/adapter/src/processor/account.rs @@ -180,6 +180,12 @@ pub trait AccountQueryProcessor: Send + Sync + 'static { executor: &mut Self::Executor, nanoid: &Nanoid, ) -> impl Future, KernelError>> + Send; + + fn find_by_nanoids( + &self, + executor: &mut Self::Executor, + nanoids: &[Nanoid], + ) -> impl Future, KernelError>> + Send; } impl AccountQueryProcessor for T @@ -216,6 +222,16 @@ where .find_by_nanoid(executor, nanoid) .await } + + async fn find_by_nanoids( + &self, + executor: &mut Self::Executor, + nanoids: &[Nanoid], + ) -> error_stack::Result, KernelError> { + self.account_read_model() + .find_by_nanoids(executor, nanoids) + .await + } } pub trait DependOnAccountQueryProcessor: DependOnDatabaseConnection + Send + Sync { diff --git a/adapter/src/processor/metadata.rs b/adapter/src/processor/metadata.rs index ce0f944..0d92437 100644 --- a/adapter/src/processor/metadata.rs +++ b/adapter/src/processor/metadata.rs @@ -164,6 +164,12 @@ pub trait MetadataQueryProcessor: Send + Sync + 'static { executor: &mut Self::Executor, account_id: &AccountId, ) -> impl Future, KernelError>> + Send; + + fn find_by_account_ids( + &self, + executor: &mut Self::Executor, + account_ids: &[AccountId], + ) -> impl Future, KernelError>> + Send; } impl MetadataQueryProcessor for T @@ -190,6 +196,16 @@ where .find_by_account_id(executor, account_id) .await } + + async fn find_by_account_ids( + &self, + executor: &mut Self::Executor, + account_ids: &[AccountId], + ) -> error_stack::Result, KernelError> { + self.metadata_read_model() + .find_by_account_ids(executor, account_ids) + .await + } } pub trait DependOnMetadataQueryProcessor: DependOnDatabaseConnection + Send + Sync { diff --git a/adapter/src/processor/profile.rs b/adapter/src/processor/profile.rs index 968d242..3200f78 100644 --- a/adapter/src/processor/profile.rs +++ b/adapter/src/processor/profile.rs @@ -162,6 +162,12 @@ pub trait ProfileQueryProcessor: Send + Sync + 'static { executor: &mut Self::Executor, account_id: &AccountId, ) -> impl Future, KernelError>> + Send; + + fn find_by_account_ids( + &self, + executor: &mut Self::Executor, + account_ids: &[AccountId], + ) -> impl Future, KernelError>> + Send; } impl ProfileQueryProcessor for T @@ -188,6 +194,16 @@ where .find_by_account_id(executor, account_id) .await } + + async fn find_by_account_ids( + &self, + executor: &mut Self::Executor, + account_ids: &[AccountId], + ) -> error_stack::Result, KernelError> { + self.profile_read_model() + .find_by_account_ids(executor, account_ids) + .await + } } pub trait DependOnProfileQueryProcessor: DependOnDatabaseConnection + Send + Sync { diff --git a/application/src/service/account.rs b/application/src/service/account.rs index 5240212..fb3e6db 100644 --- a/application/src/service/account.rs +++ b/application/src/service/account.rs @@ -51,29 +51,32 @@ pub trait GetAccountUseCase: } } - fn get_account_by_id( + fn get_accounts_by_ids( &self, auth_account_id: &AuthAccountId, - account_id: String, - ) -> impl Future> + Send { + ids: Vec, + ) -> impl Future, KernelError>> + Send { async move { let mut transaction = self.database_connection().begin_transaction().await?; - let nanoid = Nanoid::::new(account_id); - let account = self + let nanoids: Vec> = + ids.into_iter().map(Nanoid::::new).collect(); + let accounts = self .account_query_processor() - .find_by_nanoid(&mut transaction, &nanoid) - .await? - .ok_or_else(|| { - Report::new(KernelError::NotFound).attach_printable(format!( - "Account not found with nanoid: {}", - nanoid.as_ref() - )) - })?; + .find_by_nanoids(&mut transaction, &nanoids) + .await?; - check_permission(self, auth_account_id, &account_view(account.id())).await?; + let mut result = Vec::new(); + for account in accounts { + if check_permission(self, auth_account_id, &account_view(account.id())) + .await + .is_ok() + { + result.push(AccountDto::from(account)); + } + } - Ok(AccountDto::from(account)) + Ok(result) } } } diff --git a/application/src/service/metadata.rs b/application/src/service/metadata.rs index cf4e3cc..0412e8e 100644 --- a/application/src/service/metadata.rs +++ b/application/src/service/metadata.rs @@ -94,34 +94,55 @@ pub trait GetMetadataUseCase: + DependOnAccountQueryProcessor + DependOnPermissionChecker { - fn get_metadata( + fn get_metadata_batch( &self, auth_account_id: &AuthAccountId, - account_nanoid: String, + account_nanoids: Vec, ) -> impl Future, KernelError>> + Send { async move { let mut transaction = self.database_connection().begin_transaction().await?; - let nanoid = kernel::prelude::entity::Nanoid::::new(account_nanoid); - let account = self + let nanoids: Vec> = account_nanoids + .into_iter() + .map(Nanoid::::new) + .collect(); + let accounts = self .account_query_processor() - .find_by_nanoid(&mut transaction, &nanoid) - .await? - .ok_or_else(|| { - Report::new(KernelError::NotFound).attach_printable(format!( - "Account not found with nanoid: {}", - nanoid.as_ref() - )) - })?; + .find_by_nanoids(&mut transaction, &nanoids) + .await?; + + let mut permitted_accounts = Vec::new(); + for account in accounts { + if check_permission(self, auth_account_id, &account_view(account.id())) + .await + .is_ok() + { + permitted_accounts.push(account); + } + } + + if permitted_accounts.is_empty() { + return Ok(Vec::new()); + } - check_permission(self, auth_account_id, &account_view(account.id())).await?; + let account_ids: Vec<_> = permitted_accounts.iter().map(|a| a.id().clone()).collect(); + let nanoid_map: std::collections::HashMap<_, _> = permitted_accounts + .iter() + .map(|a| (a.id().clone(), a.nanoid().as_ref().to_string())) + .collect(); let metadata_list = self .metadata_query_processor() - .find_by_account_id(&mut transaction, account.id()) + .find_by_account_ids(&mut transaction, &account_ids) .await?; - Ok(metadata_list.into_iter().map(MetadataDto::from).collect()) + Ok(metadata_list + .into_iter() + .filter_map(|metadata| { + let account_nanoid = nanoid_map.get(metadata.account_id())?.clone(); + Some(MetadataDto::new(metadata, account_nanoid)) + }) + .collect()) } } } @@ -168,6 +189,7 @@ pub trait CreateMetadataUseCase: check_permission(self, auth_account_id, &account_edit(account.id())).await?; + let account_nanoid_str = account.nanoid().as_ref().to_string(); let account_id = account.id().clone(); let metadata_nanoid = Nanoid::::default(); let metadata = self @@ -181,7 +203,7 @@ pub trait CreateMetadataUseCase: ) .await?; - Ok(MetadataDto::from(metadata)) + Ok(MetadataDto::new(metadata, account_nanoid_str)) } } } diff --git a/application/src/service/profile.rs b/application/src/service/profile.rs index ffd665d..6e1acdb 100644 --- a/application/src/service/profile.rs +++ b/application/src/service/profile.rs @@ -88,38 +88,55 @@ pub trait GetProfileUseCase: + DependOnAccountQueryProcessor + DependOnPermissionChecker { - fn get_profile( + fn get_profiles_batch( &self, auth_account_id: &AuthAccountId, - account_nanoid: String, - ) -> impl Future> + Send { + account_nanoids: Vec, + ) -> impl Future, KernelError>> + Send { async move { let mut transaction = self.database_connection().begin_transaction().await?; - let nanoid = kernel::prelude::entity::Nanoid::::new(account_nanoid); - let account = self + let nanoids: Vec> = account_nanoids + .into_iter() + .map(Nanoid::::new) + .collect(); + let accounts = self .account_query_processor() - .find_by_nanoid(&mut transaction, &nanoid) - .await? - .ok_or_else(|| { - Report::new(KernelError::NotFound).attach_printable(format!( - "Account not found with nanoid: {}", - nanoid.as_ref() - )) - })?; + .find_by_nanoids(&mut transaction, &nanoids) + .await?; - check_permission(self, auth_account_id, &account_view(account.id())).await?; + let mut permitted_accounts = Vec::new(); + for account in accounts { + if check_permission(self, auth_account_id, &account_view(account.id())) + .await + .is_ok() + { + permitted_accounts.push(account); + } + } - let profile = self + if permitted_accounts.is_empty() { + return Ok(Vec::new()); + } + + let account_ids: Vec<_> = permitted_accounts.iter().map(|a| a.id().clone()).collect(); + let nanoid_map: std::collections::HashMap<_, _> = permitted_accounts + .iter() + .map(|a| (a.id().clone(), a.nanoid().as_ref().to_string())) + .collect(); + + let profiles = self .profile_query_processor() - .find_by_account_id(&mut transaction, account.id()) - .await? - .ok_or_else(|| { - Report::new(KernelError::NotFound) - .attach_printable("Profile not found for this account") - })?; + .find_by_account_ids(&mut transaction, &account_ids) + .await?; - Ok(ProfileDto::from(profile)) + Ok(profiles + .into_iter() + .filter_map(|profile| { + let account_nanoid = nanoid_map.get(profile.account_id())?.clone(); + Some(ProfileDto::new(profile, account_nanoid)) + }) + .collect()) } } } @@ -178,6 +195,7 @@ pub trait CreateProfileUseCase: .attach_printable("Profile already exists for this account")); } + let account_nanoid_str = account.nanoid().as_ref().to_string(); let account_id = account.id().clone(); let profile_nanoid = Nanoid::::default(); let profile = self @@ -193,7 +211,7 @@ pub trait CreateProfileUseCase: ) .await?; - Ok(ProfileDto::from(profile)) + Ok(ProfileDto::new(profile, account_nanoid_str)) } } } diff --git a/application/src/transfer/metadata.rs b/application/src/transfer/metadata.rs index 24b0b4e..b8f4fcd 100644 --- a/application/src/transfer/metadata.rs +++ b/application/src/transfer/metadata.rs @@ -2,14 +2,16 @@ use kernel::prelude::entity::Metadata; #[derive(Debug)] pub struct MetadataDto { + pub account_nanoid: String, pub nanoid: String, pub label: String, pub content: String, } -impl From for MetadataDto { - fn from(metadata: Metadata) -> Self { +impl MetadataDto { + pub fn new(metadata: Metadata, account_nanoid: String) -> Self { Self { + account_nanoid, nanoid: metadata.nanoid().as_ref().to_string(), label: metadata.label().as_ref().to_string(), content: metadata.content().as_ref().to_string(), @@ -26,13 +28,14 @@ mod tests { use uuid::Uuid; #[test] - fn test_metadata_dto_from_metadata() { + fn test_metadata_dto_new() { let metadata_id = MetadataId::new(Uuid::now_v7()); let account_id = AccountId::new(Uuid::now_v7()); let label = MetadataLabel::new("test label".to_string()); let content = MetadataContent::new("test content".to_string()); let nanoid = Nanoid::default(); let version = EventVersion::new(Uuid::now_v7()); + let account_nanoid = "acc-nanoid-789".to_string(); let metadata = Metadata::new( metadata_id, @@ -43,8 +46,9 @@ mod tests { nanoid.clone(), ); - let dto = MetadataDto::from(metadata); + let dto = MetadataDto::new(metadata, account_nanoid.clone()); + assert_eq!(dto.account_nanoid, account_nanoid); assert_eq!(dto.nanoid, nanoid.as_ref().to_string()); assert_eq!(dto.label, label.as_ref().to_string()); assert_eq!(dto.content, content.as_ref().to_string()); diff --git a/application/src/transfer/profile.rs b/application/src/transfer/profile.rs index 0500927..4073101 100644 --- a/application/src/transfer/profile.rs +++ b/application/src/transfer/profile.rs @@ -3,6 +3,7 @@ use uuid::Uuid; #[derive(Debug)] pub struct ProfileDto { + pub account_nanoid: String, pub nanoid: String, pub display_name: Option, pub summary: Option, @@ -10,9 +11,10 @@ pub struct ProfileDto { pub banner_id: Option, } -impl From for ProfileDto { - fn from(profile: Profile) -> Self { +impl ProfileDto { + pub fn new(profile: Profile, account_nanoid: String) -> Self { Self { + account_nanoid, nanoid: profile.nanoid().as_ref().to_string(), display_name: profile .display_name() @@ -35,7 +37,7 @@ mod tests { use uuid::Uuid; #[test] - fn test_profile_dto_from_profile_with_all_fields() { + fn test_profile_dto_with_all_fields() { let profile_id = ProfileId::new(Uuid::now_v7()); let account_id = AccountId::new(Uuid::now_v7()); let nanoid = Nanoid::default(); @@ -44,6 +46,7 @@ mod tests { let icon_id = ImageId::new(Uuid::now_v7()); let banner_id = ImageId::new(Uuid::now_v7()); let version = EventVersion::new(Uuid::now_v7()); + let account_nanoid = "acc-nanoid-123".to_string(); let profile = Profile::new( profile_id, @@ -56,8 +59,9 @@ mod tests { nanoid.clone(), ); - let dto = ProfileDto::from(profile); + let dto = ProfileDto::new(profile, account_nanoid.clone()); + assert_eq!(dto.account_nanoid, account_nanoid); assert_eq!(dto.nanoid, nanoid.as_ref().to_string()); assert_eq!(dto.display_name, Some(display_name.as_ref().to_string())); assert_eq!(dto.summary, Some(summary.as_ref().to_string())); @@ -66,11 +70,12 @@ mod tests { } #[test] - fn test_profile_dto_from_profile_with_no_optional_fields() { + fn test_profile_dto_with_no_optional_fields() { let profile_id = ProfileId::new(Uuid::now_v7()); let account_id = AccountId::new(Uuid::now_v7()); let nanoid = Nanoid::default(); let version = EventVersion::new(Uuid::now_v7()); + let account_nanoid = "acc-nanoid-456".to_string(); let profile = Profile::new( profile_id, @@ -83,8 +88,9 @@ mod tests { nanoid.clone(), ); - let dto = ProfileDto::from(profile); + let dto = ProfileDto::new(profile, account_nanoid.clone()); + assert_eq!(dto.account_nanoid, account_nanoid); assert_eq!(dto.nanoid, nanoid.as_ref().to_string()); assert!(dto.display_name.is_none()); assert!(dto.summary.is_none()); diff --git a/driver/src/database/postgres/account.rs b/driver/src/database/postgres/account.rs index 3a1704c..37a225f 100644 --- a/driver/src/database/postgres/account.rs +++ b/driver/src/database/postgres/account.rs @@ -129,6 +129,28 @@ impl AccountReadModel for PostgresAccountReadModel { .map(|option| option.map(Account::from)) } + async fn find_by_nanoids( + &self, + executor: &mut Self::Executor, + nanoids: &[Nanoid], + ) -> error_stack::Result, KernelError> { + let con: &mut PgConnection = executor; + let nanoid_strs: Vec<&str> = nanoids.iter().map(|n| n.as_ref().as_str()).collect(); + sqlx::query_as::<_, AccountRow>( + //language=postgresql + r#" + SELECT id, name, private_key, public_key, is_bot, deleted_at, version, nanoid, created_at + FROM accounts + WHERE nanoid = ANY($1) AND deleted_at IS NULL + "#, + ) + .bind(&nanoid_strs) + .fetch_all(con) + .await + .convert_error() + .map(|rows| rows.into_iter().map(Account::from).collect()) + } + async fn create( &self, executor: &mut Self::Executor, diff --git a/driver/src/database/postgres/metadata.rs b/driver/src/database/postgres/metadata.rs index 805bdc6..168d63c 100644 --- a/driver/src/database/postgres/metadata.rs +++ b/driver/src/database/postgres/metadata.rs @@ -78,6 +78,28 @@ impl MetadataReadModel for PostgresMetadataReadModel { .map(|rows| rows.into_iter().map(|row| row.into()).collect()) } + async fn find_by_account_ids( + &self, + executor: &mut Self::Executor, + account_ids: &[AccountId], + ) -> error_stack::Result, KernelError> { + let con: &mut PgConnection = executor; + let ids: Vec = account_ids.iter().map(|id| *id.as_ref()).collect(); + sqlx::query_as::<_, MetadataRow>( + // language=postgresql + r#" + SELECT id, account_id, label, content, version, nanoid + FROM metadatas + WHERE account_id = ANY($1) + "#, + ) + .bind(&ids) + .fetch_all(con) + .await + .convert_error() + .map(|rows| rows.into_iter().map(|row| row.into()).collect()) + } + async fn create( &self, executor: &mut Self::Executor, diff --git a/driver/src/database/postgres/profile.rs b/driver/src/database/postgres/profile.rs index 6728355..ebc7a2c 100644 --- a/driver/src/database/postgres/profile.rs +++ b/driver/src/database/postgres/profile.rs @@ -83,6 +83,27 @@ impl ProfileReadModel for PostgresProfileReadModel { .map(|option| option.map(Profile::from)) } + async fn find_by_account_ids( + &self, + executor: &mut Self::Executor, + account_ids: &[AccountId], + ) -> error_stack::Result, KernelError> { + let con: &mut PgConnection = executor; + let ids: Vec = account_ids.iter().map(|id| *id.as_ref()).collect(); + sqlx::query_as::<_, ProfileRow>( + //language=postgresql + r#" + SELECT id, account_id, display, summary, icon_id, banner_id, version, nanoid + FROM profiles WHERE account_id = ANY($1) + "#, + ) + .bind(&ids) + .fetch_all(con) + .await + .convert_error() + .map(|rows| rows.into_iter().map(Profile::from).collect()) + } + async fn create( &self, executor: &mut Self::Executor, diff --git a/kernel/src/read_model/account.rs b/kernel/src/read_model/account.rs index 58cc4d4..df25353 100644 --- a/kernel/src/read_model/account.rs +++ b/kernel/src/read_model/account.rs @@ -31,6 +31,12 @@ pub trait AccountReadModel: Sync + Send + 'static { nanoid: &Nanoid, ) -> impl Future, KernelError>> + Send; + fn find_by_nanoids( + &self, + executor: &mut Self::Executor, + nanoids: &[Nanoid], + ) -> impl Future, KernelError>> + Send; + // Projection update operations (called by EventApplier pipeline) fn create( &self, diff --git a/kernel/src/read_model/metadata.rs b/kernel/src/read_model/metadata.rs index eebf338..bbe35f3 100644 --- a/kernel/src/read_model/metadata.rs +++ b/kernel/src/read_model/metadata.rs @@ -19,6 +19,12 @@ pub trait MetadataReadModel: Sync + Send + 'static { account_id: &AccountId, ) -> impl Future, KernelError>> + Send; + fn find_by_account_ids( + &self, + executor: &mut Self::Executor, + account_ids: &[AccountId], + ) -> impl Future, KernelError>> + Send; + // Projection update operations (called by EventApplier pipeline) fn create( &self, diff --git a/kernel/src/read_model/profile.rs b/kernel/src/read_model/profile.rs index 881a28f..0f12e9f 100644 --- a/kernel/src/read_model/profile.rs +++ b/kernel/src/read_model/profile.rs @@ -19,6 +19,12 @@ pub trait ProfileReadModel: Sync + Send + 'static { account_id: &AccountId, ) -> impl Future, KernelError>> + Send; + fn find_by_account_ids( + &self, + executor: &mut Self::Executor, + account_ids: &[AccountId], + ) -> impl Future, KernelError>> + Send; + // Projection update operations (called by EventApplier pipeline) fn create( &self, diff --git a/server/src/route.rs b/server/src/route.rs index 4c1e69a..50a788a 100644 --- a/server/src/route.rs +++ b/server/src/route.rs @@ -7,6 +7,29 @@ pub mod metadata; pub mod oauth2; pub mod profile; +const MAX_BATCH_SIZE: usize = 100; + +fn parse_comma_ids(raw: &str) -> Result, ErrorStatus> { + let ids: Vec = raw + .split(',') + .map(|s| s.trim().to_string()) + .filter(|s| !s.is_empty()) + .collect(); + if ids.is_empty() { + return Err(ErrorStatus::from(( + StatusCode::BAD_REQUEST, + "ID list cannot be empty".to_string(), + ))); + } + if ids.len() > MAX_BATCH_SIZE { + return Err(ErrorStatus::from(( + StatusCode::BAD_REQUEST, + format!("Too many IDs: maximum is {MAX_BATCH_SIZE}"), + ))); + } + Ok(ids) +} + trait DirectionConverter { fn convert_to_direction(self) -> Result; } diff --git a/server/src/route/account.rs b/server/src/route/account.rs index acd543d..21489d7 100644 --- a/server/src/route/account.rs +++ b/server/src/route/account.rs @@ -1,7 +1,7 @@ use crate::auth::{resolve_auth_account_id, AuthClaims, OidcAuthInfo}; use crate::error::ErrorStatus; use crate::handler::AppModule; -use crate::route::DirectionConverter; +use crate::route::{parse_comma_ids, DirectionConverter}; use application::service::account::{ CreateAccountUseCase, DeactivateAccountUseCase, EditAccountUseCase, GetAccountUseCase, }; @@ -15,6 +15,7 @@ use time::OffsetDateTime; #[derive(Debug, Deserialize)] struct GetAllAccountQuery { + ids: Option, limit: Option, cursor: Option, direction: Option, @@ -55,6 +56,7 @@ async fn get_accounts( Extension(claims): Extension, State(module): State, Query(GetAllAccountQuery { + ids, direction, limit, cursor, @@ -65,13 +67,28 @@ async fn get_accounts( .await .map_err(ErrorStatus::from)?; - let direction = direction.convert_to_direction()?; - let pagination = Pagination::new(limit, cursor, direction); - let result = module - .get_all_accounts(&auth_account_id, pagination) - .await - .map_err(ErrorStatus::from)? - .ok_or(ErrorStatus::from(StatusCode::NOT_FOUND))?; + let result = if let Some(ids) = ids { + if limit.is_some() || cursor.is_some() || direction.is_some() { + return Err(ErrorStatus::from(( + StatusCode::BAD_REQUEST, + "Cannot use ids with pagination parameters".to_string(), + ))); + } + let id_list = parse_comma_ids(&ids)?; + module + .get_accounts_by_ids(&auth_account_id, id_list) + .await + .map_err(ErrorStatus::from)? + } else { + let direction = direction.convert_to_direction()?; + let pagination = Pagination::new(limit, cursor, direction); + module + .get_all_accounts(&auth_account_id, pagination) + .await + .map_err(ErrorStatus::from)? + .ok_or(ErrorStatus::from(StatusCode::NOT_FOUND))? + }; + if result.is_empty() { return Err(ErrorStatus::from(StatusCode::NOT_FOUND)); } @@ -126,40 +143,6 @@ async fn create_account( Ok(Json(response)) } -async fn get_account_by_id( - Extension(claims): Extension, - State(module): State, - Path(id): Path, -) -> Result, ErrorStatus> { - let auth_info = OidcAuthInfo::from(claims); - - if id.trim().is_empty() { - return Err(ErrorStatus::from(( - StatusCode::BAD_REQUEST, - "Account ID cannot be empty".to_string(), - ))); - } - - let auth_account_id = resolve_auth_account_id(&module, auth_info) - .await - .map_err(ErrorStatus::from)?; - - let account = module - .get_account_by_id(&auth_account_id, id) - .await - .map_err(ErrorStatus::from)?; - - let response = AccountResponse { - id: account.nanoid, - name: account.name, - public_key: account.public_key, - is_bot: account.is_bot, - created_at: account.created_at, - }; - - Ok(Json(response)) -} - async fn update_account_by_id( Extension(claims): Extension, State(module): State, @@ -217,7 +200,6 @@ impl AccountRouter for Router { fn route_account(self) -> Self { self.route("/accounts", get(get_accounts)) .route("/accounts", post(create_account)) - .route("/accounts/:id", get(get_account_by_id)) .route("/accounts/:id", put(update_account_by_id)) .route("/accounts/:id", delete(deactivate_account_by_id)) } diff --git a/server/src/route/metadata.rs b/server/src/route/metadata.rs index 15f896f..6e56321 100644 --- a/server/src/route/metadata.rs +++ b/server/src/route/metadata.rs @@ -1,12 +1,13 @@ use crate::auth::{resolve_auth_account_id, AuthClaims, OidcAuthInfo}; use crate::error::ErrorStatus; use crate::handler::AppModule; +use crate::route::parse_comma_ids; use application::service::metadata::{ CreateMetadataUseCase, DeleteMetadataUseCase, EditMetadataUseCase, GetMetadataUseCase, }; -use axum::extract::{Path, State}; +use axum::extract::{Path, Query, State}; use axum::http::StatusCode; -use axum::routing::{get, put}; +use axum::routing::{get, post, put}; use axum::{Extension, Json, Router}; use serde::{Deserialize, Serialize}; @@ -24,6 +25,7 @@ struct UpdateMetadataRequest { #[derive(Debug, Serialize)] struct MetadataResponse { + account_id: String, nanoid: String, label: String, content: String, @@ -32,6 +34,7 @@ struct MetadataResponse { impl From for MetadataResponse { fn from(dto: application::transfer::metadata::MetadataDto) -> Self { Self { + account_id: dto.account_nanoid, nanoid: dto.nanoid, label: dto.label, content: dto.content, @@ -39,30 +42,30 @@ impl From for MetadataResponse { } } +#[derive(Debug, Deserialize)] +struct GetMetadataQuery { + account_ids: String, +} + pub trait MetadataRouter { fn route_metadata(self) -> Self; } -async fn get_metadata( +async fn get_metadata_batch( Extension(claims): Extension, State(module): State, - Path(account_id): Path, + Query(query): Query, ) -> Result>, ErrorStatus> { let auth_info = OidcAuthInfo::from(claims); - if account_id.trim().is_empty() { - return Err(ErrorStatus::from(( - StatusCode::BAD_REQUEST, - "Account ID cannot be empty".to_string(), - ))); - } + let account_ids = parse_comma_ids(&query.account_ids)?; let auth_account_id = resolve_auth_account_id(&module, auth_info) .await .map_err(ErrorStatus::from)?; let metadata_list = module - .get_metadata(&auth_account_id, account_id) + .get_metadata_batch(&auth_account_id, account_ids) .await .map_err(ErrorStatus::from)?; @@ -176,14 +179,12 @@ async fn delete_metadata( impl MetadataRouter for Router { fn route_metadata(self) -> Self { - self.route( - "/accounts/:account_id/metadata", - get(get_metadata).post(create_metadata), - ) - .route( - "/accounts/:account_id/metadata/:id", - put(update_metadata).delete(delete_metadata), - ) + self.route("/metadata", get(get_metadata_batch)) + .route("/accounts/:account_id/metadata", post(create_metadata)) + .route( + "/accounts/:account_id/metadata/:id", + put(update_metadata).delete(delete_metadata), + ) } } @@ -195,6 +196,7 @@ mod tests { #[test] fn test_metadata_response_from_dto() { let dto = MetadataDto { + account_nanoid: "acc-123".to_string(), nanoid: "test-nanoid".to_string(), label: "test-label".to_string(), content: "test-content".to_string(), @@ -202,6 +204,7 @@ mod tests { let response = MetadataResponse::from(dto); + assert_eq!(response.account_id, "acc-123"); assert_eq!(response.nanoid, "test-nanoid"); assert_eq!(response.label, "test-label"); assert_eq!(response.content, "test-content"); diff --git a/server/src/route/profile.rs b/server/src/route/profile.rs index 24a59b2..36c2c8e 100644 --- a/server/src/route/profile.rs +++ b/server/src/route/profile.rs @@ -1,10 +1,11 @@ use crate::auth::{resolve_auth_account_id, AuthClaims, OidcAuthInfo}; use crate::error::ErrorStatus; use crate::handler::AppModule; +use crate::route::parse_comma_ids; use application::service::profile::{CreateProfileUseCase, EditProfileUseCase, GetProfileUseCase}; -use axum::extract::{Path, State}; +use axum::extract::{Path, Query, State}; use axum::http::StatusCode; -use axum::routing::get; +use axum::routing::{get, post}; use axum::{Extension, Json, Router}; use kernel::prelude::entity::ImageId; use serde::{Deserialize, Serialize}; @@ -28,6 +29,7 @@ struct UpdateProfileRequest { #[derive(Debug, Serialize)] struct ProfileResponse { + account_id: String, nanoid: String, display_name: Option, summary: Option, @@ -38,6 +40,7 @@ struct ProfileResponse { impl From for ProfileResponse { fn from(dto: application::transfer::profile::ProfileDto) -> Self { Self { + account_id: dto.account_nanoid, nanoid: dto.nanoid, display_name: dto.display_name, summary: dto.summary, @@ -47,34 +50,36 @@ impl From for ProfileResponse { } } +#[derive(Debug, Deserialize)] +struct GetProfilesQuery { + account_ids: String, +} + pub trait ProfileRouter { fn route_profile(self) -> Self; } -async fn get_profile( +async fn get_profiles_batch( Extension(claims): Extension, State(module): State, - Path(account_id): Path, -) -> Result, ErrorStatus> { + Query(query): Query, +) -> Result>, ErrorStatus> { let auth_info = OidcAuthInfo::from(claims); - if account_id.trim().is_empty() { - return Err(ErrorStatus::from(( - StatusCode::BAD_REQUEST, - "Account ID cannot be empty".to_string(), - ))); - } + let account_ids = parse_comma_ids(&query.account_ids)?; let auth_account_id = resolve_auth_account_id(&module, auth_info) .await .map_err(ErrorStatus::from)?; - let profile = module - .get_profile(&auth_account_id, account_id) + let profiles = module + .get_profiles_batch(&auth_account_id, account_ids) .await .map_err(ErrorStatus::from)?; - Ok(Json(ProfileResponse::from(profile))) + Ok(Json( + profiles.into_iter().map(ProfileResponse::from).collect(), + )) } async fn create_profile( @@ -153,9 +158,9 @@ async fn update_profile( impl ProfileRouter for Router { fn route_profile(self) -> Self { - self.route( + self.route("/profiles", get(get_profiles_batch)).route( "/accounts/:account_id/profile", - get(get_profile).post(create_profile).put(update_profile), + post(create_profile).put(update_profile), ) } } @@ -168,6 +173,7 @@ mod tests { #[test] fn test_profile_response_from_dto_with_all_fields() { let dto = ProfileDto { + account_nanoid: "acc-123".to_string(), nanoid: "test-nanoid".to_string(), display_name: Some("Test User".to_string()), summary: Some("A test summary".to_string()), @@ -177,6 +183,7 @@ mod tests { let response = ProfileResponse::from(dto); + assert_eq!(response.account_id, "acc-123"); assert_eq!(response.nanoid, "test-nanoid"); assert_eq!(response.display_name, Some("Test User".to_string())); assert_eq!(response.summary, Some("A test summary".to_string())); @@ -187,6 +194,7 @@ mod tests { #[test] fn test_profile_response_from_dto_with_no_optional_fields() { let dto = ProfileDto { + account_nanoid: "acc-456".to_string(), nanoid: "test-nanoid-2".to_string(), display_name: None, summary: None, @@ -196,6 +204,7 @@ mod tests { let response = ProfileResponse::from(dto); + assert_eq!(response.account_id, "acc-456"); assert_eq!(response.nanoid, "test-nanoid-2"); assert!(response.display_name.is_none()); assert!(response.summary.is_none());