From 63551fce3864a382d4977e2297d1d739f9959d6b Mon Sep 17 00:00:00 2001 From: ramiroaisen <52116153+ramiroaisen@users.noreply.github.com> Date: Mon, 15 Jan 2024 13:19:16 -0300 Subject: [PATCH 1/2] feat: add modify and validate, validate all json payloads --- .cargo/config.toml | 2 + Cargo.lock | 140 +++++++---- Cargo.toml | 3 + defs/api/accounts/POST/Payload.schema.json | 4 +- .../[account]/PATCH/Payload.schema.json | 48 ++-- defs/api/accounts/[account]/PATCH/Payload.ts | 2 +- defs/api/admins/POST/Payload.schema.json | 16 +- .../admins/[admin]/PATCH/Payload.schema.json | 7 +- defs/api/admins/[admin]/PATCH/Payload.ts | 2 +- .../change-password/POST/Payload.schema.json | 4 +- .../delegate/[user]/POST/Payload.schema.json | 4 +- .../set-password/POST/Payload.schema.json | 4 +- .../user/register/POST/Payload.schema.json | 34 ++- defs/api/auth/user/register/POST/Payload.ts | 6 +- defs/api/invitations/POST/Payload.schema.json | 4 +- .../accept/POST/Payload.schema.json | 14 +- defs/api/invitations/accept/POST/Payload.ts | 2 +- ...authenticatedAcceptPayloadData.schema.json | 14 +- defs/api/me/api-keys/POST/Payload.schema.json | 4 +- .../api-keys/[id]/PATCH/Payload.schema.json | 2 + defs/api/plans/POST/Payload.schema.json | 3 + .../plans/[plan]/PATCH/Payload.schema.json | 3 + .../[station]/PATCH/Payload.schema.json | 3 +- defs/api/stations/[station]/PATCH/Payload.ts | 2 +- .../[file]/metadata/PUT/Payload.schema.json | 6 + defs/api/users/POST/Payload.schema.json | 20 +- .../users/[user]/PATCH/Payload.schema.json | 8 + .../change-password/POST/Payload.schema.json | 4 +- defs/constants.ts | 28 ++- defs/error/PublicErrorCode.ts | 1 + defs/error/PublicErrorPayload.schema.json | 1 + openapi.json | 193 ++++++++++---- rs/config/constants/src/lib.rs | 41 ++- rs/packages/api/Cargo.toml | 3 +- rs/packages/api/src/error/mod.rs | 11 +- rs/packages/api/src/error/public.rs | 2 + rs/packages/api/src/routes/accounts/id.rs | 41 ++- .../api/src/routes/accounts/members/id.rs | 10 +- rs/packages/api/src/routes/accounts/mod.rs | 14 +- .../api/src/routes/admins/change_password.rs | 10 +- rs/packages/api/src/routes/admins/id.rs | 14 +- rs/packages/api/src/routes/admins/mod.rs | 42 +++- .../src/routes/auth/admin/delegate/user.rs | 11 +- .../api/src/routes/auth/admin/login.rs | 5 +- .../auth/email_verification/send_code.rs | 4 +- rs/packages/api/src/routes/auth/user/login.rs | 4 +- .../api/src/routes/auth/user/recover.rs | 4 +- .../user/recovery_token/token/set_password.rs | 10 +- .../api/src/routes/auth/user/register.rs | 74 +++++- .../api/src/routes/invitations/accept.rs | 66 ++++- rs/packages/api/src/routes/invitations/mod.rs | 16 +- .../api/src/routes/invitations/reject.rs | 10 +- rs/packages/api/src/routes/me/api_keys/id.rs | 14 +- rs/packages/api/src/routes/me/api_keys/mod.rs | 20 +- .../api/src/routes/payment_methods/mod.rs | 4 +- rs/packages/api/src/routes/plans/id.rs | 41 ++- rs/packages/api/src/routes/plans/mod.rs | 44 ++-- .../api/src/routes/stations/files/id.rs | 2 +- .../api/src/routes/stations/files/metadata.rs | 60 ++++- .../api/src/routes/stations/files/order.rs | 12 +- rs/packages/api/src/routes/stations/id.rs | 18 +- rs/packages/api/src/routes/stations/mod.rs | 15 +- .../api/src/routes/stations/transfer.rs | 4 +- .../api/src/routes/users/change_password.rs | 11 +- rs/packages/api/src/routes/users/id.rs | 44 +++- rs/packages/api/src/routes/users/mod.rs | 61 ++++- rs/packages/db/Cargo.toml | 3 +- rs/packages/db/src/models/account/mod.rs | 6 +- rs/packages/db/src/models/admin/mod.rs | 23 +- rs/packages/db/src/models/station/mod.rs | 12 +- rs/packages/db/src/models/user/mod.rs | 5 +- rs/packages/modify/Cargo.toml | 30 +++ rs/packages/modify/src/lib.rs | 10 + rs/packages/modify/src/traits.rs | 214 ++++++++++++++++ rs/packages/modify_derive/Cargo.toml | 26 ++ rs/packages/modify_derive/src/fields.rs | 235 ++++++++++++++++++ rs/packages/modify_derive/src/lib.rs | 11 + rs/packages/modify_derive/src/modify/impl.rs | 81 ++++++ rs/packages/modify_derive/src/modify/mod.rs | 2 + .../modify_derive/src/modify/modifier.rs | 148 +++++++++++ rs/packages/modify_derive/src/tokens.rs | 18 ++ rs/packages/prex/Cargo.toml | 2 + rs/packages/prex/src/request.rs | 24 +- 83 files changed, 1860 insertions(+), 320 deletions(-) create mode 100644 rs/packages/modify/Cargo.toml create mode 100644 rs/packages/modify/src/lib.rs create mode 100644 rs/packages/modify/src/traits.rs create mode 100644 rs/packages/modify_derive/Cargo.toml create mode 100644 rs/packages/modify_derive/src/fields.rs create mode 100644 rs/packages/modify_derive/src/lib.rs create mode 100644 rs/packages/modify_derive/src/modify/impl.rs create mode 100644 rs/packages/modify_derive/src/modify/mod.rs create mode 100644 rs/packages/modify_derive/src/modify/modifier.rs create mode 100644 rs/packages/modify_derive/src/tokens.rs diff --git a/.cargo/config.toml b/.cargo/config.toml index d7ed11eb..54dd4676 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,3 +1,5 @@ +vendor = false + [build] rustflags = ["--cfg", "tokio_unstable"] # target="x86_64-unknown-linux-musl" diff --git a/Cargo.lock b/Cargo.lock index 23337ce4..ff9c07f7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -118,6 +118,12 @@ dependencies = [ "alloc-no-stdlib", ] +[[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" @@ -162,6 +168,7 @@ dependencies = [ "macros", "mailer", "media", + "modify", "mongodb", "openapi", "owo-colors 3.5.0", @@ -192,7 +199,7 @@ dependencies = [ "url", "user-agent", "validate", - "validify", + "validator", ] [[package]] @@ -714,18 +721,17 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.23" +version = "0.4.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "16b0a3d9ed01224b22057780a37bb8c5dbfe1be8ba48678e7bf57ec4b385411f" +checksum = "7f2c685bad3eb3d45a01354cedb7d5faa66194d1d58ba6e267a8de788f79db38" dependencies = [ + "android-tzdata", "iana-time-zone", "js-sys", - "num-integer", "num-traits", "serde", - "time 0.1.45", "wasm-bindgen", - "winapi", + "windows-targets", ] [[package]] @@ -1340,6 +1346,7 @@ dependencies = [ "log", "logger", "macros", + "modify", "mongodb", "once_cell", "openapi", @@ -1364,7 +1371,7 @@ dependencies = [ "url", "user-agent", "validate", - "validify", + "validator", "woothee", ] @@ -1467,36 +1474,6 @@ dependencies = [ "syn 1.0.107", ] -[[package]] -name = "derive_validator" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fa86405aedd71f0b86df8d2e3c333319fdbfe2cffd69f1159070fd51e9a465e4" -dependencies = [ - "lazy_static", - "proc-macro-error", - "proc-macro2", - "quote", - "regex", - "syn 1.0.107", - "validify_types", -] - -[[package]] -name = "derive_validify" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbce8c9c563f7dc4205e5cda8e3675b283a29aa2b12fb2a74dd55e25263ee9e7" -dependencies = [ - "lazy_static", - "proc-macro-error", - "proc-macro2", - "quote", - "regex", - "syn 1.0.107", - "validify_types", -] - [[package]] name = "dialoguer" version = "0.10.2" @@ -2663,6 +2640,16 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "idna" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d20d6b07bfbc108882d88ed8e37d39636dcc260e15e30c45e6ba089610b917c" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + [[package]] name = "idna" version = "0.5.0" @@ -2673,6 +2660,12 @@ dependencies = [ "unicode-normalization", ] +[[package]] +name = "if_chain" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" + [[package]] name = "image" version = "0.24.6" @@ -2715,6 +2708,7 @@ checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d" dependencies = [ "equivalent", "hashbrown 0.14.0", + "serde", ] [[package]] @@ -3498,6 +3492,37 @@ dependencies = [ "tokio", ] +[[package]] +name = "modify" +version = "1.3.0" +dependencies = [ + "card-validate", + "chrono", + "idna 0.5.0", + "indexmap 2.0.0", + "lazy_static", + "modify_derive", + "phonenumber", + "regex", + "serde", + "serde_json", + "unic-ucd-common", + "url", +] + +[[package]] +name = "modify_derive" +version = "1.3.0" +dependencies = [ + "chrono", + "lazy_static", + "proc-macro-error", + "proc-macro2", + "quote", + "regex", + "syn 2.0.48", +] + [[package]] name = "mongodb" version = "2.7.0" @@ -4148,6 +4173,7 @@ dependencies = [ "hyper-util", "ip_rfc", "log", + "modify", "pin-project-lite", "regex", "serde", @@ -4159,6 +4185,7 @@ dependencies = [ "tokio-tungstenite", "tower", "tungstenite", + "validator", ] [[package]] @@ -6643,33 +6670,42 @@ dependencies = [ ] [[package]] -name = "validify" -version = "0.1.0" +name = "validator" +version = "0.16.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "632953dabb38aa1d395cd3b9fa5a9e7cc3504c4450539dfdbd1c585b7e9c8fd0" +checksum = "b92f40481c04ff1f4f61f304d61793c7b56ff76ac1469f1beb199b1445b253bd" dependencies = [ - "card-validate", - "derive_validator", - "derive_validify", - "idna 0.2.3", - "indexmap 1.9.2", + "idna 0.4.0", "lazy_static", - "phonenumber", - "proc-macro2", "regex", "serde", "serde_derive", "serde_json", - "syn 1.0.107", - "unic-ucd-common", "url", + "validator_derive", ] [[package]] -name = "validify_types" -version = "0.1.0" +name = "validator_derive" +version = "0.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc44ca3088bb3ba384d9aecf40c6a23a676ce23e09bdaca2073d99c207f864af" +dependencies = [ + "if_chain", + "lazy_static", + "proc-macro-error", + "proc-macro2", + "quote", + "regex", + "syn 1.0.107", + "validator_types", +] + +[[package]] +name = "validator_types" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34f59254530446d576ef853976831863a660e815d19729fb24d513eb4cb66deb" +checksum = "111abfe30072511849c5910134e8baf8dc05de4c0e5903d681cbd5c9c4d611e3" dependencies = [ "proc-macro2", "syn 1.0.107", diff --git a/Cargo.toml b/Cargo.toml index 2375417c..eb3a4f1e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -82,6 +82,9 @@ members = [ "rs/packages/payments", + "rs/packages/modify", + "rs/packages/modify_derive", + "rs/packages/metre", "rs/packages/metre-macros", ] diff --git a/defs/api/accounts/POST/Payload.schema.json b/defs/api/accounts/POST/Payload.schema.json index 4a06c28b..d1015321 100644 --- a/defs/api/accounts/POST/Payload.schema.json +++ b/defs/api/accounts/POST/Payload.schema.json @@ -7,7 +7,9 @@ ], "properties": { "name": { - "type": "string" + "type": "string", + "maxLength": 60, + "minLength": 1 }, "plan_id": { "type": "string" diff --git a/defs/api/accounts/[account]/PATCH/Payload.schema.json b/defs/api/accounts/[account]/PATCH/Payload.schema.json index 4aea9d8d..b9fe728f 100644 --- a/defs/api/accounts/[account]/PATCH/Payload.schema.json +++ b/defs/api/accounts/[account]/PATCH/Payload.schema.json @@ -1,26 +1,34 @@ { "type": "object", + "required": [ + "patch" + ], "properties": { - "name": { - "type": "string", - "maxLength": 60, - "minLength": 1, - "nullable": true - }, - "plan_id": { - "type": "string", - "nullable": true - }, - "user_metadata": { + "patch": { "type": "object", - "additionalProperties": true, - "nullable": true - }, - "system_metadata": { - "type": "object", - "additionalProperties": true, - "nullable": true + "properties": { + "name": { + "type": "string", + "maxLength": 60, + "minLength": 1, + "nullable": true + }, + "plan_id": { + "type": "string", + "nullable": true + }, + "user_metadata": { + "type": "object", + "additionalProperties": true, + "nullable": true + }, + "system_metadata": { + "type": "object", + "additionalProperties": true, + "nullable": true + } + }, + "additionalProperties": false } - }, - "additionalProperties": false + } } \ No newline at end of file diff --git a/defs/api/accounts/[account]/PATCH/Payload.ts b/defs/api/accounts/[account]/PATCH/Payload.ts index a80b9351..4a8d5af8 100644 --- a/defs/api/accounts/[account]/PATCH/Payload.ts +++ b/defs/api/accounts/[account]/PATCH/Payload.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { AccountPatch } from "../../../../ops/AccountPatch"; -export type Payload = AccountPatch; +export type Payload = { patch: AccountPatch }; diff --git a/defs/api/admins/POST/Payload.schema.json b/defs/api/admins/POST/Payload.schema.json index 5ae0774a..e725a778 100644 --- a/defs/api/admins/POST/Payload.schema.json +++ b/defs/api/admins/POST/Payload.schema.json @@ -8,16 +8,24 @@ ], "properties": { "first_name": { - "type": "string" + "type": "string", + "maxLength": 100, + "minLength": 1 }, "last_name": { - "type": "string" + "type": "string", + "maxLength": 100, + "minLength": 1 }, "email": { - "type": "string" + "type": "string", + "maxLength": 100, + "minLength": 1 }, "password": { - "type": "string" + "type": "string", + "maxLength": 60, + "minLength": 8 }, "system_metadata": { "type": "object", diff --git a/defs/api/admins/[admin]/PATCH/Payload.schema.json b/defs/api/admins/[admin]/PATCH/Payload.schema.json index e52b8107..0cb9a299 100644 --- a/defs/api/admins/[admin]/PATCH/Payload.schema.json +++ b/defs/api/admins/[admin]/PATCH/Payload.schema.json @@ -3,10 +3,14 @@ "properties": { "first_name": { "type": "string", + "maxLength": 100, + "minLength": 1, "nullable": true }, "last_name": { "type": "string", + "maxLength": 100, + "minLength": 1, "nullable": true }, "system_metadata": { @@ -14,6 +18,5 @@ "additionalProperties": true, "nullable": true } - }, - "additionalProperties": false + } } \ No newline at end of file diff --git a/defs/api/admins/[admin]/PATCH/Payload.ts b/defs/api/admins/[admin]/PATCH/Payload.ts index f7719e11..55e750bf 100644 --- a/defs/api/admins/[admin]/PATCH/Payload.ts +++ b/defs/api/admins/[admin]/PATCH/Payload.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { AdminPatch } from "../../../../ops/AdminPatch"; -export type Payload = AdminPatch; +export type Payload = {} & AdminPatch; diff --git a/defs/api/admins/[admin]/change-password/POST/Payload.schema.json b/defs/api/admins/[admin]/change-password/POST/Payload.schema.json index 93ea319a..20e9d2f8 100644 --- a/defs/api/admins/[admin]/change-password/POST/Payload.schema.json +++ b/defs/api/admins/[admin]/change-password/POST/Payload.schema.json @@ -9,7 +9,9 @@ "type": "string" }, "new_password": { - "type": "string" + "type": "string", + "maxLength": 60, + "minLength": 8 } } } \ No newline at end of file diff --git a/defs/api/auth/admin/delegate/[user]/POST/Payload.schema.json b/defs/api/auth/admin/delegate/[user]/POST/Payload.schema.json index 40686f9b..778bfede 100644 --- a/defs/api/auth/admin/delegate/[user]/POST/Payload.schema.json +++ b/defs/api/auth/admin/delegate/[user]/POST/Payload.schema.json @@ -5,7 +5,9 @@ ], "properties": { "title": { - "type": "string" + "type": "string", + "maxLength": 150, + "minLength": 1 } } } \ No newline at end of file diff --git a/defs/api/auth/user/recovery-token/[token]/set-password/POST/Payload.schema.json b/defs/api/auth/user/recovery-token/[token]/set-password/POST/Payload.schema.json index 1576c77c..728ec5d4 100644 --- a/defs/api/auth/user/recovery-token/[token]/set-password/POST/Payload.schema.json +++ b/defs/api/auth/user/recovery-token/[token]/set-password/POST/Payload.schema.json @@ -5,7 +5,9 @@ ], "properties": { "new_password": { - "type": "string" + "type": "string", + "maxLength": 60, + "minLength": 8 } } } \ No newline at end of file diff --git a/defs/api/auth/user/register/POST/Payload.schema.json b/defs/api/auth/user/register/POST/Payload.schema.json index 3e23c957..fd155476 100644 --- a/defs/api/auth/user/register/POST/Payload.schema.json +++ b/defs/api/auth/user/register/POST/Payload.schema.json @@ -12,32 +12,44 @@ "plan_id" ], "properties": { + "first_name": { + "type": "string", + "maxLength": 100, + "minLength": 1 + }, + "last_name": { + "type": "string", + "maxLength": 100, + "minLength": 1 + }, + "account_name": { + "type": "string", + "maxLength": 60, + "minLength": 1 + }, "plan_id": { "type": "string" }, "email": { - "type": "string" + "type": "string", + "maxLength": 100, + "minLength": 1 }, "password": { - "type": "string" + "type": "string", + "maxLength": 60, + "minLength": 8 }, "phone": { "type": "string", + "maxLength": 40, "nullable": true }, "language": { "type": "string", + "maxLength": 100, "nullable": true }, - "first_name": { - "type": "string" - }, - "last_name": { - "type": "string" - }, - "account_name": { - "type": "string" - }, "user_user_metadata": { "type": "object", "additionalProperties": true, diff --git a/defs/api/auth/user/register/POST/Payload.ts b/defs/api/auth/user/register/POST/Payload.ts index af203e25..3ebea7d4 100644 --- a/defs/api/auth/user/register/POST/Payload.ts +++ b/defs/api/auth/user/register/POST/Payload.ts @@ -2,14 +2,14 @@ import type { Metadata } from "../../../../../db/Metadata"; export type Payload = { + first_name: string; + last_name: string; + account_name: string; plan_id: string; email: string; password: string; phone: string | null; language?: string; - first_name: string; - last_name: string; - account_name: string; user_user_metadata?: Metadata; user_system_metadata?: Metadata; account_user_metadata?: Metadata; diff --git a/defs/api/invitations/POST/Payload.schema.json b/defs/api/invitations/POST/Payload.schema.json index 745b243c..bc7a7be4 100644 --- a/defs/api/invitations/POST/Payload.schema.json +++ b/defs/api/invitations/POST/Payload.schema.json @@ -9,7 +9,9 @@ "type": "string" }, "email": { - "type": "string" + "type": "string", + "maxLength": 100, + "minLength": 1 } } } \ No newline at end of file diff --git a/defs/api/invitations/accept/POST/Payload.schema.json b/defs/api/invitations/accept/POST/Payload.schema.json index 81758bbb..53c6b136 100644 --- a/defs/api/invitations/accept/POST/Payload.schema.json +++ b/defs/api/invitations/accept/POST/Payload.schema.json @@ -13,17 +13,25 @@ "type": "string" }, "first_name": { - "type": "string" + "type": "string", + "maxLength": 100, + "minLength": 1 }, "last_name": { - "type": "string" + "type": "string", + "maxLength": 100, + "minLength": 1 }, "phone": { "type": "string", + "maxLength": 40, + "minLength": 1, "nullable": true }, "password": { - "type": "string" + "type": "string", + "maxLength": 60, + "minLength": 8 } } }, diff --git a/defs/api/invitations/accept/POST/Payload.ts b/defs/api/invitations/accept/POST/Payload.ts index 7b9b7aa5..6f4fc050 100644 --- a/defs/api/invitations/accept/POST/Payload.ts +++ b/defs/api/invitations/accept/POST/Payload.ts @@ -1,6 +1,6 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { UnauthenticatedAcceptPayloadData } from "./UnauthenticatedAcceptPayloadData"; -export type Payload = UnauthenticatedAcceptPayloadData | { +export type Payload = {} & UnauthenticatedAcceptPayloadData | { invitation_id: string; }; diff --git a/defs/api/invitations/accept/POST/UnauthenticatedAcceptPayloadData.schema.json b/defs/api/invitations/accept/POST/UnauthenticatedAcceptPayloadData.schema.json index dd784d4f..4807a493 100644 --- a/defs/api/invitations/accept/POST/UnauthenticatedAcceptPayloadData.schema.json +++ b/defs/api/invitations/accept/POST/UnauthenticatedAcceptPayloadData.schema.json @@ -11,17 +11,25 @@ "type": "string" }, "first_name": { - "type": "string" + "type": "string", + "maxLength": 100, + "minLength": 1 }, "last_name": { - "type": "string" + "type": "string", + "maxLength": 100, + "minLength": 1 }, "phone": { "type": "string", + "maxLength": 40, + "minLength": 1, "nullable": true }, "password": { - "type": "string" + "type": "string", + "maxLength": 60, + "minLength": 8 } } } \ No newline at end of file diff --git a/defs/api/me/api-keys/POST/Payload.schema.json b/defs/api/me/api-keys/POST/Payload.schema.json index 2a7e52a8..15ede07e 100644 --- a/defs/api/me/api-keys/POST/Payload.schema.json +++ b/defs/api/me/api-keys/POST/Payload.schema.json @@ -6,7 +6,9 @@ ], "properties": { "title": { - "type": "string" + "type": "string", + "maxLength": 150, + "minLength": 1 }, "password": { "type": "string" diff --git a/defs/api/me/api-keys/[id]/PATCH/Payload.schema.json b/defs/api/me/api-keys/[id]/PATCH/Payload.schema.json index 937d19a7..01d86c10 100644 --- a/defs/api/me/api-keys/[id]/PATCH/Payload.schema.json +++ b/defs/api/me/api-keys/[id]/PATCH/Payload.schema.json @@ -3,6 +3,8 @@ "properties": { "title": { "type": "string", + "maxLength": 150, + "minLength": 1, "nullable": true } } diff --git a/defs/api/plans/POST/Payload.schema.json b/defs/api/plans/POST/Payload.schema.json index 181a3b30..0a6fd723 100644 --- a/defs/api/plans/POST/Payload.schema.json +++ b/defs/api/plans/POST/Payload.schema.json @@ -15,14 +15,17 @@ "properties": { "identifier": { "type": "string", + "maxLength": 100, "minLength": 1 }, "slug": { "type": "string", + "maxLength": 100, "minLength": 1 }, "display_name": { "type": "string", + "maxLength": 100, "minLength": 1 }, "is_user_selectable": { diff --git a/defs/api/plans/[plan]/PATCH/Payload.schema.json b/defs/api/plans/[plan]/PATCH/Payload.schema.json index 52c0d998..e901c541 100644 --- a/defs/api/plans/[plan]/PATCH/Payload.schema.json +++ b/defs/api/plans/[plan]/PATCH/Payload.schema.json @@ -9,16 +9,19 @@ }, "identifier": { "type": "string", + "maxLength": 100, "minLength": 1, "nullable": true }, "slug": { "type": "string", + "maxLength": 100, "minLength": 1, "nullable": true }, "display_name": { "type": "string", + "maxLength": 100, "minLength": 1, "nullable": true }, diff --git a/defs/api/stations/[station]/PATCH/Payload.schema.json b/defs/api/stations/[station]/PATCH/Payload.schema.json index ae91a89f..3353c2d0 100644 --- a/defs/api/stations/[station]/PATCH/Payload.schema.json +++ b/defs/api/stations/[station]/PATCH/Payload.schema.json @@ -613,6 +613,5 @@ "additionalProperties": true, "nullable": true } - }, - "additionalProperties": false + } } \ No newline at end of file diff --git a/defs/api/stations/[station]/PATCH/Payload.ts b/defs/api/stations/[station]/PATCH/Payload.ts index 12857f3b..7f6c818b 100644 --- a/defs/api/stations/[station]/PATCH/Payload.ts +++ b/defs/api/stations/[station]/PATCH/Payload.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { StationPatch } from "../../../../ops/StationPatch"; -export type Payload = StationPatch; +export type Payload = {} & StationPatch; diff --git a/defs/api/stations/[station]/files/[file]/metadata/PUT/Payload.schema.json b/defs/api/stations/[station]/files/[file]/metadata/PUT/Payload.schema.json index 935c63ed..6305f41c 100644 --- a/defs/api/stations/[station]/files/[file]/metadata/PUT/Payload.schema.json +++ b/defs/api/stations/[station]/files/[file]/metadata/PUT/Payload.schema.json @@ -3,22 +3,27 @@ "properties": { "title": { "type": "string", + "maxLength": 100, "nullable": true }, "artist": { "type": "string", + "maxLength": 100, "nullable": true }, "album": { "type": "string", + "maxLength": 100, "nullable": true }, "album_artist": { "type": "string", + "maxLength": 100, "nullable": true }, "genre": { "type": "string", + "maxLength": 100, "nullable": true }, "year": { @@ -28,6 +33,7 @@ }, "comment": { "type": "string", + "maxLength": 100, "nullable": true }, "track": { diff --git a/defs/api/users/POST/Payload.schema.json b/defs/api/users/POST/Payload.schema.json index 434125cd..1c320ce3 100644 --- a/defs/api/users/POST/Payload.schema.json +++ b/defs/api/users/POST/Payload.schema.json @@ -8,23 +8,35 @@ ], "properties": { "email": { - "type": "string" + "type": "string", + "maxLength": 100, + "minLength": 1 }, "phone": { "type": "string", + "maxLength": 40, + "minLength": 1, "nullable": true }, "password": { - "type": "string" + "type": "string", + "maxLength": 60, + "minLength": 8 }, "first_name": { - "type": "string" + "type": "string", + "maxLength": 100, + "minLength": 1 }, "last_name": { - "type": "string" + "type": "string", + "maxLength": 100, + "minLength": 1 }, "language": { "type": "string", + "maxLength": 100, + "minLength": 1, "nullable": true }, "user_metadata": { diff --git a/defs/api/users/[user]/PATCH/Payload.schema.json b/defs/api/users/[user]/PATCH/Payload.schema.json index e9f57e10..e3a2d6ed 100644 --- a/defs/api/users/[user]/PATCH/Payload.schema.json +++ b/defs/api/users/[user]/PATCH/Payload.schema.json @@ -3,18 +3,26 @@ "properties": { "first_name": { "type": "string", + "maxLength": 100, + "minLength": 1, "nullable": true }, "last_name": { "type": "string", + "maxLength": 100, + "minLength": 1, "nullable": true }, "phone": { "type": "string", + "maxLength": 40, + "minLength": 1, "nullable": true }, "language": { "type": "string", + "maxLength": 100, + "minLength": 1, "nullable": true } } diff --git a/defs/api/users/[user]/change-password/POST/Payload.schema.json b/defs/api/users/[user]/change-password/POST/Payload.schema.json index 93ea319a..20e9d2f8 100644 --- a/defs/api/users/[user]/change-password/POST/Payload.schema.json +++ b/defs/api/users/[user]/change-password/POST/Payload.schema.json @@ -9,7 +9,9 @@ "type": "string" }, "new_password": { - "type": "string" + "type": "string", + "maxLength": 60, + "minLength": 8 } } } \ No newline at end of file diff --git a/defs/constants.ts b/defs/constants.ts index 428c863c..b3e982a2 100644 --- a/defs/constants.ts +++ b/defs/constants.ts @@ -125,18 +125,42 @@ export const TOKEN_USER_RECOVERY_VALIDITY_SECS = 3600; /** station's transfer save interval in milliseconds */ export const TRANSFER_SAVE_INTERVAL_MILLIS = 5000; +export const VALIDATE_ACCESS_TOKEN_TITLE_MAX_LEN = 150; + export const VALIDATE_ACCOUNT_NAME_MAX_LEN = 60; export const VALIDATE_ACCOUNT_NAME_MIN_LEN = 1; +export const VALIDATE_ADMIN_EMAIL_MAX_LEN = 100; + export const VALIDATE_ADMIN_FIRST_NAME_MAX_LEN = 100; +export const VALIDATE_ADMIN_LANGUAGE_MAX_LEN = 100; + export const VALIDATE_ADMIN_LAST_NAME_MAX_LEN = 100; export const VALIDATE_ADMIN_PASSWORD_MAX_LEN = 60; export const VALIDATE_ADMIN_PASSWORD_MIN_LEN = 8; +export const VALIDATE_AUDIO_FILE_METADATA_ALBUM_ARTIST_MAX_LEN = 100; + +export const VALIDATE_AUDIO_FILE_METADATA_ALBUM_MAX_LEN = 100; + +export const VALIDATE_AUDIO_FILE_METADATA_ARTIST_MAX_LEN = 100; + +export const VALIDATE_AUDIO_FILE_METADATA_COMMENT_MAX_LEN = 100; + +export const VALIDATE_AUDIO_FILE_METADATA_GENRE_MAX_LEN = 100; + +export const VALIDATE_AUDIO_FILE_METADATA_TITLE_MAX_LEN = 100; + +export const VALIDATE_PLAN_IDENTIFIER_MAX_LEN = 100; + +export const VALIDATE_PLAN_NAME_MAX_LEN = 100; + +export const VALIDATE_PLAN_SLUG_MAX_LEN = 100; + export const VALIDATE_STATION_DESC_MAX_LEN = 4000; export const VALIDATE_STATION_DESC_MIN_LEN = 1; @@ -163,10 +187,12 @@ export const VALIDATE_STATION_URLS_MAX_LEN = 150; export const VALIDATE_STATION_WHATSAPP_MAX_LEN = 60; -export const VALIDATE_USER_EMAIL_MAX_LEN = 80; +export const VALIDATE_USER_EMAIL_MAX_LEN = 100; export const VALIDATE_USER_FIRST_NAME_MAX_LEN = 100; +export const VALIDATE_USER_LANGUAGE_MAX_LEN = 100; + export const VALIDATE_USER_LAST_NAME_MAX_LEN = 100; export const VALIDATE_USER_PASSWORD_MAX_LEN = 60; diff --git a/defs/error/PublicErrorCode.ts b/defs/error/PublicErrorCode.ts index 1e607b66..8e65754c 100644 --- a/defs/error/PublicErrorCode.ts +++ b/defs/error/PublicErrorCode.ts @@ -31,6 +31,7 @@ export type PublicErrorCode = | "PAYLOAD_JSON" | "PAYLOAD_TOO_LARGE" | "PAYLOAD_INVALID" + | "PAYLOAD_VALIDATION_FAILED" | "USER_AUTH_FAILED" | "ADMIN_AUTH_FAILED" | "USER_EMAIL_EXISTS" diff --git a/defs/error/PublicErrorPayload.schema.json b/defs/error/PublicErrorPayload.schema.json index cd531c02..779c340b 100644 --- a/defs/error/PublicErrorPayload.schema.json +++ b/defs/error/PublicErrorPayload.schema.json @@ -54,6 +54,7 @@ "PAYLOAD_JSON", "PAYLOAD_TOO_LARGE", "PAYLOAD_INVALID", + "PAYLOAD_VALIDATION_FAILED", "USER_AUTH_FAILED", "ADMIN_AUTH_FAILED", "USER_EMAIL_EXISTS", diff --git a/openapi.json b/openapi.json index 90cd8709..15604da3 100644 --- a/openapi.json +++ b/openapi.json @@ -420,7 +420,9 @@ ], "properties": { "name": { - "type": "string" + "type": "string", + "maxLength": 60, + "minLength": 1 }, "plan_id": { "type": "string" @@ -1412,29 +1414,37 @@ "application/json": { "schema": { "type": "object", + "required": [ + "patch" + ], "properties": { - "name": { - "type": "string", - "maxLength": 60, - "minLength": 1, - "nullable": true - }, - "plan_id": { - "type": "string", - "nullable": true - }, - "user_metadata": { + "patch": { "type": "object", - "additionalProperties": true, - "nullable": true - }, - "system_metadata": { - "type": "object", - "additionalProperties": true, - "nullable": true + "properties": { + "name": { + "type": "string", + "maxLength": 60, + "minLength": 1, + "nullable": true + }, + "plan_id": { + "type": "string", + "nullable": true + }, + "user_metadata": { + "type": "object", + "additionalProperties": true, + "nullable": true + }, + "system_metadata": { + "type": "object", + "additionalProperties": true, + "nullable": true + } + }, + "additionalProperties": false } - }, - "additionalProperties": false + } } } } @@ -2608,16 +2618,24 @@ ], "properties": { "first_name": { - "type": "string" + "type": "string", + "maxLength": 100, + "minLength": 1 }, "last_name": { - "type": "string" + "type": "string", + "maxLength": 100, + "minLength": 1 }, "email": { - "type": "string" + "type": "string", + "maxLength": 100, + "minLength": 1 }, "password": { - "type": "string" + "type": "string", + "maxLength": 60, + "minLength": 8 }, "system_metadata": { "type": "object", @@ -2825,10 +2843,14 @@ "properties": { "first_name": { "type": "string", + "maxLength": 100, + "minLength": 1, "nullable": true }, "last_name": { "type": "string", + "maxLength": 100, + "minLength": 1, "nullable": true }, "system_metadata": { @@ -2836,8 +2858,7 @@ "additionalProperties": true, "nullable": true } - }, - "additionalProperties": false + } } } } @@ -2943,7 +2964,9 @@ "type": "string" }, "new_password": { - "type": "string" + "type": "string", + "maxLength": 60, + "minLength": 8 } } } @@ -5420,7 +5443,9 @@ ], "properties": { "title": { - "type": "string" + "type": "string", + "maxLength": 150, + "minLength": 1 } } } @@ -6140,7 +6165,9 @@ ], "properties": { "new_password": { - "type": "string" + "type": "string", + "maxLength": 60, + "minLength": 8 } } } @@ -6214,32 +6241,44 @@ "plan_id" ], "properties": { + "first_name": { + "type": "string", + "maxLength": 100, + "minLength": 1 + }, + "last_name": { + "type": "string", + "maxLength": 100, + "minLength": 1 + }, + "account_name": { + "type": "string", + "maxLength": 60, + "minLength": 1 + }, "plan_id": { "type": "string" }, "email": { - "type": "string" + "type": "string", + "maxLength": 100, + "minLength": 1 }, "password": { - "type": "string" + "type": "string", + "maxLength": 60, + "minLength": 8 }, "phone": { "type": "string", + "maxLength": 40, "nullable": true }, "language": { "type": "string", + "maxLength": 100, "nullable": true }, - "first_name": { - "type": "string" - }, - "last_name": { - "type": "string" - }, - "account_name": { - "type": "string" - }, "user_user_metadata": { "type": "object", "additionalProperties": true, @@ -7064,7 +7103,9 @@ "type": "string" }, "email": { - "type": "string" + "type": "string", + "maxLength": 100, + "minLength": 1 } } } @@ -7595,17 +7636,25 @@ "type": "string" }, "first_name": { - "type": "string" + "type": "string", + "maxLength": 100, + "minLength": 1 }, "last_name": { - "type": "string" + "type": "string", + "maxLength": 100, + "minLength": 1 }, "phone": { "type": "string", + "maxLength": 40, + "minLength": 1, "nullable": true }, "password": { - "type": "string" + "type": "string", + "maxLength": 60, + "minLength": 8 } } }, @@ -8604,7 +8653,9 @@ ], "properties": { "title": { - "type": "string" + "type": "string", + "maxLength": 150, + "minLength": 1 }, "password": { "type": "string" @@ -8763,6 +8814,8 @@ "properties": { "title": { "type": "string", + "maxLength": 150, + "minLength": 1, "nullable": true } } @@ -9697,14 +9750,17 @@ "properties": { "identifier": { "type": "string", + "maxLength": 100, "minLength": 1 }, "slug": { "type": "string", + "maxLength": 100, "minLength": 1 }, "display_name": { "type": "string", + "maxLength": 100, "minLength": 1 }, "is_user_selectable": { @@ -10165,16 +10221,19 @@ }, "identifier": { "type": "string", + "maxLength": 100, "minLength": 1, "nullable": true }, "slug": { "type": "string", + "maxLength": 100, "minLength": 1, "nullable": true }, "display_name": { "type": "string", + "maxLength": 100, "minLength": 1, "nullable": true }, @@ -16221,8 +16280,7 @@ "additionalProperties": true, "nullable": true } - }, - "additionalProperties": false + } } } } @@ -18295,22 +18353,27 @@ "properties": { "title": { "type": "string", + "maxLength": 100, "nullable": true }, "artist": { "type": "string", + "maxLength": 100, "nullable": true }, "album": { "type": "string", + "maxLength": 100, "nullable": true }, "album_artist": { "type": "string", + "maxLength": 100, "nullable": true }, "genre": { "type": "string", + "maxLength": 100, "nullable": true }, "year": { @@ -18320,6 +18383,7 @@ }, "comment": { "type": "string", + "maxLength": 100, "nullable": true }, "track": { @@ -22521,23 +22585,35 @@ ], "properties": { "email": { - "type": "string" + "type": "string", + "maxLength": 100, + "minLength": 1 }, "phone": { "type": "string", + "maxLength": 40, + "minLength": 1, "nullable": true }, "password": { - "type": "string" + "type": "string", + "maxLength": 60, + "minLength": 8 }, "first_name": { - "type": "string" + "type": "string", + "maxLength": 100, + "minLength": 1 }, "last_name": { - "type": "string" + "type": "string", + "maxLength": 100, + "minLength": 1 }, "language": { "type": "string", + "maxLength": 100, + "minLength": 1, "nullable": true }, "user_metadata": { @@ -22992,18 +23068,26 @@ "properties": { "first_name": { "type": "string", + "maxLength": 100, + "minLength": 1, "nullable": true }, "last_name": { "type": "string", + "maxLength": 100, + "minLength": 1, "nullable": true }, "phone": { "type": "string", + "maxLength": 40, + "minLength": 1, "nullable": true }, "language": { "type": "string", + "maxLength": 100, + "minLength": 1, "nullable": true } } @@ -23188,7 +23272,9 @@ "type": "string" }, "new_password": { - "type": "string" + "type": "string", + "maxLength": 60, + "minLength": 8 } } } @@ -23295,6 +23381,7 @@ "PAYLOAD_JSON", "PAYLOAD_TOO_LARGE", "PAYLOAD_INVALID", + "PAYLOAD_VALIDATION_FAILED", "USER_AUTH_FAILED", "ADMIN_AUTH_FAILED", "USER_EMAIL_EXISTS", diff --git a/rs/config/constants/src/lib.rs b/rs/config/constants/src/lib.rs index 638b4453..deebfa56 100644 --- a/rs/config/constants/src/lib.rs +++ b/rs/config/constants/src/lib.rs @@ -232,7 +232,10 @@ pub mod validate { pub const VALIDATE_ACCOUNT_NAME_MAX_LEN: usize = 60; #[const_register] - pub const VALIDATE_USER_EMAIL_MAX_LEN: usize = 80; + pub const VALIDATE_USER_EMAIL_MAX_LEN: usize = 100; + + #[const_register] + pub const VALIDATE_ADMIN_EMAIL_MAX_LEN: usize = 100; #[const_register] pub const VALIDATE_USER_FIRST_NAME_MAX_LEN: usize = 100; @@ -260,6 +263,42 @@ pub mod validate { #[const_register] pub const VALIDATE_ADMIN_PASSWORD_MAX_LEN: usize = 60; + + #[const_register] + pub const VALIDATE_ACCESS_TOKEN_TITLE_MAX_LEN: usize = 150; + + #[const_register] + pub const VALIDATE_USER_LANGUAGE_MAX_LEN: usize = 100; + + #[const_register] + pub const VALIDATE_ADMIN_LANGUAGE_MAX_LEN: usize = 100; + + #[const_register] + pub const VALIDATE_PLAN_IDENTIFIER_MAX_LEN: usize = 100; + + #[const_register] + pub const VALIDATE_PLAN_SLUG_MAX_LEN: usize = 100; + + #[const_register] + pub const VALIDATE_PLAN_NAME_MAX_LEN: usize = 100; + + #[const_register] + pub const VALIDATE_AUDIO_FILE_METADATA_TITLE_MAX_LEN: usize = 100; + + #[const_register] + pub const VALIDATE_AUDIO_FILE_METADATA_ARTIST_MAX_LEN: usize = 100; + + #[const_register] + pub const VALIDATE_AUDIO_FILE_METADATA_ALBUM_MAX_LEN: usize = 100; + + #[const_register] + pub const VALIDATE_AUDIO_FILE_METADATA_ALBUM_ARTIST_MAX_LEN: usize = 100; + + #[const_register] + pub const VALIDATE_AUDIO_FILE_METADATA_GENRE_MAX_LEN: usize = 100; + + #[const_register] + pub const VALIDATE_AUDIO_FILE_METADATA_COMMENT_MAX_LEN: usize = 100; } #[cfg(test)] diff --git a/rs/packages/api/Cargo.toml b/rs/packages/api/Cargo.toml index e7513326..8204228e 100644 --- a/rs/packages/api/Cargo.toml +++ b/rs/packages/api/Cargo.toml @@ -59,7 +59,8 @@ upload = { version = "0.1.0", path = "../upload" } url = "2.3.1" user-agent = { version = "0.1.0", path = "../user-agent" } validate = { version = "0.1.0", path = "../validate" } -validify = "0.1.0" +modify = { path = "../modify" } +validator = { version = "0.16.1", features = ["derive"] } [dev-dependencies] test-util = { version = "0.1.0", path = "../test-util" } diff --git a/rs/packages/api/src/error/mod.rs b/rs/packages/api/src/error/mod.rs index 12b143d4..7773c960 100644 --- a/rs/packages/api/src/error/mod.rs +++ b/rs/packages/api/src/error/mod.rs @@ -10,6 +10,7 @@ use mailer::send::SendError; use prex::request::{ReadBodyBytesError, ReadBodyJsonError}; use prex::*; use serde_json; +use validator::ValidationErrors; use std::convert::Infallible; use std::process::ExitStatus; @@ -108,6 +109,9 @@ pub enum ApiError { #[error("payload invalid: {0}")] PayloadInvalid(String), + #[error("payload validation failed: {0}")] + PayloadValidation(ValidationErrors), + #[error("user auth failed")] UserAuthFailed, @@ -224,6 +228,7 @@ impl ApiError { PayloadJson(_) => StatusCode::BAD_REQUEST, PayloadTooLarge(_) => StatusCode::BAD_REQUEST, PayloadInvalid(_) => StatusCode::BAD_REQUEST, + PayloadValidation(_) => StatusCode::BAD_REQUEST, UserAuthFailed => StatusCode::BAD_REQUEST, AdminAuthFailed => StatusCode::BAD_REQUEST, @@ -299,6 +304,7 @@ impl ApiError { PayloadJson(e) => format!("Invalid JSON payload: {e}"), PayloadTooLarge(_) => format!("Payload size exceeded"), PayloadInvalid(e) => format!("{e}"), + PayloadValidation(e) => format!("{e}"), UserAuthFailed => format!("There's no user with that email and password"), AdminAuthFailed => format!("There's no admin with that email and password"), UserEmailExists => format!("User email already exists"), @@ -369,10 +375,13 @@ impl ApiError { QueryString(_) => PublicErrorCode::QueryStringInvalid, QueryStringCustom(_) => PublicErrorCode::QueryStringInvalid, + PayloadIo(_) => PublicErrorCode::PayloadIo, PayloadJson(_) => PublicErrorCode::PayloadJson, PayloadTooLarge(_) => PublicErrorCode::PayloadTooLarge, PayloadInvalid(_) => PublicErrorCode::PayloadInvalid, + PayloadValidation(_) => PublicErrorCode::PayloadValidationFailed, + UserAuthFailed => PublicErrorCode::UserAuthFailed, AdminAuthFailed => PublicErrorCode::AdminAuthFailed, UserEmailExists => PublicErrorCode::UserEmailExists, @@ -446,7 +455,7 @@ impl From for ApiError { ReadBodyJsonError::Hyper(e) => Self::PayloadIo(e), ReadBodyJsonError::Json(e) => Self::PayloadJson(e), ReadBodyJsonError::TooLarge(maxlen) => Self::PayloadTooLarge(maxlen), - ReadBodyJsonError::PayloadInvalid(s) => Self::PayloadInvalid(s), + ReadBodyJsonError::Validation(s) => Self::PayloadValidation(s), } } } diff --git a/rs/packages/api/src/error/public.rs b/rs/packages/api/src/error/public.rs index 4061366f..6b15d518 100644 --- a/rs/packages/api/src/error/public.rs +++ b/rs/packages/api/src/error/public.rs @@ -86,10 +86,12 @@ pub enum PublicErrorCode { InvitationNotFound, QueryStringInvalid, + PayloadIo, PayloadJson, PayloadTooLarge, PayloadInvalid, + PayloadValidationFailed, UserAuthFailed, AdminAuthFailed, diff --git a/rs/packages/api/src/routes/accounts/id.rs b/rs/packages/api/src/routes/accounts/id.rs index ffd7724f..42dfbe1a 100644 --- a/rs/packages/api/src/routes/accounts/id.rs +++ b/rs/packages/api/src/routes/accounts/id.rs @@ -93,17 +93,21 @@ pub mod patch { plan::Plan, run_transaction, Model, }; + use modify::Modify; use prex::request::ReadBodyJsonError; use schemars::JsonSchema; - use validify::{ValidationErrors, Validify}; + use validator::Validate; #[derive(Debug, Clone)] pub struct Endpoint {} - #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] + #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema, Modify, Validate)] #[ts(export, export_to = "../../../defs/api/accounts/[account]/PATCH/")] #[macros::schema_ts_export] - pub struct Payload(pub AccountPatch); + pub struct Payload { + #[validate] + pub patch: AccountPatch, + } #[derive(Debug, Clone)] pub struct Input { @@ -140,8 +144,6 @@ pub mod patch { Db(#[from] mongodb::error::Error), #[error("mongodb: {0}")] Token(#[from] GetAccessTokenScopeError), - #[error("mongodb: {0}")] - Validate(#[from] ValidationErrors), #[error("plan not found: {0}")] PlanNotFound(String), #[error("station not found: {0}")] @@ -157,7 +159,6 @@ pub mod patch { HandleError::PlanNotFound(id) => { Self::BadRequestCustom(format!("Plan with {} not found", id)) } - HandleError::Validate(e) => ApiError::BadRequestCustom(format!("{e}")), } } } @@ -185,13 +186,11 @@ pub mod patch { async fn perform(&self, input: Self::Input) -> Result { let Self::Input { - payload: Payload(payload), + payload: Payload { patch }, access_token_scope, account_id, } = input; - let payload: AccountPatch = AccountPatch::validify(payload.into())?; - let account = match access_token_scope { AccessTokenScope::Global | AccessTokenScope::Admin(_) => { run_transaction!(session => { @@ -200,26 +199,26 @@ pub mod patch { Some(account) => account, }; - if let Some(ref name) = payload.name { + if let Some(ref name) = patch.name { account.name = name.clone(); } - if let Some(ref user_metadata) = payload.user_metadata { + if let Some(ref user_metadata) = patch.user_metadata { account.user_metadata.merge(user_metadata.clone()); } - if let Some(ref system_metadata) = payload.system_metadata { + if let Some(ref system_metadata) = patch.system_metadata { account.system_metadata.merge(system_metadata.clone()); } - if let Some(ref plan_id) = payload.plan_id { + if let Some(plan_id) = &patch.plan_id { let plan = match tx_try!(Plan::get_by_id(plan_id).await) { - None => return Err(HandleError::PlanNotFound(plan_id.clone())), + None => return Err(HandleError::PlanNotFound(plan_id.to_string())), Some(plan_id) => plan_id, }; if plan.deleted_at.is_some() { - return Err(HandleError::PlanNotFound(plan_id.clone())); + return Err(HandleError::PlanNotFound(plan_id.to_string())); } account.plan_id = plan.id.clone(); @@ -244,22 +243,22 @@ pub mod patch { Some(account) => account, }; - if let Some(ref name) = payload.name { - account.name = name.clone(); + if let Some(name) = &patch.name { + account.name = name.to_string(); } - if let Some(ref user_metadata) = payload.user_metadata { + if let Some(user_metadata) = &patch.user_metadata { account.user_metadata.merge(user_metadata.clone()); } - if let Some(ref plan_id) = payload.plan_id { + if let Some(plan_id) = &patch.plan_id { let plan = match tx_try!(Plan::get_by_id(plan_id).await) { - None => return Err(HandleError::PlanNotFound(plan_id.clone())), + None => return Err(HandleError::PlanNotFound(plan_id.to_string())), Some(plan_id) => plan_id, }; if plan.deleted_at.is_some() || !plan.is_user_selectable { - return Err(HandleError::PlanNotFound(plan_id.clone())); + return Err(HandleError::PlanNotFound(plan_id.to_string())); } account.plan_id = plan.id.clone(); diff --git a/rs/packages/api/src/routes/accounts/members/id.rs b/rs/packages/api/src/routes/accounts/members/id.rs index 4d7ebe77..7dce3f3d 100644 --- a/rs/packages/api/src/routes/accounts/members/id.rs +++ b/rs/packages/api/src/routes/accounts/members/id.rs @@ -91,9 +91,11 @@ pub mod set_role { models::user_account_relation::UserAccountRelation, user_account_relation::UserAccountRelationKind, }; + use modify::Modify; use prex::request::ReadBodyJsonError; use schemars::JsonSchema; use serde_util::empty_struct::EmptyStruct; + use validator::Validate; #[derive(Debug, Clone)] pub struct Endpoint {} @@ -105,9 +107,11 @@ pub mod set_role { payload: Payload, } - #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] - #[ts(export)] - #[ts(export_to = "../../../defs/api/accounts/[account]/members/[member]/set-role/POST/")] + #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema, Modify, Validate)] + #[ts( + export, + export_to = "../../../defs/api/accounts/[account]/members/[member]/set-role/POST/" + )] #[macros::schema_ts_export] pub struct Payload { role: AccessKind, diff --git a/rs/packages/api/src/routes/accounts/mod.rs b/rs/packages/api/src/routes/accounts/mod.rs index 7efb2c7f..ba741f9c 100644 --- a/rs/packages/api/src/routes/accounts/mod.rs +++ b/rs/packages/api/src/routes/accounts/mod.rs @@ -157,23 +157,35 @@ pub mod get { pub mod post { + use constants::validate::*; use db::account::{Limit, Limits}; use db::models::user_account_relation::UserAccountRelationKind; use db::payment_method::PaymentMethod; use db::plan::Plan; use db::user::User; use db::{current_filter_doc, run_transaction}; + use modify::Modify; use schemars::JsonSchema; use serde_util::DateTime; use ts_rs::TS; + use validator::Validate; use super::*; - #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] + #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema, Modify, Validate)] #[ts(export, export_to = "../../../defs/api/accounts/POST/")] #[macros::schema_ts_export] #[serde(rename_all = "snake_case", deny_unknown_fields)] pub struct Payload { + #[modify(trim)] + #[validate( + length( + min = "VALIDATE_ACCOUNT_NAME_MIN_LEN", + max = "VALIDATE_ACCOUNT_NAME_MAX_LEN", + message = "Account name is either too long or empty" + ), + non_control_character(message = "Account name contains invalid characters") + )] pub name: String, pub plan_id: String, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/rs/packages/api/src/routes/admins/change_password.rs b/rs/packages/api/src/routes/admins/change_password.rs index 38241a53..04f21443 100644 --- a/rs/packages/api/src/routes/admins/change_password.rs +++ b/rs/packages/api/src/routes/admins/change_password.rs @@ -6,7 +6,9 @@ pub mod post { json::JsonHandler, request_ext::{self, GetAccessTokenScopeError}, }; + use constants::validate::*; use db::{admin::Admin, Model}; + use modify::Modify; use mongodb::bson::doc; use prex::{request::ReadBodyJsonError, Request}; use schemars::JsonSchema; @@ -14,11 +16,12 @@ pub mod post { use serde_util::empty_struct::EmptyStruct; use std::net::IpAddr; use ts_rs::TS; + use validator::Validate; #[derive(Debug, Clone)] pub struct Endpoint {} - #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] + #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema, Modify, Validate)] #[ts( export, export_to = "../../../defs/api/admins/[admin]/change-password/POST/" @@ -26,6 +29,11 @@ pub mod post { #[macros::schema_ts_export] pub struct Payload { pub current_password: String, + #[validate(length( + min = "VALIDATE_ADMIN_PASSWORD_MIN_LEN", + max = "VALIDATE_ADMIN_PASSWORD_MAX_LEN", + message = "New password is either too short or too long", + ))] pub new_password: String, } diff --git a/rs/packages/api/src/routes/admins/id.rs b/rs/packages/api/src/routes/admins/id.rs index 1a65543a..0c7bbdb8 100644 --- a/rs/packages/api/src/routes/admins/id.rs +++ b/rs/packages/api/src/routes/admins/id.rs @@ -66,16 +66,22 @@ pub mod patch { error::ApplyPatchError, fetch_and_patch, run_transaction, Model, }; + use modify::Modify; use prex::request::ReadBodyJsonError; use schemars::JsonSchema; + use validator::Validate; #[derive(Debug, Clone)] pub struct Endpoint {} - #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] + #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema, Modify, Validate)] #[ts(export, export_to = "../../../defs/api/admins/[admin]/PATCH/")] #[macros::schema_ts_export] - pub struct Payload(pub AdminPatch); + pub struct Payload { + #[serde(flatten)] + #[validate] + pub patch: AdminPatch, + } #[derive(Debug, Clone)] pub struct Input { @@ -146,7 +152,7 @@ pub mod patch { async fn perform(&self, input: Self::Input) -> Result { let Self::Input { - payload: Payload(payload), + payload: Payload { patch }, admin, } = input; @@ -154,7 +160,7 @@ pub mod patch { let admin = run_transaction!(session => { fetch_and_patch!(Admin, admin, &id, Err(HandleError::AdminNotFound(id)), session, { - admin.apply_patch(payload.clone())? + admin.apply_patch(patch.clone())? }) }); diff --git a/rs/packages/api/src/routes/admins/mod.rs b/rs/packages/api/src/routes/admins/mod.rs index 480b007f..7ff96992 100644 --- a/rs/packages/api/src/routes/admins/mod.rs +++ b/rs/packages/api/src/routes/admins/mod.rs @@ -104,26 +104,66 @@ pub mod get { pub mod post { + use constants::validate::*; use db::admin::{Admin, PublicAdmin}; use db::metadata::Metadata; use db::run_transaction; + use modify::Modify; use prex::request::ReadBodyJsonError; use schemars::JsonSchema; use serde_util::DateTime; use ts_rs::TS; use validate::email::is_valid_email; + use validator::Validate; use super::*; - #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] + #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema, Modify, Validate)] #[ts(export, export_to = "../../../defs/api/admins/POST/")] #[macros::schema_ts_export] #[serde(rename_all = "snake_case", deny_unknown_fields)] pub struct Payload { + #[modify(trim)] + #[validate( + length( + min = 1, + max = "VALIDATE_ADMIN_FIRST_NAME_MAX_LEN", + message = "First name is either too short or too long", + ), + non_control_character(message = "First name contains invalid characters") + )] pub first_name: String, + + #[modify(trim)] + #[validate( + length( + min = 1, + max = "VALIDATE_ADMIN_LAST_NAME_MAX_LEN", + message = "Last name is either too short or too long", + ), + non_control_character(message = "Last name contains invalid characters") + )] pub last_name: String, + + #[modify(trim)] + #[validate( + email(message = "Email is invalid"), + length( + min = 1, + max = "VALIDATE_ADMIN_EMAIL_MAX_LEN", + message = "Email is either too short or too long", + ), + non_control_character(message = "Email contains invalid characters") + )] pub email: String, + + #[validate(length( + min = "VALIDATE_ADMIN_PASSWORD_MIN_LEN", + max = "VALIDATE_ADMIN_PASSWORD_MAX_LEN", + message = "Password is either too short or too long", + ))] pub password: String, + #[serde(skip_serializing_if = "Option::is_none")] pub system_metadata: Option, } diff --git a/rs/packages/api/src/routes/auth/admin/delegate/user.rs b/rs/packages/api/src/routes/auth/admin/delegate/user.rs index fd6e4650..8182b954 100644 --- a/rs/packages/api/src/routes/auth/admin/delegate/user.rs +++ b/rs/packages/api/src/routes/auth/admin/delegate/user.rs @@ -1,9 +1,11 @@ pub mod post { use async_trait::async_trait; + use constants::validate::*; use db::access_token::{AccessToken, GeneratedBy, Scope}; use db::user::{AdminPublicUser, User}; use db::Model; + use modify::Modify; use mongodb::bson::doc; use prex::request::ReadBodyJsonError; use prex::Request; @@ -11,6 +13,7 @@ pub mod post { use serde::{Deserialize, Serialize}; use serde_util::DateTime; use ts_rs::TS; + use validator::Validate; use crate::error::ApiError; use crate::json::JsonHandler; @@ -23,13 +26,19 @@ pub mod post { access_token: AccessToken, } - #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] + #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema, Modify, Validate)] #[ts( export, export_to = "../../../defs/api/auth/admin/delegate/[user]/POST/" )] #[macros::schema_ts_export] pub struct Payload { + #[modify(trim)] + #[validate(length( + min = 1, + max = "VALIDATE_ACCESS_TOKEN_TITLE_MAX_LEN", + message = "Title is either too short or too long" + ))] title: String, } diff --git a/rs/packages/api/src/routes/auth/admin/login.rs b/rs/packages/api/src/routes/auth/admin/login.rs index 4f6d3f6e..7d5d899f 100644 --- a/rs/packages/api/src/routes/auth/admin/login.rs +++ b/rs/packages/api/src/routes/auth/admin/login.rs @@ -3,6 +3,7 @@ pub mod post { use db::access_token::{AccessToken, GeneratedBy, Scope}; use db::admin::{Admin, PublicAdmin}; use db::{current_filter_doc, Model}; + use modify::Modify; use mongodb::bson::doc; use prex::{request::ReadBodyJsonError, Request}; use schemars::JsonSchema; @@ -11,16 +12,18 @@ pub mod post { use std::net::IpAddr; use ts_rs::TS; use user_agent::{UserAgent, UserAgentExt}; + use validator::Validate; use crate::error::ApiError; use crate::ip_limit::{hit, should_reject}; use crate::json::JsonHandler; - #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] + #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema, Modify, Validate)] #[ts(export, export_to = "../../../defs/api/auth/admin/login/POST/")] #[macros::schema_ts_export] #[serde(rename_all = "snake_case", deny_unknown_fields)] pub struct Payload { + #[modify(trim)] email: String, password: String, device_id: String, diff --git a/rs/packages/api/src/routes/auth/email_verification/send_code.rs b/rs/packages/api/src/routes/auth/email_verification/send_code.rs index 08c99cd2..5a76a79a 100644 --- a/rs/packages/api/src/routes/auth/email_verification/send_code.rs +++ b/rs/packages/api/src/routes/auth/email_verification/send_code.rs @@ -6,6 +6,7 @@ pub mod post { use log::warn; use mailer::error::RenderError; use mailer::send::{Address, Email, Mailer, SendError}; + use modify::Modify; use mongodb::bson::doc; use prex::{request::ReadBodyJsonError, Request}; use schemars::JsonSchema; @@ -15,12 +16,13 @@ pub mod post { use std::net::IpAddr; use ts_rs::TS; use validate::email::is_valid_email; + use validator::Validate; use crate::error::ApiError; use crate::ip_limit::{hit, should_reject}; use crate::json::JsonHandler; - #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] + #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema, Modify, Validate)] #[ts( export, export_to = "../../../defs/api/auth/email-verification/send-code/POST/" diff --git a/rs/packages/api/src/routes/auth/user/login.rs b/rs/packages/api/src/routes/auth/user/login.rs index 39b296ff..5132658d 100644 --- a/rs/packages/api/src/routes/auth/user/login.rs +++ b/rs/packages/api/src/routes/auth/user/login.rs @@ -3,6 +3,7 @@ pub mod post { use db::access_token::{AccessToken, GeneratedBy, Scope}; use db::user::{User, UserPublicUser}; use db::{current_filter_doc, run_transaction, Model}; + use modify::Modify; use mongodb::bson::doc; use prex::{request::ReadBodyJsonError, Request}; use schemars::JsonSchema; @@ -11,12 +12,13 @@ pub mod post { use std::net::IpAddr; use ts_rs::TS; use user_agent::{UserAgent, UserAgentExt}; + use validator::Validate; use crate::error::ApiError; use crate::ip_limit::{hit, should_reject}; use crate::json::JsonHandler; - #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] + #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema, Modify, Validate)] #[ts(export, export_to = "../../../defs/api/auth/user/login/POST/")] #[macros::schema_ts_export] #[serde(deny_unknown_fields)] diff --git a/rs/packages/api/src/routes/auth/user/recover.rs b/rs/packages/api/src/routes/auth/user/recover.rs index 13645f2f..545a72a7 100644 --- a/rs/packages/api/src/routes/auth/user/recover.rs +++ b/rs/packages/api/src/routes/auth/user/recover.rs @@ -7,6 +7,7 @@ pub mod post { use log::warn; use mailer::error::RenderError; use mailer::send::{Address, Email, Mailer, SendError}; + use modify::Modify; use mongodb::bson::doc; use prex::{request::ReadBodyJsonError, Request}; use schemars::JsonSchema; @@ -16,12 +17,13 @@ pub mod post { use std::net::IpAddr; use std::time::Duration; use ts_rs::TS; + use validator::Validate; use crate::error::ApiError; use crate::ip_limit::{hit, should_reject}; use crate::json::JsonHandler; - #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] + #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema, Modify, Validate)] #[ts(export, export_to = "../../../defs/api/auth/user/recover/POST/")] #[macros::schema_ts_export] #[serde(deny_unknown_fields)] diff --git a/rs/packages/api/src/routes/auth/user/recovery_token/token/set_password.rs b/rs/packages/api/src/routes/auth/user/recovery_token/token/set_password.rs index 7a9ee35a..773eb6a3 100644 --- a/rs/packages/api/src/routes/auth/user/recovery_token/token/set_password.rs +++ b/rs/packages/api/src/routes/auth/user/recovery_token/token/set_password.rs @@ -14,22 +14,30 @@ pub mod post { use crate::{ip_limit, json::JsonHandler}; use async_trait::async_trait; + use constants::validate::*; use db::token_user_recovery::TokenUserRecovery; + use modify::Modify; use prex::Request; use schemars::JsonSchema; + use validator::Validate; use super::*; #[derive(Debug, Clone)] pub struct Endpoint {} - #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] + #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema, Modify, Validate)] #[ts( export, export_to = "../../../defs/api/auth/user/recovery-token/[token]/set-password/POST/" )] #[macros::schema_ts_export] pub struct Payload { + #[validate(length( + min = "VALIDATE_USER_PASSWORD_MIN_LEN", + max = "VALIDATE_USER_PASSWORD_MAX_LEN", + message = "New password is either too short or too long", + ))] new_password: String, } diff --git a/rs/packages/api/src/routes/auth/user/register.rs b/rs/packages/api/src/routes/auth/user/register.rs index 202bd05b..e2bf46e1 100644 --- a/rs/packages/api/src/routes/auth/user/register.rs +++ b/rs/packages/api/src/routes/auth/user/register.rs @@ -2,6 +2,7 @@ pub mod post { use std::net::IpAddr; use async_trait::async_trait; + use constants::validate::*; use db::access_token::{AccessToken, GeneratedBy, Scope}; use db::account::{Account, Limit, Limits, PublicAccount}; use db::email_verification_code::EmailVerificationCode; @@ -11,6 +12,7 @@ pub mod post { use db::user::{PublicUser, User}; use db::user_account_relation::{UserAccountRelation, UserAccountRelationKind}; use db::{run_transaction, Model}; + use modify::Modify; use mongodb::bson::doc; use payments::query::save_payment_method::SavePaymentMethodResponse; use prex::{request::ReadBodyJsonError, Request}; @@ -20,6 +22,7 @@ pub mod post { use ts_rs::TS; use user_agent::{UserAgent, UserAgentExt}; use validate::email::is_valid_email; + use validator::Validate; use crate::error::ApiError; use crate::json::JsonHandler; @@ -154,23 +157,84 @@ pub mod post { } } - #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] + #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema, Modify, Validate)] #[ts(export, export_to = "../../../defs/api/auth/user/register/POST/")] #[macros::schema_ts_export] #[serde(deny_unknown_fields)] pub struct Payload { + #[modify(trim)] + #[validate( + length( + min = 1, + max = "VALIDATE_USER_FIRST_NAME_MAX_LEN", + message = "First name is either too short or too long" + ), + non_control_character(message = "First name contains invalid characters") + )] + first_name: String, + + #[modify(trim)] + #[validate( + length( + min = 1, + max = "VALIDATE_USER_LAST_NAME_MAX_LEN", + message = "Last name is either too short or too long" + ), + non_control_character(message = "Last name contains invalid characters") + )] + last_name: String, + + #[modify(trim)] + #[validate( + length( + min = 1, + max = "VALIDATE_ACCOUNT_NAME_MAX_LEN", + message = "Account name is either too short or too long" + ), + non_control_character(message = "Account name contains invalid characters") + )] + account_name: String, + plan_id: String, + + #[modify(trim)] + #[validate( + email(message = "Email is invalid"), + length( + min = 1, + max = "VALIDATE_USER_EMAIL_MAX_LEN", + message = "Email is either too short or too long" + ), + non_control_character(message = "Email contains invalid characters") + )] email: String, + + #[validate(length( + min = "VALIDATE_USER_PASSWORD_MIN_LEN", + max = "VALIDATE_USER_PASSWORD_MAX_LEN", + message = "Password is either too short or too long" + ))] password: String, + + #[modify(trim)] + #[validate( + phone(message = "Phone is invalid"), + length(max = "VALIDATE_USER_PHONE_MAX_LEN", message = "Phone is too long"), + non_control_character(message = "Phone name contains invalid characters") + )] phone: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[modify(trim)] + #[validate( + length( + max = "VALIDATE_USER_LANGUAGE_MAX_LEN", + message = "Language is too long" + ), + non_control_character(message = "Language contains invalid characters") + )] language: Option, - first_name: String, - last_name: String, - account_name: String, - #[serde(skip_serializing_if = "Option::is_none")] user_user_metadata: Option, diff --git a/rs/packages/api/src/routes/invitations/accept.rs b/rs/packages/api/src/routes/invitations/accept.rs index 573ed236..587deecc 100644 --- a/rs/packages/api/src/routes/invitations/accept.rs +++ b/rs/packages/api/src/routes/invitations/accept.rs @@ -4,6 +4,7 @@ use crate::{ request_ext::{AccessTokenScope, GetAccessTokenScopeError}, }; use async_trait::async_trait; +use constants::validate::*; use db::account_invitations::{AccountInvitation, AccountInvitationState}; use db::user::User; use db::Model; @@ -13,14 +14,14 @@ use prex::Request; use serde::{Deserialize, Serialize}; use serde_util::DateTime; -use validify::Validify; +use modify::Modify; +use validator::Validate; use db::{ run_transaction, user_account_relation::{UserAccountRelation, UserAccountRelationKind}, }; use ts_rs::TS; -use validify::ValidationErrors; use crate::request_ext::get_optional_access_token_scope; @@ -31,23 +32,67 @@ pub mod post { use super::*; + // TODO: add modify (works in enums?) #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] #[ts(export, export_to = "../../../defs/api/invitations/accept/POST/")] #[macros::schema_ts_export] #[serde(untagged)] pub enum Payload { - Unauthenticated(UnauthenticatedAcceptPayloadData), - Authenticated { invitation_id: String }, + Unauthenticated { + // #[validate] + #[serde(flatten)] + unauthenticated: UnauthenticatedAcceptPayloadData, + }, + Authenticated { + invitation_id: String, + }, } - #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] + #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema, Modify, Validate)] #[ts(export, export_to = "../../../defs/api/invitations/accept/POST/")] #[macros::schema_ts_export] pub struct UnauthenticatedAcceptPayloadData { pub token: String, + + #[modify(trim)] + #[validate( + length( + min = 1, + max = "VALIDATE_USER_FIRST_NAME_MAX_LEN", + message = "First name is either too short or too long" + ), + non_control_character(message = "First name contains invalid characters") + )] pub first_name: String, + + #[modify(trim)] + #[validate( + length( + min = 1, + max = "VALIDATE_USER_LAST_NAME_MAX_LEN", + message = "Last name is either too short or too long" + ), + non_control_character(message = "Last name contains invalid characters") + )] pub last_name: String, + + #[modify(trim)] + #[validate( + phone(message = "Phone number is invalid"), + length( + min = 1, + max = "VALIDATE_USER_PHONE_MAX_LEN", + message = "Phone number is either too short or too long" + ), + non_control_character(message = "Phone number contains invalid characters") + )] pub phone: Option, + + #[validate(length( + min = "VALIDATE_USER_PASSWORD_MIN_LEN", + max = "VALIDATE_USER_PASSWORD_MAX_LEN", + message = "Password is either too short or too long" + ))] pub password: String, } @@ -99,8 +144,6 @@ pub mod post { Db(#[from] mongodb::error::Error), #[error("token: {0}")] Token(#[from] GetAccessTokenScopeError), - #[error("validate")] - Validate(#[from] ValidationErrors), #[error("password too short")] PasswordTooShort, #[error("password too long")] @@ -112,7 +155,6 @@ pub mod post { match e { HandleError::Db(e) => e.into(), HandleError::Token(e) => e.into(), - HandleError::Validate(errors) => ApiError::PayloadInvalid(format!("{}", errors)), HandleError::PasswordTooShort => { ApiError::PayloadInvalid(String::from("password must have 8 characters or more")) } @@ -132,7 +174,7 @@ pub mod post { async fn parse(&self, mut req: Request) -> Result { let optional_access_token_scope = get_optional_access_token_scope(&req).await?; - let payload = req.read_body_json(5_000).await?; + let payload = req.read_body_json_no_validate(10_000).await?; Ok(Input { optional_access_token_scope, payload, @@ -222,14 +264,14 @@ pub mod post { Ok(Output::Ok) } - Payload::Unauthenticated(data) => { + Payload::Unauthenticated { unauthenticated } => { let UnauthenticatedAcceptPayloadData { token, first_name, last_name, password, phone, - } = data; + } = unauthenticated; if password.len() < 8 { return Err(HandleError::PasswordTooShort); @@ -284,8 +326,6 @@ pub mod post { deleted_at: None, }; - let user = User::validify(user.into())?; - let user_account_relation = UserAccountRelation { id: UserAccountRelation::uid(), account_id: invitation.account_id.clone(), diff --git a/rs/packages/api/src/routes/invitations/mod.rs b/rs/packages/api/src/routes/invitations/mod.rs index 0756f66c..4ea05445 100644 --- a/rs/packages/api/src/routes/invitations/mod.rs +++ b/rs/packages/api/src/routes/invitations/mod.rs @@ -326,19 +326,33 @@ pub mod get { pub mod post { + use constants::validate::*; use db::{current_filter_doc, user_account_relation::UserAccountRelation}; use mailer::{ error::RenderError, send::{Address, Email, Mailer, SendError}, }; + use modify::Modify; + use validator::Validate; use super::*; - #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] + #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema, Modify, Validate)] #[ts(export, export_to = "../../../defs/api/invitations/POST/")] #[macros::schema_ts_export] pub struct Payload { pub account_id: String, + + #[modify(trim)] + #[validate( + email(message = "Email is invalid"), + length( + min = 1, + max = "VALIDATE_USER_EMAIL_MAX_LEN", + message = "Email is either too short or too long", + ), + non_control_character(message = "Email contains invalid characters") + )] pub email: String, } diff --git a/rs/packages/api/src/routes/invitations/reject.rs b/rs/packages/api/src/routes/invitations/reject.rs index 5725e93e..97cf6e73 100644 --- a/rs/packages/api/src/routes/invitations/reject.rs +++ b/rs/packages/api/src/routes/invitations/reject.rs @@ -13,16 +13,15 @@ use prex::Request; use serde::{Deserialize, Serialize}; use serde_util::DateTime; use ts_rs::TS; -use validify::ValidationErrors; use crate::request_ext::get_optional_access_token_scope; pub mod post { - use schemars::JsonSchema; - use super::*; + use schemars::JsonSchema; + // TODO: add modify (works in enums?) #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] #[ts(export, export_to = "../../../defs/api/invitations/reject/POST/")] #[macros::schema_ts_export] @@ -76,8 +75,6 @@ pub mod post { Db(#[from] mongodb::error::Error), #[error("token: {0}")] Token(#[from] GetAccessTokenScopeError), - #[error("validate")] - Validate(#[from] ValidationErrors), } impl From for ApiError { @@ -85,7 +82,6 @@ pub mod post { match e { HandleError::Db(e) => e.into(), HandleError::Token(e) => e.into(), - HandleError::Validate(errors) => ApiError::PayloadInvalid(format!("{}", errors)), } } } @@ -99,7 +95,7 @@ pub mod post { async fn parse(&self, mut req: Request) -> Result { let optional_access_token_scope = get_optional_access_token_scope(&req).await?; - let payload = req.read_body_json(5_000).await?; + let payload = req.read_body_json_no_validate(5_000).await?; Ok(Input { optional_access_token_scope, payload, diff --git a/rs/packages/api/src/routes/me/api_keys/id.rs b/rs/packages/api/src/routes/me/api_keys/id.rs index 43b05fe7..c85c3f2d 100644 --- a/rs/packages/api/src/routes/me/api_keys/id.rs +++ b/rs/packages/api/src/routes/me/api_keys/id.rs @@ -135,19 +135,31 @@ pub mod delete { pub mod patch { + use constants::validate::*; + use modify::Modify; use prex::request::ReadBodyJsonError; use schemars::JsonSchema; use serde_util::empty_struct::EmptyStruct; + use validator::Validate; use super::*; #[derive(Debug, Clone)] pub struct Endpoint {} - #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] + #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema, Modify, Validate)] #[ts(export, export_to = "../../../defs/api/me/api-keys/[id]/PATCH/")] #[macros::schema_ts_export] pub struct Payload { + #[modify(trim)] + #[validate( + length( + min = 1, + max = "VALIDATE_ACCESS_TOKEN_TITLE_MAX_LEN", + message = "Title is either too short or too long" + ), + non_control_character(message = "Title contains invalid characters") + )] title: Option, } diff --git a/rs/packages/api/src/routes/me/api_keys/mod.rs b/rs/packages/api/src/routes/me/api_keys/mod.rs index e6a84a5a..869022c0 100644 --- a/rs/packages/api/src/routes/me/api_keys/mod.rs +++ b/rs/packages/api/src/routes/me/api_keys/mod.rs @@ -197,12 +197,13 @@ pub mod get { pub mod post { - use std::net::IpAddr; - + use crate::ip_limit; + use constants::validate::*; + use modify::Modify; use prex::request::ReadBodyJsonError; use schemars::JsonSchema; - - use crate::ip_limit; + use std::net::IpAddr; + use validator::Validate; use super::*; @@ -216,10 +217,19 @@ pub mod post { access_token_scope: AccessTokenScope, } - #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] + #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema, Modify, Validate)] #[ts(export, export_to = "../../../defs/api/me/api-keys/POST/")] #[macros::schema_ts_export] pub struct Payload { + #[modify(trim)] + #[validate( + length( + min = 1, + max = "VALIDATE_ACCESS_TOKEN_TITLE_MAX_LEN", + message = "API key title is either too short or too long", + ), + non_control_character(message = "API key title contains invalid characters") + )] title: String, password: String, } diff --git a/rs/packages/api/src/routes/payment_methods/mod.rs b/rs/packages/api/src/routes/payment_methods/mod.rs index c674f815..024e9cc8 100644 --- a/rs/packages/api/src/routes/payment_methods/mod.rs +++ b/rs/packages/api/src/routes/payment_methods/mod.rs @@ -136,11 +136,13 @@ pub mod get { } pub mod post { + use modify::Modify; use schemars::JsonSchema; + use validator::Validate; use super::*; - #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] + #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema, Modify, Validate)] #[ts(export, export_to = "../../../defs/api/payment-methods/POST/")] #[macros::schema_ts_export] pub struct Payload { diff --git a/rs/packages/api/src/routes/plans/id.rs b/rs/packages/api/src/routes/plans/id.rs index 6101213d..c141e589 100644 --- a/rs/packages/api/src/routes/plans/id.rs +++ b/rs/packages/api/src/routes/plans/id.rs @@ -174,21 +174,22 @@ pub mod patch { use crate::error::ApiError; use super::*; + use constants::validate::*; use db::{ account::{Account, Limit, Limits}, plan::Plan, run_transaction, Model, }; + use modify::Modify; use prex::request::ReadBodyJsonError; use schemars::JsonSchema; use serde_util::DateTime; - use validify::{validify, ValidationErrors, Validify}; + use validator::Validate; #[derive(Debug, Clone)] pub struct Endpoint {} - #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] - #[validify] + #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema, Modify, Validate)] #[ts(export, export_to = "../../../defs/api/plans/[plan]/PATCH/")] #[macros::schema_ts_export] pub struct Payload { @@ -198,22 +199,43 @@ pub mod patch { #[ts(optional)] #[modify(trim)] - #[validate(length(min = 1))] + #[validate( + length( + min = 1, + max = "VALIDATE_PLAN_IDENTIFIER_MAX_LEN", + message = "Identifier is either too short or too long" + ), + non_control_character(message = "Identifier contains invalid characters") + )] identifier: Option, #[ts(optional)] #[modify(trim)] - #[validate(length(min = 1))] + #[validate( + length( + min = 1, + max = "VALIDATE_PLAN_SLUG_MAX_LEN", + message = "Slug is either too short or too long" + ), + non_control_character(message = "Slug contains invalid characters") + )] slug: Option, #[ts(optional)] #[modify(trim)] - #[validate(length(min = 1))] + #[validate( + length( + min = 1, + max = "VALIDATE_PLAN_NAME_MAX_LEN", + message = "Display name is either too short or too long" + ), + non_control_character(message = "Display name contains invalid characters") + )] display_name: Option, #[ts(optional)] #[modify(trim)] - #[validate(length(min = 1))] + #[validate(length(min = 1, message = "Color cannot be empty"))] color: Option, #[ts(optional)] @@ -268,8 +290,6 @@ pub mod patch { PlanNotFound(String), #[error("slug exists")] SlugExists, - #[error("validfy payload: {0}")] - Validify(#[from] ValidationErrors), } impl From for ApiError { @@ -278,7 +298,6 @@ pub mod patch { HandleError::Db(e) => e.into(), HandleError::PlanNotFound(id) => ApiError::PlanNotFound(id), HandleError::SlugExists => ApiError::BadRequestCustom("The slug already exists".into()), - HandleError::Validify(errors) => ApiError::PayloadInvalid(format!("{}", errors)), } } } @@ -307,8 +326,6 @@ pub mod patch { async fn perform(&self, input: Self::Input) -> Result { let Self::Input { plan_id, payload } = input; - let payload = Payload::validify(payload.into())?; - let Payload { display_name, identifier, diff --git a/rs/packages/api/src/routes/plans/mod.rs b/rs/packages/api/src/routes/plans/mod.rs index 915fb140..0f612db1 100644 --- a/rs/packages/api/src/routes/plans/mod.rs +++ b/rs/packages/api/src/routes/plans/mod.rs @@ -115,12 +115,14 @@ pub mod get { pub mod post { use async_trait::async_trait; + use constants::validate::*; use db::Model; use db::{ current_filter_doc, plan::{Plan, PlanLimits}, run_transaction, }; + use modify::Modify; use mongodb::bson::doc; use mongodb::options::FindOneOptions; use prex::{request::ReadBodyJsonError, Request}; @@ -128,7 +130,7 @@ pub mod post { use serde::{Deserialize, Serialize}; use serde_util::DateTime; use ts_rs::TS; - use validify::{validify, ValidationErrors, Validify}; + use validator::Validate; use crate::{ error::ApiError, @@ -136,23 +138,42 @@ pub mod post { request_ext::{self, GetAccessTokenScopeError}, }; - #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] + #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema, Modify, Validate)] #[ts(export, export_to = "../../../defs/api/plans/POST/")] #[macros::schema_ts_export] - #[validify] - #[serde(rename_all = "snake_case")] - #[serde(deny_unknown_fields)] + #[serde(rename_all = "snake_case", deny_unknown_fields)] pub struct Payload { #[modify(trim)] - #[validate(length(min = 1))] + #[validate( + length( + min = 1, + max = "VALIDATE_PLAN_IDENTIFIER_MAX_LEN", + message = "Identifier is either too short or too long" + ), + non_control_character(message = "Identifier contains invalid characters") + )] pub identifier: String, #[modify(trim, lowercase)] - #[validate(length(min = 1))] + #[validate( + length( + min = 1, + max = "VALIDATE_PLAN_SLUG_MAX_LEN", + message = "Slug is either too short or too long" + ), + non_control_character(message = "Slug contains invalid characters") + )] pub slug: String, #[modify(trim)] - #[validate(length(min = 1))] + #[validate( + length( + min = 1, + max = "VALIDATE_PLAN_NAME_MAX_LEN", + message = "Display name is either too short or too long" + ), + non_control_character(message = "Display name contains invalid characters") + )] pub display_name: String, pub is_user_selectable: bool, @@ -161,7 +182,7 @@ pub mod post { pub price: f64, #[modify(trim)] - #[validate(length(min = 1))] + #[validate(length(min = 1, message = "Color cannot be empty"))] pub color: String, pub stations: u64, @@ -203,8 +224,6 @@ pub mod post { Db(#[from] mongodb::error::Error), #[error("slug exists")] SlugExists, - #[error("validify: {0}")] - Validify(#[from] ValidationErrors), } impl From for ApiError { @@ -212,7 +231,6 @@ pub mod post { match e { HandleError::Db(e) => e.into(), HandleError::SlugExists => ApiError::BadRequestCustom("The slug is already taken".into()), - HandleError::Validify(errors) => ApiError::PayloadInvalid(format!("{}", errors)), } } } @@ -241,8 +259,6 @@ pub mod post { async fn perform(&self, input: Input) -> Result { let Input { payload } = input; - let payload = Payload::validify(payload.into())?; - let Payload { ref identifier, ref slug, diff --git a/rs/packages/api/src/routes/stations/files/id.rs b/rs/packages/api/src/routes/stations/files/id.rs index 5d8cda0f..f0cc9290 100644 --- a/rs/packages/api/src/routes/stations/files/id.rs +++ b/rs/packages/api/src/routes/stations/files/id.rs @@ -173,7 +173,7 @@ pub mod stream { None => None, Some(v) => match http_range::HttpRange::parse(v, file.len) { Err(e) => return ApiError::from(e).into_json_response(), - Ok(ranges) => ranges.get(0).copied(), + Ok(ranges) => ranges.first().copied(), }, }; diff --git a/rs/packages/api/src/routes/stations/files/metadata.rs b/rs/packages/api/src/routes/stations/files/metadata.rs index 7508c03a..4f9e1cd2 100644 --- a/rs/packages/api/src/routes/stations/files/metadata.rs +++ b/rs/packages/api/src/routes/stations/files/metadata.rs @@ -11,17 +11,21 @@ use serde::{Deserialize, Serialize}; pub mod put { + use crate::error::ApiError; + use constants::validate::*; use db::run_transaction; + use modify::Modify; use prex::request::ReadBodyJsonError; use schemars::JsonSchema; - use ts_rs::TS; - - use crate::error::ApiError; use serde_util::map_some; + use ts_rs::TS; + use validator::Validate; use super::*; - #[derive(Debug, Default, Serialize, Deserialize, PartialEq, Eq, Clone, TS, JsonSchema)] + #[derive( + Debug, Default, Serialize, Deserialize, PartialEq, Eq, Clone, TS, JsonSchema, Validate, Modify, + )] #[ts( export, export_to = "../../../defs/api/stations/[station]/files/[file]/metadata/PUT/" @@ -35,6 +39,14 @@ pub mod put { deserialize_with = "map_some", skip_serializing_if = "Option::is_none" )] + #[modify(trim)] + #[validate( + length( + max = "VALIDATE_AUDIO_FILE_METADATA_TITLE_MAX_LEN", + message = "Title is too long" + ), + non_control_character(message = "Title contains invalid characters") + )] pub title: Option>, #[ts(optional)] @@ -43,6 +55,14 @@ pub mod put { deserialize_with = "map_some", skip_serializing_if = "Option::is_none" )] + #[modify(trim)] + #[validate( + length( + max = "VALIDATE_AUDIO_FILE_METADATA_ARTIST_MAX_LEN", + message = "Artist is too long" + ), + non_control_character(message = "Artist contains invalid characters") + )] pub artist: Option>, #[ts(optional)] @@ -51,6 +71,14 @@ pub mod put { deserialize_with = "map_some", skip_serializing_if = "Option::is_none" )] + #[modify(trim)] + #[validate( + length( + max = "VALIDATE_AUDIO_FILE_METADATA_ALBUM_MAX_LEN", + message = "Album is too long" + ), + non_control_character(message = "Album contains invalid characters") + )] pub album: Option>, #[ts(optional)] @@ -59,6 +87,14 @@ pub mod put { deserialize_with = "map_some", skip_serializing_if = "Option::is_none" )] + #[modify(trim)] + #[validate( + length( + max = "VALIDATE_AUDIO_FILE_METADATA_ALBUM_ARTIST_MAX_LEN", + message = "Album artist is too long" + ), + non_control_character(message = "Album artist contains invalid characters") + )] pub album_artist: Option>, #[ts(optional)] @@ -67,6 +103,14 @@ pub mod put { deserialize_with = "map_some", skip_serializing_if = "Option::is_none" )] + #[modify(trim)] + #[validate( + length( + max = "VALIDATE_AUDIO_FILE_METADATA_GENRE_MAX_LEN", + message = "Genre is too long" + ), + non_control_character(message = "Genre contains invalid characters") + )] pub genre: Option>, #[ts(optional)] @@ -83,6 +127,14 @@ pub mod put { deserialize_with = "map_some", skip_serializing_if = "Option::is_none" )] + #[modify(trim)] + #[validate( + length( + max = "VALIDATE_AUDIO_FILE_METADATA_COMMENT_MAX_LEN", + message = "Comment is too long" + ), + non_control_character(message = "Comment contains invalid characters") + )] pub comment: Option>, #[ts(optional)] diff --git a/rs/packages/api/src/routes/stations/files/order.rs b/rs/packages/api/src/routes/stations/files/order.rs index b6d0acc1..511cfe68 100644 --- a/rs/packages/api/src/routes/stations/files/order.rs +++ b/rs/packages/api/src/routes/stations/files/order.rs @@ -45,12 +45,14 @@ pub mod swap { pub mod post { + use modify::Modify; use schemars::JsonSchema; use serde_util::empty_struct::EmptyStruct; + use validator::Validate; use super::*; - #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] + #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema, Modify, Validate)] #[ts( export, export_to = "../../../defs/api/stations/[station]/files/[file]/order/swap/POST/" @@ -294,7 +296,9 @@ pub mod move_before { pub mod post { use db::audio_file::OrderDocument; + use modify::Modify; use schemars::JsonSchema; + use validator::Validate; use super::*; #[derive(Debug, Clone)] @@ -318,7 +322,7 @@ pub mod move_before { order: f64, } - #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] + #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema, Modify, Validate)] #[ts( export, export_to = "../../../defs/api/stations/[station]/files/[file]/order/move-before/POST/" @@ -401,7 +405,9 @@ pub mod move_after { use super::*; pub mod post { + use modify::Modify; use schemars::JsonSchema; + use validator::Validate; use super::*; #[derive(Debug, Clone)] @@ -425,7 +431,7 @@ pub mod move_after { order: f64, } - #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] + #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema, Modify, Validate)] #[ts( export, export_to = "../../../defs/api/stations/[station]/files/[file]/order/move-after/POST/" diff --git a/rs/packages/api/src/routes/stations/id.rs b/rs/packages/api/src/routes/stations/id.rs index 5b1c648d..516a63e4 100644 --- a/rs/packages/api/src/routes/stations/id.rs +++ b/rs/packages/api/src/routes/stations/id.rs @@ -266,9 +266,10 @@ pub mod patch { }; use hyper::{http::HeaderValue, Body}; use media::MediaSessionMap; + use modify::Modify; use prex::request::ReadBodyJsonError; use schemars::JsonSchema; - use validify::{ValidationErrors, Validify}; + use validator::Validate; #[derive(Debug, Clone)] pub struct Endpoint { @@ -276,10 +277,14 @@ pub mod patch { pub media_sessions: MediaSessionMap, } - #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] + #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema, Modify, Validate)] #[ts(export, export_to = "../../../defs/api/stations/[station]/PATCH/")] #[macros::schema_ts_export] - pub struct Payload(pub StationPatch); + pub struct Payload { + #[validate] + #[serde(flatten)] + pub patch: StationPatch, + } #[derive(Debug, Clone)] pub struct Input { @@ -321,8 +326,6 @@ pub mod patch { StationNotFound(String), #[error("picture not found {0}")] PictureNotFound(String), - #[error("validation: {0}")] - Validation(#[from] ValidationErrors), } impl From for ApiError { @@ -331,7 +334,6 @@ pub mod patch { HandleError::Db(e) => Self::from(e), HandleError::Patch(e) => Self::from(e), HandleError::StationNotFound(id) => Self::StationNotFound(id), - HandleError::Validation(e) => Self::PayloadInvalid(format!("{e}")), HandleError::PictureNotFound(id) => { Self::PayloadInvalid(format!("Picture with id {id} not found")) } @@ -367,7 +369,7 @@ pub mod patch { async fn perform(&self, input: Self::Input) -> Result { let Self::Input { - payload: Payload(patch), + payload: Payload { patch }, access_token_scope, access_token_header, station, @@ -375,8 +377,6 @@ pub mod patch { let id = station.id; - let patch: StationPatch = Validify::validify(patch.into())?; - let mut prev_external_relay_redirect: bool; let mut prev_external_relay_url: Option; diff --git a/rs/packages/api/src/routes/stations/mod.rs b/rs/packages/api/src/routes/stations/mod.rs index 154deacf..4940bc5e 100644 --- a/rs/packages/api/src/routes/stations/mod.rs +++ b/rs/packages/api/src/routes/stations/mod.rs @@ -162,19 +162,19 @@ pub mod post { use db::station_picture::StationPicture; use geoip::CountryCode; use lang::LangCode; + use modify::Modify; use schemars::JsonSchema; use serde_util::DateTime; use ts_rs::TS; use validate::url::patterns::*; - use validify::{validify, ValidationErrors, Validify}; + use validator::Validate; use super::*; - #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] + #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema, Modify, Validate)] #[ts(export, export_to = "../../../defs/api/stations/POST/")] #[macros::schema_ts_export] #[serde(rename_all = "snake_case", deny_unknown_fields)] - #[validify] pub struct Payload { pub account_id: String, @@ -477,8 +477,6 @@ pub mod post { Token(#[from] GetAccessTokenScopeError), #[error("account not found ({0})")] AccountNotFound(String), - #[error("validation error: {0}")] - ValidationError(#[from] ValidationErrors), #[error("Invalid name (slug)")] InvalidNameSlug, #[error("Picture with id {0} not found")] @@ -493,7 +491,6 @@ pub mod post { HandleError::Db(e) => ApiError::from(e), HandleError::Token(e) => ApiError::from(e), HandleError::AccountNotFound(id) => ApiError::AccountNotFound(id), - HandleError::ValidationError(e) => ApiError::PayloadInvalid(format!("{e}")), HandleError::PictureNotFound(id) => { ApiError::PayloadInvalid(format!("Picture with id {id} not found")) } @@ -530,9 +527,6 @@ pub mod post { payload, } = input; - //use validify::Validify; - //let payload = Validify::validify(payload.into())?; - let Payload { account_id, picture_id, @@ -640,9 +634,6 @@ pub mod post { deleted_at: None, }; - // we validate directly the station and not the payload - let station: Station = Validify::validify(station.into())?; - run_transaction!(session => { { let filter = doc!{ StationPicture::KEY_ACCOUNT_ID: &station.account_id, StationPicture::KEY_ID: &station.picture_id }; diff --git a/rs/packages/api/src/routes/stations/transfer.rs b/rs/packages/api/src/routes/stations/transfer.rs index d84eb385..ef2babfd 100644 --- a/rs/packages/api/src/routes/stations/transfer.rs +++ b/rs/packages/api/src/routes/stations/transfer.rs @@ -16,14 +16,16 @@ use ts_rs::TS; pub mod post { use db::station::PublicStation; + use modify::Modify; use schemars::JsonSchema; + use validator::Validate; use super::*; #[derive(Debug, Clone)] pub struct Endpoint {} - #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] + #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema, Modify, Validate)] #[ts( export, export_to = "../../../defs/api/stations/[station]/transfer/POST/" diff --git a/rs/packages/api/src/routes/users/change_password.rs b/rs/packages/api/src/routes/users/change_password.rs index 278f301b..7a649c1c 100644 --- a/rs/packages/api/src/routes/users/change_password.rs +++ b/rs/packages/api/src/routes/users/change_password.rs @@ -1,8 +1,10 @@ pub mod post { + use modify::Modify; use mongodb::bson::doc; use schemars::JsonSchema; use std::net::IpAddr; + use validator::Validate; use crate::{ error::ApiError, @@ -13,6 +15,7 @@ pub mod post { use db::{user::User, Model}; + use constants::validate::*; use prex::{request::ReadBodyJsonError, Request}; use serde::{Deserialize, Serialize}; use serde_util::empty_struct::EmptyStruct; @@ -21,7 +24,7 @@ pub mod post { #[derive(Debug, Clone)] pub struct Endpoint {} - #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] + #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema, Modify, Validate)] #[ts( export, export_to = "../../../defs/api/users/[user]/change-password/POST/" @@ -29,6 +32,12 @@ pub mod post { #[macros::schema_ts_export] pub struct Payload { pub current_password: String, + + #[validate(length( + min = "VALIDATE_USER_PASSWORD_MIN_LEN", + max = "VALIDATE_USER_PASSWORD_MAX_LEN", + message = "New password is either too short or too long", + ))] pub new_password: String, } diff --git a/rs/packages/api/src/routes/users/id.rs b/rs/packages/api/src/routes/users/id.rs index 7843e323..65e27baa 100644 --- a/rs/packages/api/src/routes/users/id.rs +++ b/rs/packages/api/src/routes/users/id.rs @@ -61,32 +61,74 @@ pub mod patch { use crate::error::ApiError; use super::*; + use constants::validate::*; use db::{fetch_and_patch, run_transaction}; + use modify::Modify; use prex::request::ReadBodyJsonError; use schemars::JsonSchema; use serde_util::DateTime; use ts_rs::TS; + use validator::Validate; #[derive(Debug, Clone)] pub struct Endpoint {} - #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] + #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema, Modify, Validate)] #[ts(export, export_to = "../../../defs/api/users/[user]/PATCH/")] #[macros::schema_ts_export] pub struct Payload { + #[modify(trim)] + #[validate( + length( + min = 1, + max = "VALIDATE_USER_FIRST_NAME_MAX_LEN", + message = "First name is either too long or too short" + ), + non_control_character(message = "First name contains invalid characters") + )] first_name: Option, + + #[modify(trim)] + #[validate( + length( + min = 1, + max = "VALIDATE_USER_LAST_NAME_MAX_LEN", + message = "Last name is either too long or too short" + ), + non_control_character(message = "Last name contains invalid characters") + )] last_name: Option, + #[serde( default, skip_serializing_if = "Option::is_none", deserialize_with = "serde_util::map_some" )] + #[modify(trim)] + #[validate( + phone(message = "Phone is invalid"), + length( + min = 1, + max = "VALIDATE_USER_PHONE_MAX_LEN", + message = "Phone is either too long or too short" + ) + )] phone: Option>, + #[serde( default, skip_serializing_if = "Option::is_none", deserialize_with = "serde_util::map_some" )] + #[modify(trim)] + #[validate( + length( + min = 1, + max = "VALIDATE_USER_LANGUAGE_MAX_LEN", + message = "Language is either too long or too short" + ), + non_control_character(message = "Language contains invalid characters") + )] language: Option>, } diff --git a/rs/packages/api/src/routes/users/mod.rs b/rs/packages/api/src/routes/users/mod.rs index 33b7d6cf..5ad43e01 100644 --- a/rs/packages/api/src/routes/users/mod.rs +++ b/rs/packages/api/src/routes/users/mod.rs @@ -118,23 +118,82 @@ pub mod get { pub mod post { + use constants::validate::*; use db::run_transaction; + use modify::Modify; use schemars::JsonSchema; use ts_rs::TS; + use validator::Validate; use super::*; - #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] + #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema, Modify, Validate)] #[ts(export, export_to = "../../../defs/api/users/POST/")] #[macros::schema_ts_export] #[serde(deny_unknown_fields)] pub struct Payload { + #[modify(trim)] + #[validate( + email(message = "Email is invalid"), + length( + min = 1, + max = "VALIDATE_USER_EMAIL_MAX_LEN", + message = "Email is either too short or too long" + ), + non_control_character(message = "Email contains invalid characters") + )] email: String, + + #[modify(trim)] + #[validate( + phone(message = "Phone is invalid"), + length( + min = 1, + max = "VALIDATE_USER_PHONE_MAX_LEN", + message = "Phone is either too short or too long" + ) + )] phone: Option, + + #[validate(length( + min = "VALIDATE_USER_PASSWORD_MIN_LEN", + max = "VALIDATE_USER_PASSWORD_MAX_LEN", + message = "Password is either too short or too long" + ))] password: String, + + #[modify(trim)] + #[validate( + length( + min = 1, + max = "VALIDATE_USER_FIRST_NAME_MAX_LEN", + message = "First name is either too short or too long" + ), + non_control_character(message = "First name contains invalid characters") + )] first_name: String, + + #[modify(trim)] + #[validate( + length( + min = 1, + max = "VALIDATE_USER_LAST_NAME_MAX_LEN", + message = "Last name is either too short or too long" + ), + non_control_character(message = "Last name contains invalid characters") + )] last_name: String, + #[serde(skip_serializing_if = "Option::is_none")] + #[modify(trim)] + #[validate( + length( + min = 1, + max = "VALIDATE_USER_LANGUAGE_MAX_LEN", + message = "Language is either too short or too long" + ), + non_control_character(message = "Language contains invalid characters") + )] language: Option, #[serde(skip_serializing_if = "Option::is_none")] diff --git a/rs/packages/db/Cargo.toml b/rs/packages/db/Cargo.toml index 17b99b03..542a0bf9 100644 --- a/rs/packages/db/Cargo.toml +++ b/rs/packages/db/Cargo.toml @@ -47,7 +47,8 @@ const-str = { version = "0.5.3", features = ["all"] } rand = "0.8.5" static_init = "1.0.3" parking_lot = "0.12.1" -validify = "0.1.0" +modify = { path = "../modify" } +validator = { version = "0.16.1", features = ["derive"] } ril = { version = "0.9.0", features = ["all"] } lazy-regex = "2.5.0" geoip = { version = "0.1.0", path = "../geoip" } diff --git a/rs/packages/db/src/models/account/mod.rs b/rs/packages/db/src/models/account/mod.rs index d90c1fd3..d480ebde 100644 --- a/rs/packages/db/src/models/account/mod.rs +++ b/rs/packages/db/src/models/account/mod.rs @@ -11,6 +11,9 @@ use serde_util::DateTime; use std::collections::HashMap; use ts_rs::TS; +use modify::Modify; +use validator::Validate; + crate::register!(Account); #[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] @@ -59,11 +62,10 @@ pub enum PublicAccount { User(UserPublicAccount), } -#[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema, Modify, Validate)] #[ts(export, export_to = "../../../defs/ops/")] #[serde(rename_all = "snake_case")] #[serde(deny_unknown_fields)] -#[validify::validify] pub struct AccountPatch { #[serde(skip_serializing_if = "Option::is_none")] #[modify(trim)] diff --git a/rs/packages/db/src/models/admin/mod.rs b/rs/packages/db/src/models/admin/mod.rs index 7be99b2e..8d7efea1 100644 --- a/rs/packages/db/src/models/admin/mod.rs +++ b/rs/packages/db/src/models/admin/mod.rs @@ -1,10 +1,13 @@ use crate::Model; use crate::{error::ApplyPatchError, metadata::Metadata}; +use constants::validate::*; +use modify::Modify; use mongodb::{bson::doc, options::IndexOptions, IndexModel}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_util::DateTime; use ts_rs::TS; +use validator::Validate; crate::register!(Admin); @@ -55,13 +58,31 @@ impl Admin { } } -#[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema, Modify, Validate)] #[ts(export, export_to = "../../../defs/ops/")] #[serde(rename_all = "snake_case", deny_unknown_fields)] pub struct AdminPatch { #[serde(skip_serializing_if = "Option::is_none")] + #[modify(trim)] + #[validate( + length( + min = 1, + max = "VALIDATE_ADMIN_FIRST_NAME_MAX_LEN", + message = "First name is either too short or too long", + ), + non_control_character(message = "Fist name cannot contain control characters") + )] pub first_name: Option, #[serde(skip_serializing_if = "Option::is_none")] + #[modify(trim)] + #[validate( + length( + min = 1, + max = "VALIDATE_ADMIN_LAST_NAME_MAX_LEN", + message = "Last name is either too short or too long", + ), + non_control_character(message = "Last name cannot contain control characters") + )] pub last_name: Option, #[serde(skip_serializing_if = "Option::is_none")] pub system_metadata: Option, diff --git a/rs/packages/db/src/models/station/mod.rs b/rs/packages/db/src/models/station/mod.rs index b7153026..5171d379 100644 --- a/rs/packages/db/src/models/station/mod.rs +++ b/rs/packages/db/src/models/station/mod.rs @@ -6,6 +6,7 @@ use constants::validate::*; use drop_tracer::Token; use geoip::CountryCode; use lang::LangCode; +use modify::Modify; use mongodb::bson::{doc, Bson, SerializerOptions}; use mongodb::options::{FindOneAndUpdateOptions, ReturnDocument}; use mongodb::{ClientSession, IndexModel}; @@ -15,14 +16,13 @@ use serde_util::map_some; use serde_util::DateTime; use ts_rs::TS; use validate::url::patterns::*; -use validify::validify; +use validator::Validate; crate::register!(Station); -#[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema, Modify, Validate)] #[ts(export, export_to = "../../../defs/db/")] #[serde(rename_all = "snake_case")] -#[validify] #[macros::keys] pub struct Station { #[serde(rename = "_id")] @@ -403,10 +403,9 @@ pub struct UserPublicStation { pub deleted_at: Option, } -#[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema, Modify, Validate)] #[ts(export, export_to = "../../../defs/")] #[serde(rename_all = "snake_case")] -#[validify] #[macros::keys] pub struct StationFrequency { kind: StationFrequencyKind, @@ -438,11 +437,10 @@ pub enum PublicStation { User(UserPublicStation), } -#[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema)] +#[derive(Debug, Clone, Serialize, Deserialize, TS, JsonSchema, Modify, Validate)] #[ts(export, export_to = "../../../defs/ops/")] #[serde(rename_all = "snake_case")] #[serde(deny_unknown_fields)] -#[validify] pub struct StationPatch { #[ts(optional)] #[serde(skip_serializing_if = "Option::is_none")] diff --git a/rs/packages/db/src/models/user/mod.rs b/rs/packages/db/src/models/user/mod.rs index d3c9bafb..b38db07e 100644 --- a/rs/packages/db/src/models/user/mod.rs +++ b/rs/packages/db/src/models/user/mod.rs @@ -1,5 +1,6 @@ use crate::metadata::Metadata; use crate::{current_filter_doc, deleted_filter_doc, Model, PublicScope}; +use modify::Modify; use mongodb::error::Result as MongoResult; use mongodb::ClientSession; use mongodb::{bson::doc, options::IndexOptions, IndexModel}; @@ -7,13 +8,13 @@ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use serde_util::DateTime; use ts_rs::TS; +use validator::Validate; crate::register!(User); -#[derive(Debug, Clone, Serialize, Deserialize, TS)] +#[derive(Debug, Clone, Serialize, Deserialize, TS, Modify, Validate)] #[ts(export, export_to = "../../../defs/db/")] #[serde(rename_all = "snake_case")] -#[validify::validify] #[macros::keys] pub struct User { #[serde(rename = "_id")] diff --git a/rs/packages/modify/Cargo.toml b/rs/packages/modify/Cargo.toml new file mode 100644 index 00000000..0d124994 --- /dev/null +++ b/rs/packages/modify/Cargo.toml @@ -0,0 +1,30 @@ +[package] +authors = [ + "Vincent Prouillet ", +] +description = "Provides struct validation and modification functionality through the use of derive macros" +edition = "2021" +exclude = ["derive_tests/"] +homepage = "https://github.com/biblius/modify" +keywords = ["modify", "payload", "validate", "modify"] +license = "MIT" +name = "modify" +readme = "../README.md" +repository = "https://github.com/biblius/modify" +version = "1.3.0" + + +[dependencies] +card-validate = { version = "2.3" } +chrono = "0.4.24" +idna = "0.5" +indexmap = { version = "2", features = ["serde"] } +lazy_static = "1.4.0" +phonenumber = "0.3.2" +regex = "1.7.3" +serde = { version = "1.0.152", features = ["derive"] } +serde_json = "1" +unic-ucd-common = { version = "0.9" } +url = "2.3.1" +modify_derive = { path = "../modify_derive" } diff --git a/rs/packages/modify/src/lib.rs b/rs/packages/modify/src/lib.rs new file mode 100644 index 00000000..88e97c00 --- /dev/null +++ b/rs/packages/modify/src/lib.rs @@ -0,0 +1,10 @@ +mod traits; + +pub use modify_derive::Modify; + +/// Modifies the struct based on the provided `modify` parameters. Automatically implemented when deriving modify. +/// See the [repository](https://github.com/biblius/modify) for a full list of possible modifiers. +pub trait Modify { + /// Apply the provided modifiers to self + fn modify(&mut self); +} diff --git a/rs/packages/modify/src/traits.rs b/rs/packages/modify/src/traits.rs new file mode 100644 index 00000000..813952e3 --- /dev/null +++ b/rs/packages/modify/src/traits.rs @@ -0,0 +1,214 @@ +use indexmap::{IndexMap, IndexSet}; +use std::borrow::Cow; +use std::collections::{BTreeMap, BTreeSet, HashMap, HashSet}; +use std::hash::Hash; + +/// Trait to implement if one wants to make the `length` validator +/// work for more types +pub trait HasLen { + fn length(&self) -> u64; +} + +impl HasLen for String { + fn length(&self) -> u64 { + self.chars().count() as u64 + } +} + +impl<'a> HasLen for &'a String { + fn length(&self) -> u64 { + self.chars().count() as u64 + } +} + +impl<'a> HasLen for &'a str { + fn length(&self) -> u64 { + self.chars().count() as u64 + } +} + +impl<'a> HasLen for Cow<'a, str> { + fn length(&self) -> u64 { + self.len() as u64 + } +} + +impl HasLen for Vec { + fn length(&self) -> u64 { + self.len() as u64 + } +} + +impl<'a, T> HasLen for &'a Vec { + fn length(&self) -> u64 { + self.len() as u64 + } +} + +impl HasLen for &[T] { + fn length(&self) -> u64 { + self.len() as u64 + } +} + +impl HasLen for [T; N] { + fn length(&self) -> u64 { + N as u64 + } +} + +impl HasLen for &[T; N] { + fn length(&self) -> u64 { + N as u64 + } +} + +impl<'a, K, V, S> HasLen for &'a HashMap { + fn length(&self) -> u64 { + self.len() as u64 + } +} + +impl HasLen for HashMap { + fn length(&self) -> u64 { + self.len() as u64 + } +} + +impl<'a, T, S> HasLen for &'a HashSet { + fn length(&self) -> u64 { + self.len() as u64 + } +} + +impl HasLen for HashSet { + fn length(&self) -> u64 { + self.len() as u64 + } +} + +impl<'a, K, V> HasLen for &'a BTreeMap { + fn length(&self) -> u64 { + self.len() as u64 + } +} + +impl HasLen for BTreeMap { + fn length(&self) -> u64 { + self.len() as u64 + } +} + +impl<'a, T> HasLen for &'a BTreeSet { + fn length(&self) -> u64 { + self.len() as u64 + } +} + +impl HasLen for BTreeSet { + fn length(&self) -> u64 { + self.len() as u64 + } +} + +impl<'a, K, V> HasLen for &'a IndexMap { + fn length(&self) -> u64 { + self.len() as u64 + } +} + +impl HasLen for IndexMap { + fn length(&self) -> u64 { + self.len() as u64 + } +} + +impl<'a, T> HasLen for &'a IndexSet { + fn length(&self) -> u64 { + self.len() as u64 + } +} + +impl HasLen for IndexSet { + fn length(&self) -> u64 { + self.len() as u64 + } +} + +/// Trait to implement if one wants to make the `contains` validator +/// work for more types +pub trait Contains { + type Needle<'a> + where + Self: 'a; + #[must_use] + fn has_element(&self, needle: Self::Needle<'_>) -> bool; +} + +impl Contains for Vec +where + T: PartialEq, +{ + type Needle<'a> = &'a T where Self: 'a; + fn has_element(&self, needle: Self::Needle<'_>) -> bool { + self.iter().any(|a| a == needle) + } +} + +impl Contains for &Vec +where + T: PartialEq, +{ + type Needle<'a> = &'a T where Self: 'a; + fn has_element<'a>(&'a self, needle: Self::Needle<'a>) -> bool { + self.iter().any(|a| a == needle) + } +} + +impl Contains for HashMap +where + T: PartialEq + Eq + Hash, +{ + type Needle<'a> = &'a T where Self: 'a; + fn has_element<'a>(&'a self, needle: Self::Needle<'a>) -> bool { + self.contains_key(needle) + } +} + +impl Contains for &HashMap +where + T: PartialEq + Eq + Hash, +{ + type Needle<'a> = &'a T where Self: 'a; + fn has_element<'a>(&'a self, needle: Self::Needle<'a>) -> bool { + self.contains_key(needle) + } +} + +impl Contains for String { + type Needle<'a> = &'a str; + fn has_element(&self, needle: &str) -> bool { + self.contains(needle) + } +} + +impl Contains for &String { + type Needle<'a> = &'a str where Self: 'a; + fn has_element(&self, needle: &str) -> bool { + self.contains(needle) + } +} + +impl Contains for &str { + type Needle<'a> = &'a str where Self: 'a; + fn has_element(&self, needle: &str) -> bool { + self.contains(needle) + } +} + +impl Contains for Cow<'_, str> { + type Needle<'a> = &'a str where Self: 'a; + fn has_element(&self, needle: &str) -> bool { + self.contains(needle) + } +} diff --git a/rs/packages/modify_derive/Cargo.toml b/rs/packages/modify_derive/Cargo.toml new file mode 100644 index 00000000..5c77f30b --- /dev/null +++ b/rs/packages/modify_derive/Cargo.toml @@ -0,0 +1,26 @@ +[package] +authors = [ + "Vincent Prouillet ", +] +description = "Derive macros for the modify crate" +edition = "2021" +homepage = "https://github.com/biblius/modify" +keywords = ["modify", "payload", "validate", "modify"] +license = "MIT" +name = "modify_derive" +readme = "../README.md" +repository = "https://github.com/biblius/modify" +version = "1.3.0" + +[lib] +proc-macro = true + +[dependencies] +chrono = "0.4.24" +lazy_static = "1.4.0" +proc-macro-error = "1.0.4" +proc-macro2 = "1.0.56" +quote = "1.0.26" +regex = "1.5.5" +syn = { version = "2.0.15", features = ["extra-traits", "full"] } diff --git a/rs/packages/modify_derive/src/fields.rs b/rs/packages/modify_derive/src/fields.rs new file mode 100644 index 00000000..ba0f4f87 --- /dev/null +++ b/rs/packages/modify_derive/src/fields.rs @@ -0,0 +1,235 @@ +use crate::modify::{modifier::Modifier, r#impl::collect_modifiers}; +use proc_macro_error::abort; +use quote::quote; +use syn::spanned::Spanned; + +/// Holds the combined validations and modifiers for one field +#[derive(Debug)] +pub struct FieldInfo { + /// The original field + pub field: syn::Field, + + /// The field's original name if annotated with `serde(rename)`` + pub name: String, + + /// Modifier annotations + pub modifiers: Vec, +} + +impl FieldInfo { + pub fn new(field: syn::Field, name: String, modifiers: Vec) -> Self { + FieldInfo { + field, + name, + modifiers, + } + } + + /// Used by both the `Validate` and `modify` implementations. Validate ignores the modifiers. + pub fn collect(input: &syn::DeriveInput) -> Vec { + let syn::Data::Struct(syn::DataStruct { ref fields, .. }) = input.data else { + abort!( + input.span(), + "#[derive(Validate/modify)] can only be used on structs with named fields" + ) + }; + + fields + .into_iter() + .map(|field| { + let field_ident = field + .ident + .as_ref() + .expect("Found unnamed field") + .to_string(); + + let modifiers = collect_field_attributes(field); + + Self::new(field.clone(), field_ident, modifiers) + }) + .collect::>() + } + + /// Returns the modification tokens as the first element and any nested validifes as the second. + pub fn quote_validifes(&self) -> (Vec, Vec) { + let mut nested_validifies = vec![]; + let mut quoted_modifications = vec![]; + + for modifier in self.modifiers.iter() { + let (tokens, nested) = modifier.to_modify_tokens(self); + quoted_modifications.push(tokens); + if let Some(nested) = nested { + nested_validifies.push(nested); + } + } + + (quoted_modifications, nested_validifies) + } + + /// Returns `self.#ident`, unless the field is an option in which case it just + /// returns an `#ident` as we always do a `if let` check on Option fields + pub fn quote_modifier_param(&self) -> proc_macro2::TokenStream { + let ident = &self.field.ident; + + if self.is_reference() { + abort!( + ident.span(), + "Fields containing modifiers must contain owned data" + ) + } + + if self.is_option() { + quote!(#ident) + } else { + quote!(self.#ident) + } + } + + pub fn wrap_modifier_if_option( + &self, + tokens: proc_macro2::TokenStream, + ) -> proc_macro2::TokenStream { + let field_ident = &self.field.ident; + + if self.is_option() { + let this = self.option_self_tokens_modifications(); + return quote!( + if let #this = self.#field_ident.as_mut() { + #tokens + } + ); + } + + tokens + } + + /// Wrap the quoted output of a modification in a for loop if + /// the field type is a collection. + pub fn wrap_modifier_if_collection( + &self, + param: proc_macro2::TokenStream, + tokens: proc_macro2::TokenStream, + modifier: &Modifier, + ) -> proc_macro2::TokenStream { + if !self.is_list() { + return tokens; + } + + let modified = match modifier { + Modifier::Trim => quote!(el.trim().to_string()), + Modifier::Uppercase => quote!(el.to_uppercase()), + Modifier::Lowercase => quote!(el.to_lowercase()), + Modifier::Capitalize => { + quote!(::std::format!("{}{}", &el[0..1].to_uppercase(), &el[1..])) + } + _ => unreachable!("modifier is never wrapped"), + }; + + quote!( + for el in #param.iter_mut() { + *el = #modified + } + ) + } + + /// Returns true if the field is an option. + pub fn is_option(&self) -> bool { + let syn::Type::Path(ref p) = self.field.ty else { + return false; + }; + + p.path + .segments + .last() + .is_some_and(|seg| seg.ident == "Option") + } + + /// Returns true if the field is &'_ T, or Option<&'_ T>. + pub fn is_reference(&self) -> bool { + is_reference(&self.field.ty) + } + + pub fn is_list(&self) -> bool { + is_list(&self.field.ty) + } + + fn option_self_tokens_modifications(&self) -> proc_macro2::TokenStream { + let ident = &self.field.ident; + let mut tokens = quote!(#ident); + let mut ty = &self.field.ty; + + while let Some(typ) = try_extract_option(ty) { + tokens = quote!(Some(#tokens)); + ty = typ; + } + tokens + } +} + +fn is_reference(ty: &syn::Type) -> bool { + // Strip any `Option`s + if let Some(ty) = try_extract_option(ty) { + return is_reference(ty); + } + + matches!(ty, syn::Type::Reference(_)) +} + +fn is_list(ty: &syn::Type) -> bool { + if let Some(ty) = try_extract_option(ty) { + return is_list(ty); + } + + // We consider arrays lists + if let syn::Type::Array(_) = ty { + return true; + } + + // If it's not a path, it's not a list + let syn::Type::Path(p) = ty else { + return false; + }; + + // Always check the last arg such as in `std::vec::Vec` + let Some(seg) = p.path.segments.last() else { + return false; + }; + + seg.ident == "Vec" || seg.ident == "HashSet" || seg.ident == "BTreeSet" || seg.ident == "IndexSet" +} + +fn try_extract_option(ty: &syn::Type) -> Option<&syn::Type> { + let syn::Type::Path(p) = ty else { + return None; + }; + + // Always check the last arg such as in `std::vec::Vec` + let seg = p.path.segments.last()?; + + if &seg.ident != "Option" { + return None; + } + + let syn::PathArguments::AngleBracketed(ref ab) = seg.arguments else { + return None; + }; + + let Some(arg) = ab.args.last() else { + return None; + }; + + match arg { + syn::GenericArgument::Type(ty) => Some(ty), + _ => None, + } +} + +/// Find everything we need to know about a field: its real name if it's changed from the deserialization +/// and the list of validators and modifiers to run on it +fn collect_field_attributes(field: &syn::Field) -> Vec { + let mut modifiers = vec![]; + + collect_modifiers(&mut modifiers, field); + + modifiers +} diff --git a/rs/packages/modify_derive/src/lib.rs b/rs/packages/modify_derive/src/lib.rs new file mode 100644 index 00000000..86288d6d --- /dev/null +++ b/rs/packages/modify_derive/src/lib.rs @@ -0,0 +1,11 @@ +use proc_macro_error::proc_macro_error; +mod fields; +mod modify; +mod tokens; + +#[proc_macro_derive(Modify, attributes(modify, nested))] +#[proc_macro_error] +pub fn derive_modify(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let ast = syn::parse(input).unwrap(); + modify::r#impl::impl_modify(&ast).into() +} diff --git a/rs/packages/modify_derive/src/modify/impl.rs b/rs/packages/modify_derive/src/modify/impl.rs new file mode 100644 index 00000000..9947f8e5 --- /dev/null +++ b/rs/packages/modify_derive/src/modify/impl.rs @@ -0,0 +1,81 @@ +use super::modifier::Modifier; +use crate::fields::FieldInfo; +use crate::tokens::quote_field_modifiers; +use proc_macro_error::abort; +use quote::quote; +use syn::parenthesized; + +const TRIM_MODIFIER: &str = "trim"; +const CUSTOM_MODIFIER: &str = "custom"; +const UPPERCASE_MODIFIER: &str = "uppercase"; +const LOWERCASE_MODIFIER: &str = "lowercase"; +const CAPITALIZE_MODIFIER: &str = "capitalize"; +const MODIFY: &str = "modify"; +const NESTED: &str = "nested"; + +/// Impl entry point +pub fn impl_modify(input: &syn::DeriveInput) -> proc_macro2::TokenStream { + let ident = &input.ident; + + let field_info = FieldInfo::collect(input); + + let (modifiers, _nested_validifies) = quote_field_modifiers(field_info); + + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + + quote!( + impl #impl_generics ::modify::Modify for #ident #ty_generics #where_clause { + fn modify(&mut self) { + #(#modifiers)* + } + } + ) +} + +pub fn collect_modifiers(modifiers: &mut Vec, field: &syn::Field) { + for attr in &field.attrs { + // Nest validified fields + if attr.path().is_ident(NESTED) { + modifiers.push(Modifier::Nested); + continue; + } + + if !attr.path().is_ident(MODIFY) { + continue; + } + + attr + .parse_nested_meta(|meta| { + if meta.path.is_ident(CUSTOM_MODIFIER) { + let content; + parenthesized!(content in meta.input); + let path: syn::Path = content.parse()?; + modifiers.push(Modifier::Custom { function: path }); + return Ok(()); + } + + if meta.path.is_ident(TRIM_MODIFIER) { + modifiers.push(Modifier::Trim); + return Ok(()); + } + + if meta.path.is_ident(LOWERCASE_MODIFIER) { + modifiers.push(Modifier::Lowercase); + return Ok(()); + } + + if meta.path.is_ident(UPPERCASE_MODIFIER) { + modifiers.push(Modifier::Uppercase); + return Ok(()); + } + + if meta.path.is_ident(CAPITALIZE_MODIFIER) { + modifiers.push(Modifier::Capitalize); + return Ok(()); + } + + Err(meta.error("Unrecognized modify parameter")) + }) + .unwrap_or_else(|e| abort!(e.span(), e)); + } +} diff --git a/rs/packages/modify_derive/src/modify/mod.rs b/rs/packages/modify_derive/src/modify/mod.rs new file mode 100644 index 00000000..5a9280d4 --- /dev/null +++ b/rs/packages/modify_derive/src/modify/mod.rs @@ -0,0 +1,2 @@ +pub mod r#impl; +pub mod modifier; diff --git a/rs/packages/modify_derive/src/modify/modifier.rs b/rs/packages/modify_derive/src/modify/modifier.rs new file mode 100644 index 00000000..67a2bf24 --- /dev/null +++ b/rs/packages/modify_derive/src/modify/modifier.rs @@ -0,0 +1,148 @@ +use crate::fields::FieldInfo; +use quote::quote; + +#[derive(Debug, PartialEq, Eq)] +pub enum Modifier { + Trim, + Uppercase, + Lowercase, + Capitalize, + Custom { function: syn::Path }, + Nested, +} + +impl Modifier { + /// Returns direct modification tokens as the first element and any nested validify tokens as the second element. + /// Necessary because we need both in case a nested validify occurs. In that case, the first element will have the + /// necessary modification tokens for nested elements in the `Modify` impl while the second will have the tokens + /// for the `Validify` impl. + pub fn to_modify_tokens( + &self, + field_info: &FieldInfo, + ) -> (proc_macro2::TokenStream, Option) { + let param = field_info.quote_modifier_param(); + match self { + Modifier::Trim => { + let tokens = if field_info.is_option() { + quote!( + *#param = #param.trim().to_string(); + ) + } else { + quote!( + #param = #param.trim().to_string(); + ) + }; + ( + field_info + .wrap_modifier_if_option(field_info.wrap_modifier_if_collection(param, tokens, self)), + None, + ) + } + Modifier::Uppercase => { + let tokens = if field_info.is_option() { + quote!( + *#param = #param.to_uppercase(); + ) + } else { + quote!( + #param = #param.to_uppercase(); + ) + }; + ( + field_info + .wrap_modifier_if_option(field_info.wrap_modifier_if_collection(param, tokens, self)), + None, + ) + } + Modifier::Lowercase => { + let tokens = if field_info.is_option() { + quote!( + *#param = #param.to_lowercase(); + ) + } else { + quote!( + #param = #param.to_lowercase(); + ) + }; + ( + field_info + .wrap_modifier_if_option(field_info.wrap_modifier_if_collection(param, tokens, self)), + None, + ) + } + Modifier::Capitalize => { + let tokens = if field_info.is_option() { + quote!( + *#param = ::std::format!("{}{}", &#param[0..1].to_uppercase(), &#param[1..]); + ) + } else { + quote!( + #param = ::std::format!("{}{}", &#param[0..1].to_uppercase(), &#param[1..]); + ) + }; + ( + field_info + .wrap_modifier_if_option(field_info.wrap_modifier_if_collection(param, tokens, self)), + None, + ) + } + Modifier::Custom { function } => { + let tokens = if field_info.is_option() { + quote!( + #function(#param); + ) + } else { + quote!( + #function(&mut #param); + ) + }; + (field_info.wrap_modifier_if_option(tokens), None) + } + Modifier::Nested => { + let par = param.to_string(); + let field = par.split('.').last().unwrap(); + + let modifications = if field_info.is_list() { + quote!( + for el in #param.iter_mut() { + el.modify(); + } + ) + } else { + quote!(#param.modify();) + }; + + let field_ident: proc_macro2::TokenStream = format!("self.{field}").parse().unwrap(); + + let param = if field_info.is_option() { + let field: proc_macro2::TokenStream = field.parse().unwrap(); + field + } else { + field_ident + }; + + let nested_validifies = if field_info.is_list() { + quote!( + for (i, el) in #param.iter_mut().enumerate() { + if let Err(mut errs) = el.validify() { + errs.errors_mut().iter_mut().for_each(|err|err.set_location_idx(i, #field)); + errors.merge(errs); + } + } + ) + } else { + quote!( + if let Err(mut err) = #param.validify() { + err.errors_mut().iter_mut().for_each(|e| e.set_location(#field)); + errors.merge(err); + } + ) + }; + ( + field_info.wrap_modifier_if_option(modifications), + Some(field_info.wrap_modifier_if_option(nested_validifies)), + ) + } + } + } +} diff --git a/rs/packages/modify_derive/src/tokens.rs b/rs/packages/modify_derive/src/tokens.rs new file mode 100644 index 00000000..a4489789 --- /dev/null +++ b/rs/packages/modify_derive/src/tokens.rs @@ -0,0 +1,18 @@ +use crate::fields::FieldInfo; +use proc_macro2::{self}; + +/// Creates a token stream applying the modifiers based on the field annotations. +pub(super) fn quote_field_modifiers( + fields: Vec, +) -> (Vec, Vec) { + let mut modifications = vec![]; + let mut nested_validifies = vec![]; + + for field_info in fields { + let (mods, nested) = field_info.quote_validifes(); + modifications.extend(mods); + nested_validifies.extend(nested); + } + + (modifications, nested_validifies) +} diff --git a/rs/packages/prex/Cargo.toml b/rs/packages/prex/Cargo.toml index cdef0bab..4a371960 100644 --- a/rs/packages/prex/Cargo.toml +++ b/rs/packages/prex/Cargo.toml @@ -25,6 +25,8 @@ tokio-tungstenite = "0.21.0" tungstenite = "0.21.0" pin-project-lite = "0.2.13" hyper-util = "0.1.2" +modify = { path = "../modify" } +validator = { version = "0.16.1", features = ["derive"] } [dev-dependencies] test-util = { version = "0.1.0", path = "../test-util" } diff --git a/rs/packages/prex/src/request.rs b/rs/packages/prex/src/request.rs index d7ba8964..8351b48f 100644 --- a/rs/packages/prex/src/request.rs +++ b/rs/packages/prex/src/request.rs @@ -5,9 +5,11 @@ use hyper::header::{HeaderName, AUTHORIZATION, HOST}; use hyper::http::Extensions; use hyper::{self, HeaderMap, Uri, Version}; use hyper::{Body, Method}; +use modify::Modify; use serde::de::DeserializeOwned; use serde::Deserialize; use std::net::{IpAddr, SocketAddr}; +use validator::{Validate, ValidationErrors}; #[allow(clippy::declare_interior_mutable_const)] const X_OPENSTREAM_FORWARDED_IP: HeaderName = HeaderName::from_static(constants::FORWARD_IP_HEADER); @@ -53,8 +55,8 @@ pub enum ReadBodyJsonError { Hyper(#[from] hyper::Error), #[error("json deserialize: {0}")] Json(#[from] serde_json::Error), - #[error("payload invalid: {0}")] - PayloadInvalid(String), + #[error("validation errors: {0}")] + Validation(#[from] ValidationErrors), } #[derive(Debug, thiserror::Error)] @@ -240,7 +242,17 @@ impl Request { }) } - pub async fn read_body_json( + pub async fn read_body_json( + &mut self, + maxlen: usize, + ) -> Result { + let mut v: T = self.read_body_json_no_validate(maxlen).await?; + v.modify(); + v.validate()?; + Ok(v) + } + + pub async fn read_body_json_no_validate( &mut self, maxlen: usize, ) -> Result { @@ -334,7 +346,7 @@ mod tests { *request.body_mut() = Body::from(r#"{"key": "value"}"#); let json: std::collections::HashMap = - request.read_body_json(1024).await.unwrap(); + request.read_body_json_no_validate(1024).await.unwrap(); assert_eq!(json.get("key").unwrap(), "value"); } @@ -448,7 +460,7 @@ mod tests { *request.body_mut() = Body::from(r#"{"key": "value"}"#); let json: Result, _> = - request.read_body_json(5).await; + request.read_body_json_no_validate(5).await; assert!(matches!(json.unwrap_err(), ReadBodyJsonError::TooLarge(5))); } @@ -470,7 +482,7 @@ mod tests { *request.body_mut() = Body::from(r#"{"key": "value",}"#); // Note the extra comma let json: Result, _> = - request.read_body_json(1024).await; + request.read_body_json_no_validate(1024).await; assert!(matches!(json.unwrap_err(), ReadBodyJsonError::Json(_))); } } From e8084357683474d6d626f1329a1701b81f1d5f1f Mon Sep 17 00:00:00 2001 From: ramiroaisen <52116153+ramiroaisen@users.noreply.github.com> Date: Mon, 15 Jan 2024 13:30:00 -0300 Subject: [PATCH 2/2] fix: serde flatten on payload (patch) --- .cargo/config.toml | 2 - .../[account]/PATCH/Payload.schema.json | 45 ++++++++----------- defs/api/accounts/[account]/PATCH/Payload.ts | 2 +- rs/packages/api/src/routes/accounts/id.rs | 1 + 4 files changed, 20 insertions(+), 30 deletions(-) diff --git a/.cargo/config.toml b/.cargo/config.toml index 54dd4676..d7ed11eb 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,5 +1,3 @@ -vendor = false - [build] rustflags = ["--cfg", "tokio_unstable"] # target="x86_64-unknown-linux-musl" diff --git a/defs/api/accounts/[account]/PATCH/Payload.schema.json b/defs/api/accounts/[account]/PATCH/Payload.schema.json index b9fe728f..791f5a6c 100644 --- a/defs/api/accounts/[account]/PATCH/Payload.schema.json +++ b/defs/api/accounts/[account]/PATCH/Payload.schema.json @@ -1,34 +1,25 @@ { "type": "object", - "required": [ - "patch" - ], "properties": { - "patch": { + "name": { + "type": "string", + "maxLength": 60, + "minLength": 1, + "nullable": true + }, + "plan_id": { + "type": "string", + "nullable": true + }, + "user_metadata": { "type": "object", - "properties": { - "name": { - "type": "string", - "maxLength": 60, - "minLength": 1, - "nullable": true - }, - "plan_id": { - "type": "string", - "nullable": true - }, - "user_metadata": { - "type": "object", - "additionalProperties": true, - "nullable": true - }, - "system_metadata": { - "type": "object", - "additionalProperties": true, - "nullable": true - } - }, - "additionalProperties": false + "additionalProperties": true, + "nullable": true + }, + "system_metadata": { + "type": "object", + "additionalProperties": true, + "nullable": true } } } \ No newline at end of file diff --git a/defs/api/accounts/[account]/PATCH/Payload.ts b/defs/api/accounts/[account]/PATCH/Payload.ts index 4a8d5af8..b9926778 100644 --- a/defs/api/accounts/[account]/PATCH/Payload.ts +++ b/defs/api/accounts/[account]/PATCH/Payload.ts @@ -1,4 +1,4 @@ // This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. import type { AccountPatch } from "../../../../ops/AccountPatch"; -export type Payload = { patch: AccountPatch }; +export type Payload = {} & AccountPatch; diff --git a/rs/packages/api/src/routes/accounts/id.rs b/rs/packages/api/src/routes/accounts/id.rs index 42dfbe1a..2d9041e2 100644 --- a/rs/packages/api/src/routes/accounts/id.rs +++ b/rs/packages/api/src/routes/accounts/id.rs @@ -106,6 +106,7 @@ pub mod patch { #[macros::schema_ts_export] pub struct Payload { #[validate] + #[serde(flatten)] pub patch: AccountPatch, }