From fadd1fd0f890625f31c82beb074fef38234ccb11 Mon Sep 17 00:00:00 2001 From: naotiki Date: Sun, 16 Feb 2025 21:29:15 +0900 Subject: [PATCH 01/10] feat: add openapi-client gen --- .env.dev | 1 + .gitignore | 2 + Cargo.lock | 266 +++++++++++++++++++++- Cargo.toml | 1 + Makefile.toml | 26 +++ README.md | 14 ++ client/Cargo.toml | 1 + client/src/api_client.rs | 10 + client/src/main.rs | 1 + schema.yaml | 471 +++++++++++++++++++++++++++++++++++++++ 10 files changed, 790 insertions(+), 3 deletions(-) create mode 100644 .env.dev create mode 100644 Makefile.toml create mode 100644 README.md create mode 100644 client/src/api_client.rs create mode 100644 schema.yaml diff --git a/.env.dev b/.env.dev new file mode 100644 index 0000000..15da5bf --- /dev/null +++ b/.env.dev @@ -0,0 +1 @@ +BACKEND_REST_HOST=http://localhost:8080 \ No newline at end of file diff --git a/.gitignore b/.gitignore index ea8c4bf..50fd5c8 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,3 @@ /target +backend_api/ +.env \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 294f279..3bc2d47 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,21 @@ version = "2.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "512761e0bb2578dd7380c6baaa0f4ce03e84f95e960231d1dec8bf4d7d6e2627" +[[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 = "anstream" version = "0.6.18" @@ -133,6 +148,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 = "clap" version = "4.5.26" @@ -181,6 +209,7 @@ dependencies = [ "common", "confy", "dialoguer", + "openapi", "reqwest", "rpassword", "serde", @@ -241,6 +270,51 @@ version = "0.8.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" +[[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", +] + +[[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", +] + +[[package]] +name = "deranged" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4" +dependencies = [ + "powerfmt", + "serde", +] + [[package]] name = "dialoguer" version = "0.11.0" @@ -421,13 +495,19 @@ dependencies = [ "futures-core", "futures-sink", "http", - "indexmap", + "indexmap 2.7.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.15.2" @@ -440,6 +520,12 @@ version = "0.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + [[package]] name = "http" version = "1.2.0" @@ -552,6 +638,29 @@ dependencies = [ "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]] name = "icu_collections" version = "1.5.0" @@ -670,6 +779,12 @@ dependencies = [ "syn", ] +[[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" @@ -691,6 +806,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.7.0" @@ -698,7 +824,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "62f822373a4fe84d4bb149bf54e584a7f4abec90e072ed49cda0edea5b95471f" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.15.2", + "serde", ] [[package]] @@ -785,6 +912,16 @@ version = "0.3.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6877bb514081ee2a7ff5ef9de3281f14a4dd4bceac4c09388074a6b5df8a139a" +[[package]] +name = "mime_guess" +version = "2.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f7c44f8e672c00fe5308fa235f821cb4198414e1c77935c1ab6948d3fd78550e" +dependencies = [ + "mime", + "unicase", +] + [[package]] name = "miniz_oxide" version = "0.8.3" @@ -822,6 +959,21 @@ dependencies = [ "tempfile", ] +[[package]] +name = "num-conv" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "object" version = "0.36.7" @@ -837,6 +989,19 @@ version = "1.20.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1261fe7e33c73b354eab43b1273a57c8f967d0391e80353e51f764ac02cf6775" +[[package]] +name = "openapi" +version = "1.0.0" +dependencies = [ + "reqwest", + "serde", + "serde_json", + "serde_repr", + "serde_with", + "url", + "uuid", +] + [[package]] name = "openssl" version = "0.10.68" @@ -934,6 +1099,12 @@ version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2" +[[package]] +name = "powerfmt" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" + [[package]] name = "proc-macro2" version = "1.0.93" @@ -995,6 +1166,7 @@ dependencies = [ "js-sys", "log", "mime", + "mime_guess", "native-tls", "once_cell", "percent-encoding", @@ -1192,6 +1364,17 @@ dependencies = [ "serde", ] +[[package]] +name = "serde_repr" +version = "0.1.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6c64451ba24fc7a6a2d60fc75dd9c83c90903b19028d4eff35e88fc1e86564e9" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "serde_spanned" version = "0.6.8" @@ -1213,6 +1396,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", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.7.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", +] + [[package]] name = "server" version = "0.1.0" @@ -1376,6 +1589,37 @@ dependencies = [ "syn", ] +[[package]] +name = "time" +version = "0.3.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35e7868883861bd0e56d9ac6efcaaca0d6d5d82a2a7ec8209ff492c07cf37b21" +dependencies = [ + "deranged", + "itoa", + "num-conv", + "powerfmt", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3" + +[[package]] +name = "time-macros" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2834e6017e3e5e4b9834939793b282bc03b37a3336245fa820e35e233e2a85de" +dependencies = [ + "num-conv", + "time-core", +] + [[package]] name = "tinystr" version = "0.7.6" @@ -1475,7 +1719,7 @@ version = "0.22.22" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ae48d6208a266e853d946088ed816055e556cc6028c5e8e2b84d9fa5dd7c7f5" dependencies = [ - "indexmap", + "indexmap 2.7.0", "serde", "serde_spanned", "toml_datetime", @@ -1534,6 +1778,12 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "unicase" +version = "2.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75b844d17643ee918803943289730bec8aac480150456169e647ed0b576ba539" + [[package]] name = "unicode-ident" version = "1.0.14" @@ -1588,6 +1838,7 @@ version = "1.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "744018581f9a3454a9e15beb8a33b017183f1e7c0cd170232a2d1453b23a51c4" dependencies = [ + "getrandom", "serde", ] @@ -1693,6 +1944,15 @@ dependencies = [ "wasm-bindgen", ] +[[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" diff --git a/Cargo.toml b/Cargo.toml index f0f6023..9600bfc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -4,4 +4,5 @@ members = [ "common", "server", "client", + "backend_api", ] diff --git a/Makefile.toml b/Makefile.toml new file mode 100644 index 0000000..51384bd --- /dev/null +++ b/Makefile.toml @@ -0,0 +1,26 @@ +env_files = [".env.dev", ".env"] +[config] +default_to_workspace = false + +[tasks.openapi-fetch] +command = "curl" +args = ["-o", "schema.yaml", "${BACKEND_REST_HOST}/api/schema.yaml"] +[tasks.openapi-gen] +command = "docker" +args = [ + "run", + "--rm", + "-v", + "${PWD}:/local", + "openapitools/openapi-generator-cli", + "generate", + "-i", + "/local/schema.yaml", + "-g", + "rust", + "-o", + "/local/backend_api", + "--skip-validate-spec", +] +[tasks.openapi] +dependencies = ["openapi-fetch", "openapi-gen"] diff --git a/README.md b/README.md new file mode 100644 index 0000000..bdab95f --- /dev/null +++ b/README.md @@ -0,0 +1,14 @@ +# 前提条件 +## cargo-make +```sh +$ cargo install --force cargo-make +``` + +https://github.com/sagiegurari/cargo-make?tab=readme-ov-file#installation + + +# Build +## Generate OpenAPI Client +```sh +$ cargo make openapi-gen +``` \ No newline at end of file diff --git a/client/Cargo.toml b/client/Cargo.toml index 2fe833e..fa985e7 100644 --- a/client/Cargo.toml +++ b/client/Cargo.toml @@ -8,6 +8,7 @@ path = "src/main.rs" [dependencies] clap = { version = "4.5.26", features = ["derive"] } common = { path = "../common" } +openapi = { path = "../backend_api" } confy = "0.6.1" dialoguer = "0.11.0" reqwest = { version = "0.12.12", features = ["json"] } diff --git a/client/src/api_client.rs b/client/src/api_client.rs new file mode 100644 index 0000000..562bab7 --- /dev/null +++ b/client/src/api_client.rs @@ -0,0 +1,10 @@ +use std::sync::{LazyLock, Mutex}; + +use openapi::apis::configuration::Configuration; + + +pub fn get_api_config(basepath:String) -> Configuration { + let mut conf = Configuration::new(); + conf.base_path = basepath; + conf +} \ No newline at end of file diff --git a/client/src/main.rs b/client/src/main.rs index 71d3a70..56dc924 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -5,6 +5,7 @@ use config::Config; mod commands; mod config; mod schema; +mod api_client; #[tokio::main] async fn main() { diff --git a/schema.yaml b/schema.yaml new file mode 100644 index 0000000..db44a3e --- /dev/null +++ b/schema.yaml @@ -0,0 +1,471 @@ +openapi: 3.1.0 +info: + title: Freight API + description: Freight API + version: 1.0.0 +externalDocs: + url: / +servers: +- url: http://localhost:8080 + description: Development Server +tags: [] +paths: + /api/auth/login: + post: + tags: [] + parameters: [] + requestBody: + content: + application/json: + schema: + type: object + properties: + username: + type: string + title: String + password: + type: string + title: String + required: + - password + - username + title: LoginCredential + required: false + responses: + "200": + headers: {} + content: + application/json: + schema: + type: object + properties: + ok: + type: boolean + title: Boolean + data: + $ref: "#/components/schemas/UserWithTokenResponse" + required: + - data + - ok + title: Success + deprecated: false + /api/auth/logout: + post: + tags: [] + parameters: [] + responses: + "200": + headers: {} + content: + application/json: + schema: + type: object + properties: + ok: + type: boolean + title: Boolean + data: + type: "null" + title: Unit + required: + - ok + title: Success + deprecated: false + security: + - AppDefaultSecurityScheme: [] + /api/networks/create: + post: + tags: [] + parameters: [] + requestBody: + content: + application/json: + schema: + type: object + properties: + name: + type: string + title: String + networkAddress: + type: string + format: ipv4-cidr + example: 10.0.0.0/8 + title: NetworkAddressWithMask + containerAddress: + type: string + format: ipv4-cidr + example: 10.0.0.0/8 + title: NetworkAddressWithMask + clientAddress: + type: string + format: ipv4-cidr + example: 10.0.0.0/8 + title: NetworkAddressWithMask + required: + - clientAddress + - containerAddress + - name + - networkAddress + title: CreateNetworkRequest + required: false + responses: + "200": + headers: {} + content: + application/json: + schema: + type: object + properties: + ok: + type: boolean + title: Boolean + data: + $ref: "#/components/schemas/UserWithTokenResponse" + required: + - data + - ok + title: Success + "401": + description: Unauthorized + headers: {} + content: + application/json: + schema: + type: object + example: "{\"ok\":false,\"message\":\"Error Message\"}" + properties: + ok: + type: boolean + title: Boolean + message: + type: string + title: String + required: + - message + - ok + title: Error + "400": + description: Bad Request + headers: {} + content: + application/json: + schema: + type: object + example: "{\"ok\":false,\"message\":\"Error Message\"}" + properties: + ok: + type: boolean + title: Boolean + message: + type: string + title: String + required: + - message + - ok + title: Error + deprecated: false + security: + - AppDefaultSecurityScheme: [] + /api/networks/me: + get: + tags: [] + parameters: [] + responses: + "200": + headers: {} + content: + application/json: + schema: + type: object + properties: + ok: + type: boolean + title: Boolean + data: + type: array + items: + $ref: "#/components/schemas/Network" + title: ArrayList + required: + - data + - ok + title: Success + "401": + description: Unauthorized + headers: {} + content: + application/json: + schema: + type: object + example: "{\"ok\":false,\"message\":\"Error Message\"}" + properties: + ok: + type: boolean + title: Boolean + message: + type: string + title: String + required: + - message + - ok + title: Error + deprecated: false + security: + - AppDefaultSecurityScheme: [] + /api/networks/{owner}/{name}: + get: + tags: [] + parameters: + - name: owner + in: path + required: true + deprecated: false + explode: false + schema: + type: string + title: String + - name: name + in: path + required: true + deprecated: false + explode: false + schema: + type: string + title: String + responses: + "200": + headers: {} + content: + application/json: + schema: + type: object + properties: + ok: + type: boolean + title: Boolean + data: + $ref: "#/components/schemas/Network" + required: + - data + - ok + title: Success + "401": + description: Unauthorized + headers: {} + content: + application/json: + schema: + type: object + example: "{\"ok\":false,\"message\":\"Error Message\"}" + properties: + ok: + type: boolean + title: Boolean + message: + type: string + title: String + required: + - message + - ok + title: Error + "404": + description: Not Found + headers: {} + content: + application/json: + schema: + type: object + example: "{\"ok\":false,\"message\":\"Error Message\"}" + properties: + ok: + type: boolean + title: Boolean + message: + type: string + title: String + required: + - message + - ok + title: Error + deprecated: false + security: + - AppDefaultSecurityScheme: [] + /api/users/me: + get: + tags: [] + parameters: [] + responses: + "200": + headers: {} + content: + application/json: + schema: + type: object + properties: + ok: + type: boolean + title: Boolean + data: + $ref: "#/components/schemas/UserResponse" + required: + - data + - ok + title: Success + deprecated: false + security: + - AppDefaultSecurityScheme: [] +components: + schemas: + UserWithTokenResponse: + type: object + properties: + userId: + type: string + format: uuid + title: Uuid + username: + type: string + title: String + token: + $ref: "#/components/schemas/Token" + required: + - token + - userId + - username + title: UserWithTokenResponse + Token: + type: object + properties: + token: + type: string + title: String + expiresAt: + type: string + format: timestamp + title: Instant + required: + - expiresAt + - token + title: Token + Network: + type: object + properties: + id: + type: string + format: uuid + title: Uuid + numericId: + type: integer + maximum: 4294967295 + minimum: 0 + title: UInt + name: + type: string + title: String + networkAddressWithMask: + type: string + format: ipv4-cidr + example: 10.0.0.0/8 + title: NetworkAddressWithMask + containersNetworkAddressWithMask: + type: string + format: ipv4-cidr + example: 10.0.0.0/8 + title: NetworkAddressWithMask + clientsNetworkAddressWithMask: + type: string + format: ipv4-cidr + example: 10.0.0.0/8 + title: NetworkAddressWithMask + owner: + $ref: "#/components/schemas/User" + members: + type: array + items: + $ref: "#/components/schemas/NetworkMember" + title: ArrayList + dockerNetworkId: + type: + - "null" + - string + title: DockerId + tunInterfaceName: + type: + - "null" + - string + title: NetworkInterfaceName + vrfInterfaceName: + type: + - "null" + - string + title: NetworkInterfaceName + bridgeInterfaceName: + type: + - "null" + - string + title: NetworkInterfaceName + required: + - clientsNetworkAddressWithMask + - containersNetworkAddressWithMask + - id + - members + - name + - networkAddressWithMask + - numericId + - owner + title: Network + User: + type: object + properties: + id: + type: string + format: uuid + title: Uuid + username: + type: string + title: String + passwordHash: + type: string + title: Argon2 + required: + - id + - passwordHash + - username + title: User + NetworkMember: + type: object + properties: + user: + $ref: "#/components/schemas/User" + permission: + $ref: "#/components/schemas/Permissions" + required: + - permission + - user + title: NetworkMember + Permissions: + type: string + enum: + - Owner + - Member + - Guest + title: Permissions + UserResponse: + type: object + properties: + id: + type: string + format: uuid + title: Uuid + username: + type: string + title: String + required: + - id + - username + title: UserResponse + examples: {} + securitySchemes: + AppDefaultSecurityScheme: + type: http + scheme: bearer From 0092a7a698411d1dc400e833a054a20defde7083 Mon Sep 17 00:00:00 2001 From: naotiki Date: Sun, 16 Feb 2025 21:48:10 +0900 Subject: [PATCH 02/10] feat: migrate apiclient to openapi client --- client/src/commands/login.rs | 67 ++++++++++++++++------------------- client/src/commands/logout.rs | 59 ++++++++++++------------------ client/src/main.rs | 1 - client/src/schema.rs | 1 - client/src/schema/api.rs | 38 -------------------- 5 files changed, 54 insertions(+), 112 deletions(-) delete mode 100644 client/src/schema.rs delete mode 100644 client/src/schema/api.rs diff --git a/client/src/commands/login.rs b/client/src/commands/login.rs index b076549..0bfc359 100644 --- a/client/src/commands/login.rs +++ b/client/src/commands/login.rs @@ -1,9 +1,11 @@ use super::{Args, Command}; -use crate::config::{Config, ServerConfig}; -use crate::schema::api::{LoginRequest, LoginResponse}; +use crate::{ + api_client::get_api_config, + config::{Config, ServerConfig}, +}; use dialoguer::{Confirm, Input, Password}; use reqwest::Client; -use std::process::exit; +use std::{ops::Deref, process::exit}; use url::Url; #[derive(clap::Parser, Debug)] @@ -60,7 +62,7 @@ impl Command for Login { let mut config = Config::load(); let server = ServerConfig { url, - token: Some(response_body.data.unwrap().token.token), + token: Some(response_body.token.token), }; config.servers.insert(self.server_name.clone(), server); if self.default { @@ -71,42 +73,35 @@ impl Command for Login { } } -async fn login(username: String, password: String, mut url: Url) -> LoginResponse { - let login_request = LoginRequest { username, password }; - url.set_path(&format!( - "{}/api/auth/login", - url.path().trim_end_matches('/') - )); - let response = Client::new().post(url).json(&login_request).send().await; +async fn login(username: String, password: String, url: Url) -> openapi::models::UserWithTokenResponse{ + let login_credential = openapi::models::LoginCredential { username, password }; + let conf = get_api_config(url.as_str().to_string()); + + let response = + openapi::apis::default_api::api_auth_login_post(&conf, Some(login_credential)).await; match response { - Ok(response) => { - let status = response.status(); - if !status.is_success() && status.as_u16() != 401 { - eprintln!("Request error: status {}", status.as_u16()); - eprintln!("Please check your internet connection or server URL and try again."); - exit(1); - } - let response_body = response.json::().await; - match response_body { - Ok(response_body) => { - if response_body.ok { - response_body - } else { - eprintln!("Login Failed!"); - eprintln!("Wrong username or password."); - eprintln!("Check your credentials."); + Ok(response) => *response.data, + Err(e) => match e { + openapi::apis::Error::ResponseError(response_content) => { + match response_content.entity { + Some((e)) => match e { + openapi::apis::default_api::ApiAuthLoginPostError::UnknownValue(_value) => { + eprintln!("Login Failed!"); + eprintln!("Wrong username or password."); + eprintln!("Check your credentials."); + exit(1); + } + }, + None => { + eprintln!("Invalid Response"); exit(1); } } - Err(e) => { - eprintln!("Invalid response: {}", e); - exit(1); - } } - } - Err(e) => { - eprintln!("Request error: {}", e); - exit(1); - } + e => { + eprintln!("Request error: {}", e); + exit(1); + } + }, } } diff --git a/client/src/commands/logout.rs b/client/src/commands/logout.rs index 9e676e1..f023d91 100644 --- a/client/src/commands/logout.rs +++ b/client/src/commands/logout.rs @@ -1,6 +1,6 @@ use super::{Args, Command}; -use crate::schema::api::LogoutResponse; -use crate::Config; +use crate::{api_client::get_api_config, Config}; +use openapi::apis::default_api::api_auth_logout_post; use reqwest::{header, Client}; use std::process::exit; use url::Url; @@ -29,44 +29,31 @@ impl Command for Logout { } } -async fn logout(token: String, mut url: Url) -> LogoutResponse { - url.set_path(&format!( - "{}/api/auth/logout", - url.path().trim_end_matches('/') - )); - let response = Client::new() - .post(url) - .header(header::AUTHORIZATION, format!("Bearer {}", token)) - .json(&{}) - .send() - .await; +async fn logout(token: String, url: Url) -> Option> { + let mut conf = get_api_config(url.as_str().to_string()); + conf.bearer_access_token = Some(token); + let response = api_auth_logout_post(&conf).await; match response { - Ok(response) => { - let status = response.status(); - if !status.is_success() { - eprintln!("Request error: status {}", status.as_u16()); - exit(1); - } - println!("{}", status.is_success()); - let response_body = response.json::().await; - match response_body { - Ok(response_body) => { - if response_body.ok { - response_body - } else { - eprintln!("Error: {}", response_body.message.unwrap()); + Ok(response) => response.data, + Err(e) => match e { + openapi::apis::Error::ResponseError(response_content) => { + match response_content.entity { + Some(e) => match e { + openapi::apis::default_api::ApiAuthLogoutPostError::UnknownValue(value) => { + eprintln!("Error: {}", value); + exit(1); + } + }, + None => { + eprintln!("Invalid response {}", response_content.status); exit(1); } } - Err(ref e) => { - eprintln!("Invalid response: {}", e); - exit(1); - } } - } - Err(e) => { - eprintln!("Request error: {}", e); - exit(1); - } + e => { + eprintln!("Request error: {}", e); + exit(1); + } + }, } } diff --git a/client/src/main.rs b/client/src/main.rs index 56dc924..713ba45 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -4,7 +4,6 @@ use config::Config; mod commands; mod config; -mod schema; mod api_client; #[tokio::main] diff --git a/client/src/schema.rs b/client/src/schema.rs deleted file mode 100644 index e5fdf85..0000000 --- a/client/src/schema.rs +++ /dev/null @@ -1 +0,0 @@ -pub mod api; diff --git a/client/src/schema/api.rs b/client/src/schema/api.rs deleted file mode 100644 index 34205e4..0000000 --- a/client/src/schema/api.rs +++ /dev/null @@ -1,38 +0,0 @@ -#![allow(dead_code)] -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -#[derive(Debug, Serialize)] -pub struct LoginRequest { - pub username: String, - pub password: String, -} - -#[derive(Debug, Deserialize)] -pub struct LoginResponse { - pub ok: bool, - pub data: Option, - pub message: Option, -} - -#[derive(Debug, Deserialize)] -pub struct LoginResponseData { - #[serde(rename = "userId")] - pub user_id: Uuid, - pub username: String, - pub token: LoginResponseToken, -} - -#[derive(Debug, Deserialize)] -pub struct LoginResponseToken { - pub token: String, - #[serde(rename = "expiresAt")] - pub expires_at: String, -} - -#[derive(Debug, Deserialize)] -pub struct LogoutResponse { - pub ok: bool, - pub data: Option, // Always returned null or empty - pub message: Option, -} From 525e6f72530a3f67e1235909718d2166a83d2c35 Mon Sep 17 00:00:00 2001 From: naotiki Date: Sun, 16 Feb 2025 22:12:01 +0900 Subject: [PATCH 03/10] fix: permission --- Makefile.toml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Makefile.toml b/Makefile.toml index 51384bd..124d63a 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -7,9 +7,15 @@ command = "curl" args = ["-o", "schema.yaml", "${BACKEND_REST_HOST}/api/schema.yaml"] [tasks.openapi-gen] command = "docker" +linux.env = { "UID" = { script = [ + "id -u", +] }, "GID" = { script = [ + "id -g", +] }, "OPTS" = "--user=${UID}:${GID}" } args = [ "run", "--rm", + "${OPTS}", "-v", "${PWD}:/local", "openapitools/openapi-generator-cli", From ce6bf7fe4cb5ceaaef3c6ca46b7307f7dff3a348 Mon Sep 17 00:00:00 2001 From: Naotiki Date: Sat, 22 Feb 2025 21:49:25 +0900 Subject: [PATCH 04/10] fix: issue on windows chore: update latest schema --- Makefile.toml | 4 +- schema.yaml | 425 +++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 423 insertions(+), 6 deletions(-) diff --git a/Makefile.toml b/Makefile.toml index 124d63a..dbab002 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -7,15 +7,17 @@ command = "curl" args = ["-o", "schema.yaml", "${BACKEND_REST_HOST}/api/schema.yaml"] [tasks.openapi-gen] command = "docker" +windows.env = { "PWD" = {script = [ "cd" ]}} linux.env = { "UID" = { script = [ "id -u", ] }, "GID" = { script = [ "id -g", ] }, "OPTS" = "--user=${UID}:${GID}" } + args = [ "run", "--rm", - "${OPTS}", + "@@remove-empty(OPTS)", "-v", "${PWD}:/local", "openapitools/openapi-generator-cli", diff --git a/schema.yaml b/schema.yaml index db44a3e..b1b162e 100644 --- a/schema.yaml +++ b/schema.yaml @@ -48,6 +48,44 @@ paths: - data - ok title: Success + "400": + description: Bad Request + headers: {} + content: + application/json: + schema: + type: object + example: "{\"ok\":false,\"message\":\"Error Message\"}" + properties: + ok: + type: boolean + title: Boolean + message: + type: string + title: String + required: + - message + - ok + title: Error + "401": + description: Unauthorized + headers: {} + content: + application/json: + schema: + type: object + example: "{\"ok\":false,\"message\":\"Error Message\"}" + properties: + ok: + type: boolean + title: Boolean + message: + type: string + title: String + required: + - message + - ok + title: Error deprecated: false /api/auth/logout: post: @@ -70,6 +108,44 @@ paths: required: - ok title: Success + "401": + description: Unauthorized + headers: {} + content: + application/json: + schema: + type: object + example: "{\"ok\":false,\"message\":\"Error Message\"}" + properties: + ok: + type: boolean + title: Boolean + message: + type: string + title: String + required: + - message + - ok + title: Error + "500": + description: Internal Server Error + headers: {} + content: + application/json: + schema: + type: object + example: "{\"ok\":false,\"message\":\"Error Message\"}" + properties: + ok: + type: boolean + title: Boolean + message: + type: string + title: String + required: + - message + - ok + title: Error deprecated: false security: - AppDefaultSecurityScheme: [] @@ -290,6 +366,164 @@ paths: deprecated: false security: - AppDefaultSecurityScheme: [] + /api/networks/{owner}/{name}/init: + post: + tags: [] + parameters: + - name: owner + in: path + required: true + deprecated: false + explode: false + schema: + type: string + title: String + - name: name + in: path + required: true + deprecated: false + explode: false + schema: + type: string + title: String + responses: + "200": + headers: {} + content: + application/json: + schema: + type: object + properties: + ok: + type: boolean + title: Boolean + data: + type: "null" + title: Unit + required: + - data + - ok + title: Success + "401": + description: Unauthorized + headers: {} + content: + application/json: + schema: + type: object + example: "{\"ok\":false,\"message\":\"Error Message\"}" + properties: + ok: + type: boolean + title: Boolean + message: + type: string + title: String + required: + - message + - ok + title: Error + "404": + description: Not Found + headers: {} + content: + application/json: + schema: + type: object + example: "{\"ok\":false,\"message\":\"Error Message\"}" + properties: + ok: + type: boolean + title: Boolean + message: + type: string + title: String + required: + - message + - ok + title: Error + deprecated: false + security: + - AppDefaultSecurityScheme: [] + /api/networks/{owner}/{name}/clean: + post: + tags: [] + parameters: + - name: owner + in: path + required: true + deprecated: false + explode: false + schema: + type: string + title: String + - name: name + in: path + required: true + deprecated: false + explode: false + schema: + type: string + title: String + responses: + "200": + headers: {} + content: + application/json: + schema: + type: object + properties: + ok: + type: boolean + title: Boolean + data: + type: "null" + title: Unit + required: + - data + - ok + title: Success + "401": + description: Unauthorized + headers: {} + content: + application/json: + schema: + type: object + example: "{\"ok\":false,\"message\":\"Error Message\"}" + properties: + ok: + type: boolean + title: Boolean + message: + type: string + title: String + required: + - message + - ok + title: Error + "404": + description: Not Found + headers: {} + content: + application/json: + schema: + type: object + example: "{\"ok\":false,\"message\":\"Error Message\"}" + properties: + ok: + type: boolean + title: Boolean + message: + type: string + title: String + required: + - message + - ok + title: Error + deprecated: false + security: + - AppDefaultSecurityScheme: [] /api/users/me: get: tags: [] @@ -311,6 +545,158 @@ paths: - data - ok title: Success + "401": + description: Unauthorized + headers: {} + content: + application/json: + schema: + type: object + example: "{\"ok\":false,\"message\":\"Error Message\"}" + properties: + ok: + type: boolean + title: Boolean + message: + type: string + title: String + required: + - message + - ok + title: Error + deprecated: false + security: + - AppDefaultSecurityScheme: [] + /api/containers/create: + post: + tags: [] + parameters: [] + requestBody: + content: + application/json: + schema: + type: object + properties: + fullNetworkName: + type: string + example: naotiki/my-nw1 + title: String + required: + - fullNetworkName + title: CreateContainerRequest + required: false + responses: + "200": + headers: {} + content: + application/json: + schema: + type: object + properties: + ok: + type: boolean + title: Boolean + data: + $ref: "#/components/schemas/Container" + required: + - data + - ok + title: Success + "201": + description: Created + headers: {} + content: + application/json: + schema: + type: object + properties: + id: + type: string + format: uuid + title: Uuid + containerId: + type: string + title: DockerId + network: + $ref: "#/components/schemas/Network" + ipAddress: + type: integer + maximum: 4294967295 + minimum: 0 + title: IPv4Address + hostVEthName: + type: + - "null" + - string + title: NetworkInterfaceName + containerVEthName: + type: + - "null" + - string + title: NetworkInterfaceName + required: + - containerId + - id + - ipAddress + - network + title: Container + "401": + description: Unauthorized + headers: {} + content: + application/json: + schema: + type: object + example: "{\"ok\":false,\"message\":\"Error Message\"}" + properties: + ok: + type: boolean + title: Boolean + message: + type: string + title: String + required: + - message + - ok + title: Error + "400": + description: Bad Request + headers: {} + content: + application/json: + schema: + type: object + example: "{\"ok\":false,\"message\":\"Error Message\"}" + properties: + ok: + type: boolean + title: Boolean + message: + type: string + title: String + required: + - message + - ok + title: Error + "500": + description: Internal Server Error + headers: {} + content: + application/json: + schema: + type: object + example: "{\"ok\":false,\"message\":\"Error Message\"}" + properties: + ok: + type: boolean + title: Boolean + message: + type: string + title: String + required: + - message + - ok + title: Error deprecated: false security: - AppDefaultSecurityScheme: [] @@ -388,7 +774,7 @@ components: type: - "null" - string - title: DockerId + title: DockerId#6781840936060560326 tunInterfaceName: type: - "null" @@ -424,12 +810,8 @@ components: username: type: string title: String - passwordHash: - type: string - title: Argon2 required: - id - - passwordHash - username title: User NetworkMember: @@ -464,6 +846,39 @@ components: - id - username title: UserResponse + Container: + type: object + properties: + id: + type: string + format: uuid + title: Uuid + containerId: + type: string + title: DockerId + network: + $ref: "#/components/schemas/Network" + ipAddress: + type: integer + maximum: 4294967295 + minimum: 0 + title: IPv4Address + hostVEthName: + type: + - "null" + - string + title: NetworkInterfaceName + containerVEthName: + type: + - "null" + - string + title: NetworkInterfaceName + required: + - containerId + - id + - ipAddress + - network + title: Container examples: {} securitySchemes: AppDefaultSecurityScheme: From fc94c9cffd6c4c41597f37713db7e82c5222cf78 Mon Sep 17 00:00:00 2001 From: Naotiki Date: Sat, 22 Feb 2025 22:34:49 +0900 Subject: [PATCH 05/10] chore: Makefile.toml add description --- Makefile.toml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/Makefile.toml b/Makefile.toml index dbab002..b534826 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -3,9 +3,12 @@ env_files = [".env.dev", ".env"] default_to_workspace = false [tasks.openapi-fetch] +description = "Fetch latest OpenAPI schema" command = "curl" args = ["-o", "schema.yaml", "${BACKEND_REST_HOST}/api/schema.yaml"] + [tasks.openapi-gen] +description = "Generate Rust client from OpenAPI schema" command = "docker" windows.env = { "PWD" = {script = [ "cd" ]}} linux.env = { "UID" = { script = [ @@ -30,5 +33,7 @@ args = [ "/local/backend_api", "--skip-validate-spec", ] + [tasks.openapi] +description = "Fetch and generate OpenAPI schema" dependencies = ["openapi-fetch", "openapi-gen"] From e562f6220bfe165c83d52cb9a94cbe92cbe1fdf9 Mon Sep 17 00:00:00 2001 From: Naotiki Date: Sat, 22 Feb 2025 22:36:45 +0900 Subject: [PATCH 06/10] docs: .env.dev add comment --- .env.dev | 3 +++ server/proto | 1 + 2 files changed, 4 insertions(+) create mode 160000 server/proto diff --git a/.env.dev b/.env.dev index 15da5bf..dc7d3d9 100644 --- a/.env.dev +++ b/.env.dev @@ -1 +1,4 @@ +### このファイルの内容は .env ファイルで上書きできます + +# OpenAPI Schemaを取得するホスト BACKEND_REST_HOST=http://localhost:8080 \ No newline at end of file diff --git a/server/proto b/server/proto new file mode 160000 index 0000000..b2e8120 --- /dev/null +++ b/server/proto @@ -0,0 +1 @@ +Subproject commit b2e812008b0ad6a29e9503c9f6d6d679b35c87a0 From f597fb132d6335f58b94d4266a00479ef2513103 Mon Sep 17 00:00:00 2001 From: Naotiki Date: Sun, 23 Feb 2025 19:10:36 +0900 Subject: [PATCH 07/10] feat: update api error handling --- client/src/commands/login.rs | 16 +++++++++++++++- client/src/commands/logout.rs | 10 ++++------ 2 files changed, 19 insertions(+), 7 deletions(-) diff --git a/client/src/commands/login.rs b/client/src/commands/login.rs index 0bfc359..bd28d48 100644 --- a/client/src/commands/login.rs +++ b/client/src/commands/login.rs @@ -73,7 +73,11 @@ impl Command for Login { } } -async fn login(username: String, password: String, url: Url) -> openapi::models::UserWithTokenResponse{ +async fn login( + username: String, + password: String, + url: Url, +) -> openapi::models::UserWithTokenResponse { let login_credential = openapi::models::LoginCredential { username, password }; let conf = get_api_config(url.as_str().to_string()); @@ -86,6 +90,16 @@ async fn login(username: String, password: String, url: Url) -> openapi::models: match response_content.entity { Some((e)) => match e { openapi::apis::default_api::ApiAuthLoginPostError::UnknownValue(_value) => { + eprintln!("Login Failed!"); + eprintln!("Unknown Error {:?}", _value); + exit(1); + } + openapi::apis::default_api::ApiAuthLoginPostError::Status400(error) => { + eprint!("Login Failed!"); + eprintln!("Bad Request: {:?}", error); + exit(1); + } + openapi::apis::default_api::ApiAuthLoginPostError::Status401(error) => { eprintln!("Login Failed!"); eprintln!("Wrong username or password."); eprintln!("Check your credentials."); diff --git a/client/src/commands/logout.rs b/client/src/commands/logout.rs index f023d91..3df897d 100644 --- a/client/src/commands/logout.rs +++ b/client/src/commands/logout.rs @@ -38,12 +38,10 @@ async fn logout(token: String, url: Url) -> Option> { Err(e) => match e { openapi::apis::Error::ResponseError(response_content) => { match response_content.entity { - Some(e) => match e { - openapi::apis::default_api::ApiAuthLogoutPostError::UnknownValue(value) => { - eprintln!("Error: {}", value); - exit(1); - } - }, + Some(e) => { + eprintln!("Error: {:?}", e); + exit(1); + } None => { eprintln!("Invalid response {}", response_content.status); exit(1); From fb542569299f35edb2ed1de8f8f21306ff42c214 Mon Sep 17 00:00:00 2001 From: Naotiki Date: Sun, 23 Feb 2025 19:19:54 +0900 Subject: [PATCH 08/10] feat: add example .env file and update Makefile for environment loading --- .env.dev => .env.example | 0 Makefile.toml | 16 ++++++++++++++-- 2 files changed, 14 insertions(+), 2 deletions(-) rename .env.dev => .env.example (100%) diff --git a/.env.dev b/.env.example similarity index 100% rename from .env.dev rename to .env.example diff --git a/Makefile.toml b/Makefile.toml index b534826..aeb3fd6 100644 --- a/Makefile.toml +++ b/Makefile.toml @@ -1,4 +1,16 @@ -env_files = [".env.dev", ".env"] +env_files = [{ path = ".env.example"}] +# Load .env file if it exists +env_scripts = [''' +#!@duckscript +if is_file .env + text = readfile .env + handle = map + map_load_properties ${handle} ${text} + set_env --handle ${handle} +end +'''] + + [config] default_to_workspace = false @@ -10,7 +22,7 @@ args = ["-o", "schema.yaml", "${BACKEND_REST_HOST}/api/schema.yaml"] [tasks.openapi-gen] description = "Generate Rust client from OpenAPI schema" command = "docker" -windows.env = { "PWD" = {script = [ "cd" ]}} +windows.env = { "PWD" = { script = ["cd"] } } linux.env = { "UID" = { script = [ "id -u", ] }, "GID" = { script = [ From 90542b50e988802419a150be182a5c1f212b0ddd Mon Sep 17 00:00:00 2001 From: Naotiki Date: Sun, 23 Feb 2025 19:20:07 +0900 Subject: [PATCH 09/10] style: apply lint --- client/src/api_client.rs | 5 ++--- client/src/main.rs | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/client/src/api_client.rs b/client/src/api_client.rs index 562bab7..358310a 100644 --- a/client/src/api_client.rs +++ b/client/src/api_client.rs @@ -2,9 +2,8 @@ use std::sync::{LazyLock, Mutex}; use openapi::apis::configuration::Configuration; - -pub fn get_api_config(basepath:String) -> Configuration { +pub fn get_api_config(basepath: String) -> Configuration { let mut conf = Configuration::new(); conf.base_path = basepath; conf -} \ No newline at end of file +} diff --git a/client/src/main.rs b/client/src/main.rs index 713ba45..623ed24 100644 --- a/client/src/main.rs +++ b/client/src/main.rs @@ -2,9 +2,9 @@ use clap::Parser; use commands::Args; use config::Config; +mod api_client; mod commands; mod config; -mod api_client; #[tokio::main] async fn main() { From d3774f56dda30c92bb9249498f3ceb344a5c33ee Mon Sep 17 00:00:00 2001 From: Naotiki Date: Sun, 23 Feb 2025 19:22:05 +0900 Subject: [PATCH 10/10] refactor: remove unused code --- client/src/api_client.rs | 2 -- client/src/commands/login.rs | 7 +++---- client/src/commands/logout.rs | 1 - 3 files changed, 3 insertions(+), 7 deletions(-) diff --git a/client/src/api_client.rs b/client/src/api_client.rs index 358310a..bb778f0 100644 --- a/client/src/api_client.rs +++ b/client/src/api_client.rs @@ -1,5 +1,3 @@ -use std::sync::{LazyLock, Mutex}; - use openapi::apis::configuration::Configuration; pub fn get_api_config(basepath: String) -> Configuration { diff --git a/client/src/commands/login.rs b/client/src/commands/login.rs index bd28d48..f21605e 100644 --- a/client/src/commands/login.rs +++ b/client/src/commands/login.rs @@ -4,8 +4,7 @@ use crate::{ config::{Config, ServerConfig}, }; use dialoguer::{Confirm, Input, Password}; -use reqwest::Client; -use std::{ops::Deref, process::exit}; +use std::process::exit; use url::Url; #[derive(clap::Parser, Debug)] @@ -88,7 +87,7 @@ async fn login( Err(e) => match e { openapi::apis::Error::ResponseError(response_content) => { match response_content.entity { - Some((e)) => match e { + Some(e) => match e { openapi::apis::default_api::ApiAuthLoginPostError::UnknownValue(_value) => { eprintln!("Login Failed!"); eprintln!("Unknown Error {:?}", _value); @@ -99,7 +98,7 @@ async fn login( eprintln!("Bad Request: {:?}", error); exit(1); } - openapi::apis::default_api::ApiAuthLoginPostError::Status401(error) => { + openapi::apis::default_api::ApiAuthLoginPostError::Status401(_error) => { eprintln!("Login Failed!"); eprintln!("Wrong username or password."); eprintln!("Check your credentials."); diff --git a/client/src/commands/logout.rs b/client/src/commands/logout.rs index 3df897d..000119f 100644 --- a/client/src/commands/logout.rs +++ b/client/src/commands/logout.rs @@ -1,7 +1,6 @@ use super::{Args, Command}; use crate::{api_client::get_api_config, Config}; use openapi::apis::default_api::api_auth_logout_post; -use reqwest::{header, Client}; use std::process::exit; use url::Url;