From cf0fba7891fdf2cdece6e385940a73548b2dabc2 Mon Sep 17 00:00:00 2001 From: Sean Hagstrom Date: Wed, 13 Nov 2024 09:30:47 -0800 Subject: [PATCH] feature!: add maximum character limit to password length (#21593) This change now limits the password for a profile to be a maximum of a 100 characters. --- .../common/password_with_hint/style.cljs | 4 + .../common/password_with_hint/view.cljs | 24 +++++ src/status_im/common/validation/password.cljs | 36 ++++++++ src/status_im/constants.cljs | 8 +- .../onboarding/create_password/view.cljs | 87 +++++++------------ .../change_password/new_password_form.cljs | 62 ++++--------- src/utils/string.cljs | 4 + translations/en.json | 1 + 8 files changed, 122 insertions(+), 104 deletions(-) create mode 100644 src/status_im/common/password_with_hint/style.cljs create mode 100644 src/status_im/common/password_with_hint/view.cljs create mode 100644 src/status_im/common/validation/password.cljs diff --git a/src/status_im/common/password_with_hint/style.cljs b/src/status_im/common/password_with_hint/style.cljs new file mode 100644 index 0000000000..dea0227170 --- /dev/null +++ b/src/status_im/common/password_with_hint/style.cljs @@ -0,0 +1,4 @@ +(ns status-im.common.password-with-hint.style) + +(def info-message + {:margin-top 8}) diff --git a/src/status_im/common/password_with_hint/view.cljs b/src/status_im/common/password_with_hint/view.cljs new file mode 100644 index 0000000000..1308d0e559 --- /dev/null +++ b/src/status_im/common/password_with_hint/view.cljs @@ -0,0 +1,24 @@ +(ns status-im.common.password-with-hint.view + (:require + [quo.core :as quo] + [quo.foundations.colors :as colors] + [react-native.core :as rn] + [status-im.common.password-with-hint.style :as style])) + +(defn view + [{{:keys [text status shown?]} :hint :as input-props}] + [:<> + [quo/input + (-> input-props + (dissoc :hint) + (assoc :type :password + :blur? true))] + [rn/view {:style style/info-message} + (when shown? + [quo/info-message + (cond-> {:status status + :size :default} + (not= :success status) (assoc :icon :i/info) + (= :success status) (assoc :icon :i/check-circle) + (= :default status) (assoc :color colors/white-70-blur)) + text])]]) diff --git a/src/status_im/common/validation/password.cljs b/src/status_im/common/validation/password.cljs new file mode 100644 index 0000000000..d536a4d271 --- /dev/null +++ b/src/status_im/common/validation/password.cljs @@ -0,0 +1,36 @@ +(ns status-im.common.validation.password + (:require + [status-im.constants :as constants] + [utils.string :as utils.string])) + +(defn validate-short-enough? + [password] + (utils.string/at-most-n-chars? password + constants/new-password-max-length)) + +(defn validate-long-enough? + [password] + (utils.string/at-least-n-chars? password + constants/new-password-min-length)) + +(defn validate + [password] + (let [validations (juxt + utils.string/has-lower-case? + utils.string/has-upper-case? + utils.string/has-numbers? + utils.string/has-symbols? + validate-long-enough? + validate-short-enough?)] + (->> password + validations + (zipmap (conj constants/password-tips + :long-enough? + :short-enough?))))) + +(defn strength + [validations] + (->> (select-keys validations constants/password-tips) + (vals) + (filter true?) + count)) diff --git a/src/status_im/constants.cljs b/src/status_im/constants.cljs index 96427f8c79..f4b6651cba 100644 --- a/src/status_im/constants.cljs +++ b/src/status_im/constants.cljs @@ -133,12 +133,18 @@ (def ^:const min-password-length 6) (def ^:const pincode-length 6) (def ^:const new-password-min-length 10) +(def ^:const new-password-max-length 100) (def ^:const max-group-chat-participants 20) (def ^:const max-group-chat-name-length 24) (def ^:const default-number-of-messages 20) (def ^:const default-number-of-pin-messages 3) -(def ^:const password-tips [:lower-case? :upper-case? :numbers? :symbols?]) +(def ^:const password-tips + [:lower-case? + :upper-case? + :numbers? + :symbols?]) + (def ^:const strength-status {1 :very-weak 2 :weak diff --git a/src/status_im/contexts/onboarding/create_password/view.cljs b/src/status_im/contexts/onboarding/create_password/view.cljs index 3f295e6981..c09e3fd8be 100644 --- a/src/status_im/contexts/onboarding/create_password/view.cljs +++ b/src/status_im/contexts/onboarding/create_password/view.cljs @@ -6,12 +6,13 @@ [react-native.platform :as platform] [react-native.safe-area :as safe-area] [status-im.common.floating-button-page.view :as floating-button] + [status-im.common.password-with-hint.view :as password-with-hint] + [status-im.common.validation.password :as password] [status-im.constants :as constants] [status-im.contexts.onboarding.create-password.style :as style] [utils.i18n :as i18n] [utils.re-frame :as rf] - [utils.security.core :as security] - [utils.string :as utils.string])) + [utils.security.core :as security])) (defn header [] @@ -27,27 +28,9 @@ :size :paragraph-1} (i18n/label :t/password-creation-subtitle)]]) -(defn password-with-hint - [{{:keys [text status shown]} :hint :as input-props}] - [rn/view - [quo/input - (-> input-props - (dissoc :hint) - (assoc :type :password - :blur? true))] - [rn/view {:style style/info-message} - (when shown - [quo/info-message - {:status status - :size :default - :icon (if (= status :success) :i/check-circle :i/info) - :color (when (= status :default) - colors/white-70-blur)} - text])]]) - (defn password-inputs [{:keys [passwords-match? on-change-password on-change-repeat-password on-input-focus - password-long-enough? empty-password? show-password-validation? + password-long-enough? password-short-enough? empty-password? show-password-validation? on-blur-repeat-password]}] (let [hint-1-status (if password-long-enough? :success :default) hint-2-status (if passwords-match? :success :error) @@ -58,19 +41,24 @@ (not passwords-match?) (not empty-password?))] [:<> - [password-with-hint - {:hint {:text (i18n/label :t/password-creation-hint) - :status hint-1-status - :shown true} + [password-with-hint/view + {:hint (if (not password-short-enough?) + {:text (i18n/label + :t/password-creation-max-length-hint) + :status :error + :shown? true} + {:text (i18n/label :t/password-creation-hint) + :status hint-1-status + :shown? true}) :placeholder (i18n/label :t/password-creation-placeholder-1) :on-change-text on-change-password :on-focus on-input-focus :auto-focus true}] [rn/view {:style style/space-between-inputs}] - [password-with-hint + [password-with-hint/view {:hint {:text hint-2-text :status hint-2-status - :shown (and (not empty-password?) + :shown? (and (not empty-password?) show-password-validation?)} :error? error? :placeholder (i18n/label :t/password-creation-placeholder-2) @@ -94,33 +82,17 @@ [quo/tips {:completed? symbols?} (i18n/label :t/password-creation-tips-4)]]]) -(defn validate-password - [password] - (let [validations (juxt utils.string/has-lower-case? - utils.string/has-upper-case? - utils.string/has-numbers? - utils.string/has-symbols? - #(utils.string/at-least-n-chars? % constants/new-password-min-length))] - (->> password - validations - (zipmap (conj constants/password-tips :long-enough?))))) - -(defn calc-password-strength - [validations] - (->> (vals validations) - (filter true?) - count)) - (defn- use-password-checks [password] (rn/use-memo (fn [] - (let [{:keys [long-enough?] - :as validations} (validate-password password)] - {:password-long-enough? long-enough? - :password-validations validations - :password-strength (calc-password-strength validations) - :empty-password? (empty? password)})) + (let [{:keys [long-enough? short-enough?] + :as validations} (password/validate password)] + {:password-long-enough? long-enough? + :password-short-enough? short-enough? + :password-validations validations + :password-strength (password/strength validations) + :empty-password? (empty? password)})) [password])) (defn- use-repeat-password-checks @@ -174,20 +146,18 @@ {:keys [password-long-enough? + password-short-enough? password-validations password-strength empty-password?]} (use-password-checks password) {:keys [same-password-length? same-passwords?]} (use-repeat-password-checks password repeat-password) - meet-requirements? (rn/use-memo - #(and (not empty-password?) - (utils.string/at-least-n-chars? password - 10) - same-passwords? - accepts-disclaimer?) - [password repeat-password - accepts-disclaimer?])] + meet-requirements? (and (not empty-password?) + password-long-enough? + password-short-enough? + same-passwords? + accepts-disclaimer?)] [floating-button/view {:header [page-nav] @@ -231,6 +201,7 @@ [header] [password-inputs {:password-long-enough? password-long-enough? + :password-short-enough? password-short-enough? :passwords-match? same-passwords? :empty-password? empty-password? :show-password-validation? show-password-validation? diff --git a/src/status_im/contexts/profile/settings/screens/password/change_password/new_password_form.cljs b/src/status_im/contexts/profile/settings/screens/password/change_password/new_password_form.cljs index 17630d8b6c..f66a5f63a3 100644 --- a/src/status_im/contexts/profile/settings/screens/password/change_password/new_password_form.cljs +++ b/src/status_im/contexts/profile/settings/screens/password/change_password/new_password_form.cljs @@ -2,45 +2,21 @@ (:require [clojure.string :as string] [quo.core :as quo] - [quo.foundations.colors :as colors] [react-native.core :as rn] + [status-im.common.password-with-hint.view :as password-with-hint] + [status-im.common.validation.password :as password] [status-im.constants :as constant] [status-im.contexts.profile.settings.screens.password.change-password.events] [status-im.contexts.profile.settings.screens.password.change-password.header :as header] [status-im.contexts.profile.settings.screens.password.change-password.style :as style] [utils.i18n :as i18n] [utils.re-frame :as rf] - [utils.security.core :as security] - [utils.string :as utils.string])) - -(defn- password-with-hint - [{{:keys [text status shown?]} :hint :as input-props}] - [:<> - [quo/input - (-> input-props - (dissoc :hint) - (assoc :type :password - :blur? true))] - [rn/view {:style style/info-message} - (when shown? - [quo/info-message - (cond-> {:status status - :size :default} - (not= :success status) (assoc :icon :i/info) - (= :success status) (assoc :icon :i/check-circle) - (= :default status) (assoc :color colors/white-70-blur)) - text])]]) - -(defn- calc-password-strength - [validations] - (->> (vals validations) - (filter true?) - count)) + [utils.security.core :as security])) (defn- help [{:keys [validations]}] (let [{:keys [lower-case? upper-case? numbers? symbols?]} validations - password-strength (calc-password-strength validations)] + password-strength (password/strength validations)] [rn/view [quo/strength-divider {:type (constant/strength-status password-strength :info)} (i18n/label :t/password-creation-tips-title)] @@ -54,12 +30,7 @@ [quo/tips {:completed? symbols?} (i18n/label :t/password-creation-tips-4)]]])) -(defn- password-validations - [password] - {:lower-case? (utils.string/has-lower-case? password) - :upper-case? (utils.string/has-upper-case? password) - :numbers? (utils.string/has-numbers? password) - :symbols? (utils.string/has-symbols? password)}) +(def not-blank? (complement string/blank?)) (defn view [] @@ -70,17 +41,14 @@ [focused? set-focused] (rn/use-state false) [show-validation? set-show-validation] (rn/use-state false) - ;; validations - not-blank? (complement string/blank?) - validations (password-validations password) - long-enough? (utils.string/at-least-n-chars? - password - constant/new-password-min-length) + {:keys [long-enough? short-enough?] + :as validations} (password/validate password) empty-password? (string/blank? password) same-passwords? (and (not empty-password?) (= password repeat-password)) meet-requirements? (and (not empty-password?) long-enough? + short-enough? same-passwords? disclaimer-accepted?) error? (and show-validation? @@ -115,17 +83,21 @@ [:<> [rn/scroll-view {:style style/form-container} [header/view] - [password-with-hint - {:hint {:text (i18n/label :t/password-creation-hint) - :status (if long-enough? :success :default) - :shown? true} + [password-with-hint/view + {:hint (if (not short-enough?) + {:text (i18n/label :t/password-creation-max-length-hint) + :status :error + :shown? true} + {:text (i18n/label :t/password-creation-hint) + :status (if long-enough? :success :default) + :shown? true}) :placeholder (i18n/label :t/change-password-new-password-placeholder) :label (i18n/label :t/change-password-new-password-label) :on-change-text on-change-password :on-focus on-input-focus :auto-focus true}] [rn/view {:style style/space-between-inputs}] - [password-with-hint + [password-with-hint/view {:hint {:text (if same-passwords? (i18n/label :t/password-creation-match) (i18n/label :t/password-creation-dont-match)) diff --git a/src/utils/string.cljs b/src/utils/string.cljs index ccef67856e..d09833b393 100644 --- a/src/utils/string.cljs +++ b/src/utils/string.cljs @@ -42,6 +42,10 @@ [s n] (>= (count s) n)) +(defn at-most-n-chars? + [s n] + (<= (count s) n)) + (defn safe-trim [s] (when (string? s) diff --git a/translations/en.json b/translations/en.json index 14e18e7245..9a90e08e3b 100644 --- a/translations/en.json +++ b/translations/en.json @@ -1900,6 +1900,7 @@ "password-creation-dont-match": "Passwords do not match", "password-creation-hint": "Minimum 10 characters", "password-creation-match": "Passwords match", + "password-creation-max-length-hint": "Maximum 100 characters", "password-creation-placeholder-1": "Type password", "password-creation-placeholder-2": "Repeat password", "password-creation-subtitle": "To log in to Status and sign transactions",