on master help sign in
Got password generator working
commit595331b575287ff31c3d77787c5e9f837a5fb014 authoriko <ilyakooo0@gmail.com> dateFri 12 Aug 2022, 13:19 (4 years ago) parent83e9a4e
17 files changed+575−54
.vscode/tasks.json+31 backend/BW.js+9−2 backend/BW.purs+7 backend/Bridge.purs+30 backend/Main.purs+5 bridge.yaml+19 frontend/Bridge.elm+65 frontend/GlobalEvents.elm+1 frontend/Main.elm+8−1 frontend/Modal.elm+33 frontend/Pages/Cipher.elm+3 frontend/Pages/Ciphers.elm+3 frontend/Pages/EditCipher.elm+84−38 frontend/Pages/PasswordGenerator.elm+158 frontend/Switch.elm+35 scratchpad/password_generator.html+59 style.css+25−13
.vscode/tasks.jsonnew file+31
@@ -0,0 +1,31 @@
1+{
2+ // See https://go.microsoft.com/fwlink/?LinkId=733558
3+ // for the documentation about the tasks.json format
4+ "version": "2.0.0",
5+ "tasks": [
6+ {
7+ "label": "Generate bridge",
8+ "type": "process",
9+ "command": "${workspaceFolder}/dev/generate-bridge.sh",
10+ "problemMatcher": []
11+ },
12+ {
13+ "label": "Build backend",
14+ "type": "process",
15+ "command": "${workspaceFolder}/dev/build-backend.sh",
16+ "problemMatcher": []
17+ },
18+ {
19+ "label": "Start Chrome",
20+ "type": "process",
21+ "command": "${workspaceFolder}/dev/start-chrome.sh",
22+ "problemMatcher": []
23+ },
24+ {
25+ "label": "Start Elm-live",
26+ "type": "process",
27+ "command": "${workspaceFolder}/dev/elm-live.sh",
28+ "problemMatcher": []
29+ }
30+ ]
31+}
backend/BW.js+9−2
@@ -15,10 +15,11 @@ import { CryptoService } from "../../deps/bw/libs/shared/dist/src/services/crypt
1515import { I18nService } from "../../deps/bw/libs/shared/dist/src/services/i18n.service";
1616import { StateService as BaseStateService } from "../../deps/bw/libs/shared/dist/src/services/state.service";
1717import { PasswordTokenRequest } from "../../deps/bw/libs/shared/dist/src/models/request/identityToken/passwordTokenRequest";
18−import { TokenRequestTwoFactor } from "../../deps/bw/libs/common/src/models/request/identityToken/tokenRequestTwoFactor.ts";
18+import { TokenRequestTwoFactor } from "../../deps/bw/libs/common/src/models/request/identityToken/tokenRequestTwoFactor";
1919import { CipherRequest } from "../../deps/bw/libs/common/src/models/request/cipherRequest";
2020import { Cipher } from "../../deps/bw/libs/common/src/models/domain/cipher";
2121import { CipherData } from "../../deps/bw/libs/common/src/models/data/cipherData";
22+import { PasswordGenerationService } from "../../deps/bw/libs/common/src/services/passwordGeneration.service";
2223
2324function sanitize(obj) {
2425 return JSON.parse(JSON.stringify(obj));
@@ -80,7 +81,8 @@ export function getServices() {
8081 }
8182 },
8283 crypto: bg.cryptoService,
83− cryptoFunctions: bg.cryptoFunctionService
84+ cryptoFunctions: bg.cryptoFunctionService,
85+ passwordGeneration: bg.passwordGenerationService
8486 }
8587}
8688
@@ -162,6 +164,11 @@ class MainBackground {
162164 this.appIdService,
163165 (expired) => this.logout(expired)
164166 );
167+ this.passwordGenerationService = new PasswordGenerationService(
168+ this.cryptoService,
169+ this.policyService,
170+ this.stateService
171+ );
165172 }
166173
167174 async bootstrap() {
backend/BW.purs+7
@@ -1,8 +1,10 @@
11module BW where
22
33import BW.Types
4+import Bridge
45import Prelude
56
7+import Bridge as Bridge
68import Control.Promise (Promise)
79import Data.Argonaut (class DecodeJson, class EncodeJson, decodeJson, encodeJson)
810import Data.ArrayBuffer.Typed as AB
@@ -34,6 +36,10 @@ type ApiService
3436 , putCipher :: CipherResponse -> Promise CipherResponse
3537 }
3638
39+type PasswordGeneration =
40+ { generatePassword :: Bridge.PasswordGeneratorConfig -> Promise String
41+ }
42+
3743cryptoFunctionsTypeSha1 = "sha1" :: CryptoFunctionsType
3844
3945cryptoFunctionsTypeSha256 = "sha256" :: CryptoFunctionsType
@@ -77,6 +83,7 @@ type Services
7783 = { crypto :: CryptoService
7884 , getApi :: Urls -> JNullable IdentityTokenResponse -> Promise ApiService
7985 , cryptoFunctions :: CryptoFunctions
86+ , passwordGeneration :: PasswordGeneration
8087 }
8188
8289foreign import getServices :: Effect Services
backend/Bridge.purs+30
@@ -448,6 +448,7 @@ derive instance ordCmd_Login :: Ord Cmd_Login
448448
449449data Cmd =
450450 Copy String
451+ | GeneratePassword PasswordGeneratorConfig
451452 | Init
452453 | Login Cmd_Login
453454 | NeedCiphersList
@@ -484,6 +485,34 @@ derive instance genericFullCipher :: Generic FullCipher _
484485derive instance eqFullCipher :: Eq FullCipher
485486derive instance ordFullCipher :: Ord FullCipher
486487
488+newtype PasswordGeneratorConfig =
489+ PasswordGeneratorConfig {
490+ ambiguous :: Boolean
491+ , capitalize :: Boolean
492+ , includeNumber :: Boolean
493+ , length :: Int
494+ , lowercase :: Boolean
495+ , minLowercase :: Int
496+ , minNumber :: Int
497+ , minSpecial :: Int
498+ , minUppercase :: Int
499+ , numWords :: Int
500+ , number :: Boolean
501+ , special :: Boolean
502+ , type :: String
503+ , uppercase :: Boolean
504+ , wordSeparator :: String
505+ }
506+
507+derive instance newtypePasswordGeneratorConfig :: Newtype PasswordGeneratorConfig _
508+instance encodeJsonPasswordGeneratorConfig :: EncodeJson PasswordGeneratorConfig where
509+ encodeJson = genericEncodeAeson Argonaut.defaultOptions
510+instance decodeJsonPasswordGeneratorConfig :: DecodeJson PasswordGeneratorConfig where
511+ decodeJson = genericDecodeAeson Argonaut.defaultOptions
512+derive instance genericPasswordGeneratorConfig :: Generic PasswordGeneratorConfig _
513+derive instance eqPasswordGeneratorConfig :: Eq PasswordGeneratorConfig
514+derive instance ordPasswordGeneratorConfig :: Ord PasswordGeneratorConfig
515+
487516newtype Sub_LoadCiphers =
488517 Sub_LoadCiphers {
489518 cipherType :: CipherType
@@ -532,6 +561,7 @@ data Sub =
532561 CaptchaDone
533562 | CipherChanged FullCipher
534563 | Error String
564+ | GeneratedPassword String
535565 | LoadCipher FullCipher
536566 | LoadCiphers Sub_LoadCiphers_List
537567 | LoginSuccessful
backend/Main.purs+5
@@ -190,6 +190,11 @@ main = do
190190 send $ Bridge.CipherChanged newCipher
191191 performSync
192192 pure unit
193+ Bridge.GeneratePassword cfg ->
194+ runWithDecryptionKey do
195+ password <- liftPromise $ services.passwordGeneration.generatePassword cfg
196+ send $ Bridge.GeneratedPassword password
197+ pure unit
193198
194199processCipher ::
195200 forall r.
bridge.yaml+19
@@ -17,6 +17,7 @@ Sub:
1717- LoadCipher: FullCipher
1818- RecieveEmail: String
1919- CipherChanged: FullCipher
20+- GeneratedPassword: String
2021
2122Cmd:
2223- Init
@@ -32,6 +33,7 @@ Cmd:
3233- Copy: String
3334- Open: String
3435- UpdateCipher: FullCipher
36+- GeneratePassword: PasswordGeneratorConfig
3537
3638FullCipher:
3739 reprompt: Int
@@ -104,3 +106,20 @@ CipherType:
104106- CardType
105107- NoteType
106108- IdentityType
109+
110+PasswordGeneratorConfig:
111+ length: Int
112+ ambiguous: Bool
113+ number: Bool
114+ minNumber: Int
115+ uppercase: Bool
116+ minUppercase: Int
117+ lowercase: Bool
118+ minLowercase: Int
119+ special: Bool
120+ minSpecial: Int
121+ type: String
122+ numWords: Int
123+ wordSeparator: String
124+ capitalize: Bool
125+ includeNumber: Bool
frontend/Bridge.elm+65
@@ -502,6 +502,7 @@ jsonEncCmd_Login val =
502502
503503type Cmd =
504504 Copy String
505+ | GeneratePassword PasswordGeneratorConfig
505506 | Init
506507 | Login Cmd_Login
507508 | NeedCiphersList
@@ -516,6 +517,7 @@ jsonDecCmd : Json.Decode.Decoder ( Cmd )
516517jsonDecCmd =
517518 let jsonDecDictCmd = Dict.fromList
518519 [ ("Copy", Json.Decode.lazy (\_ -> Json.Decode.map Copy (Json.Decode.string)))
520+ , ("GeneratePassword", Json.Decode.lazy (\_ -> Json.Decode.map GeneratePassword (jsonDecPasswordGeneratorConfig)))
519521 , ("Init", Json.Decode.lazy (\_ -> Json.Decode.succeed Init))
520522 , ("Login", Json.Decode.lazy (\_ -> Json.Decode.map Login (jsonDecCmd_Login)))
521523 , ("NeedCiphersList", Json.Decode.lazy (\_ -> Json.Decode.succeed NeedCiphersList))
@@ -533,6 +535,7 @@ jsonEncCmd : Cmd -> Value
533535jsonEncCmd val =
534536 let keyval v = case v of
535537 Copy v1 -> ("Copy", encodeValue (Json.Encode.string v1))
538+ GeneratePassword v1 -> ("GeneratePassword", encodeValue (jsonEncPasswordGeneratorConfig v1))
536539 Init -> ("Init", encodeValue (Json.Encode.list identity []))
537540 Login v1 -> ("Login", encodeValue (jsonEncCmd_Login v1))
538541 NeedCiphersList -> ("NeedCiphersList", encodeValue (Json.Encode.list identity []))
@@ -575,6 +578,65 @@ jsonEncFullCipher val =
575578
576579
577580
581+type alias PasswordGeneratorConfig =
582+ { ambiguous: Bool
583+ , capitalize: Bool
584+ , includeNumber: Bool
585+ , length: Int
586+ , lowercase: Bool
587+ , minLowercase: Int
588+ , minNumber: Int
589+ , minSpecial: Int
590+ , minUppercase: Int
591+ , numWords: Int
592+ , number: Bool
593+ , special: Bool
594+ , type_: String
595+ , uppercase: Bool
596+ , wordSeparator: String
597+ }
598+
599+jsonDecPasswordGeneratorConfig : Json.Decode.Decoder ( PasswordGeneratorConfig )
600+jsonDecPasswordGeneratorConfig =
601+ Json.Decode.succeed (\pambiguous pcapitalize pincludeNumber plength plowercase pminLowercase pminNumber pminSpecial pminUppercase pnumWords pnumber pspecial ptype puppercase pwordSeparator -> {ambiguous = pambiguous, capitalize = pcapitalize, includeNumber = pincludeNumber, length = plength, lowercase = plowercase, minLowercase = pminLowercase, minNumber = pminNumber, minSpecial = pminSpecial, minUppercase = pminUppercase, numWords = pnumWords, number = pnumber, special = pspecial, type_ = ptype, uppercase = puppercase, wordSeparator = pwordSeparator})
602+ |> required "ambiguous" (Json.Decode.bool)
603+ |> required "capitalize" (Json.Decode.bool)
604+ |> required "includeNumber" (Json.Decode.bool)
605+ |> required "length" (Json.Decode.int)
606+ |> required "lowercase" (Json.Decode.bool)
607+ |> required "minLowercase" (Json.Decode.int)
608+ |> required "minNumber" (Json.Decode.int)
609+ |> required "minSpecial" (Json.Decode.int)
610+ |> required "minUppercase" (Json.Decode.int)
611+ |> required "numWords" (Json.Decode.int)
612+ |> required "number" (Json.Decode.bool)
613+ |> required "special" (Json.Decode.bool)
614+ |> required "type" (Json.Decode.string)
615+ |> required "uppercase" (Json.Decode.bool)
616+ |> required "wordSeparator" (Json.Decode.string)
617+
618+jsonEncPasswordGeneratorConfig : PasswordGeneratorConfig -> Value
619+jsonEncPasswordGeneratorConfig val =
620+ Json.Encode.object
621+ [ ("ambiguous", Json.Encode.bool val.ambiguous)
622+ , ("capitalize", Json.Encode.bool val.capitalize)
623+ , ("includeNumber", Json.Encode.bool val.includeNumber)
624+ , ("length", Json.Encode.int val.length)
625+ , ("lowercase", Json.Encode.bool val.lowercase)
626+ , ("minLowercase", Json.Encode.int val.minLowercase)
627+ , ("minNumber", Json.Encode.int val.minNumber)
628+ , ("minSpecial", Json.Encode.int val.minSpecial)
629+ , ("minUppercase", Json.Encode.int val.minUppercase)
630+ , ("numWords", Json.Encode.int val.numWords)
631+ , ("number", Json.Encode.bool val.number)
632+ , ("special", Json.Encode.bool val.special)
633+ , ("type", Json.Encode.string val.type_)
634+ , ("uppercase", Json.Encode.bool val.uppercase)
635+ , ("wordSeparator", Json.Encode.string val.wordSeparator)
636+ ]
637+
638+
639+
578640type alias Sub_LoadCiphers =
579641 { cipherType: CipherType
580642 , date: String
@@ -636,6 +698,7 @@ type Sub =
636698 CaptchaDone
637699 | CipherChanged FullCipher
638700 | Error String
701+ | GeneratedPassword String
639702 | LoadCipher FullCipher
640703 | LoadCiphers Sub_LoadCiphers_List
641704 | LoginSuccessful
@@ -651,6 +714,7 @@ jsonDecSub =
651714 [ ("CaptchaDone", Json.Decode.lazy (\_ -> Json.Decode.succeed CaptchaDone))
652715 , ("CipherChanged", Json.Decode.lazy (\_ -> Json.Decode.map CipherChanged (jsonDecFullCipher)))
653716 , ("Error", Json.Decode.lazy (\_ -> Json.Decode.map Error (Json.Decode.string)))
717+ , ("GeneratedPassword", Json.Decode.lazy (\_ -> Json.Decode.map GeneratedPassword (Json.Decode.string)))
654718 , ("LoadCipher", Json.Decode.lazy (\_ -> Json.Decode.map LoadCipher (jsonDecFullCipher)))
655719 , ("LoadCiphers", Json.Decode.lazy (\_ -> Json.Decode.map LoadCiphers (jsonDecSub_LoadCiphers_List)))
656720 , ("LoginSuccessful", Json.Decode.lazy (\_ -> Json.Decode.succeed LoginSuccessful))
@@ -669,6 +733,7 @@ jsonEncSub val =
669733 CaptchaDone -> ("CaptchaDone", encodeValue (Json.Encode.list identity []))
670734 CipherChanged v1 -> ("CipherChanged", encodeValue (jsonEncFullCipher v1))
671735 Error v1 -> ("Error", encodeValue (Json.Encode.string v1))
736+ GeneratedPassword v1 -> ("GeneratedPassword", encodeValue (Json.Encode.string v1))
672737 LoadCipher v1 -> ("LoadCipher", encodeValue (jsonEncFullCipher v1))
673738 LoadCiphers v1 -> ("LoadCiphers", encodeValue (jsonEncSub_LoadCiphers_List v1))
674739 LoginSuccessful -> ("LoginSuccessful", encodeValue (Json.Encode.list identity []))
frontend/GlobalEvents.elm+1
@@ -5,3 +5,4 @@ import Bridge
55
66type Event
77 = UpdateCipher Bridge.FullCipher
8+ | GeneratedPassword String
frontend/Main.elm+8−1
@@ -59,6 +59,7 @@ type Msg
5959 | EditCipher Bridge.FullCipher
6060 | UpdateCipher Bridge.FullCipher
6161 | FireGlobalEvent Event
62+ | GeneratePassword Bridge.PasswordGeneratorConfig
6263
6364
6465type alias Model =
@@ -217,6 +218,9 @@ subscriptions model =
217218
218219 Bridge.CipherChanged c ->
219220 ShowInfo "Entry updated" ("The entry \"" ++ c.name ++ "\" has been successfully updated.")
221+
222+ Bridge.GeneratedPassword password ->
223+ GlobalEvents.GeneratedPassword password |> FireGlobalEvent
220224 )
221225 ]
222226 ++ optional (List.isEmpty model.notifications |> not) (Time.every 1000 (\t -> ClearNotification { currentTime = t }))
@@ -519,6 +523,9 @@ update msg model =
519523 , Cmd.none
520524 )
521525
526+ GeneratePassword cfg ->
527+ ( model, FFI.sendBridge (Bridge.GeneratePassword cfg) )
528+
522529
523530loginCallbacks : Login.Callbacks Msg
524531loginCallbacks =
@@ -537,7 +544,7 @@ cipherCallbacks =
537544
538545editCipherCallbacks : EditCipher.Callbacks Msg
539546editCipherCallbacks =
540− { save = UpdateCipher }
547+ { save = UpdateCipher, generatePassword = GeneratePassword }
541548
542549
543550captchaCallbacks : Captcha.Callbacks
frontend/Modal.elmnew file+33
@@ -0,0 +1,33 @@
1+module Modal exposing (..)
2+
3+import Html exposing (..)
4+import Html.Attributes as Attr
5+import Html.Events as Ev
6+
7+
8+type alias ModelConfig msg =
9+ { title : String
10+ , body : List (Html msg)
11+ , close : msg
12+ , footer : List (Html msg)
13+ }
14+
15+
16+modal : ModelConfig msg -> Html msg
17+modal cfg =
18+ div
19+ [ Attr.class "p-modal" ]
20+ [ section
21+ [ Attr.class "p-modal__dialog"
22+ ]
23+ (header
24+ [ Attr.class "p-modal__header"
25+ ]
26+ [ h2 [ Attr.class "p-modal__title" ] [ text cfg.title ]
27+ , button [ Attr.class "p-modal__close", Ev.onClick cfg.close ] [ text "Close" ]
28+ ]
29+ :: cfg.body
30+ ++ [ footer [ Attr.class "p-modal__footer" ] cfg.footer
31+ ]
32+ )
33+ ]
frontend/Pages/Cipher.elm+3
@@ -53,6 +53,9 @@ page callbacks liftMsg =
5353 else
5454 model.cipher
5555 }
56+
57+ _ ->
58+ model
5659 }
5760
5861
frontend/Pages/Ciphers.elm+3
@@ -135,6 +135,9 @@ page callbacks liftMsg =
135135 cipher
136136 )
137137 }
138+
139+ _ ->
140+ model
138141 }
139142
140143
frontend/Pages/EditCipher.elm+84−38
@@ -5,21 +5,30 @@ import GlobalEvents
55import Html exposing (..)
66import Html.Attributes as Attr
77import Html.Events as Ev
8+import Modal exposing (modal)
89import Page exposing (..)
10+import Pages.PasswordGenerator as PasswordGenerator
911import Utils exposing (..)
1012
1113
1214type alias Model =
1315 { fullCipher : Bridge.FullCipher
16+ , passwordGenerator : Maybe PasswordGenerator.Model
1417 }
1518
1619
1720type Msg
1821 = EditCipher Bridge.FullCipher
22+ | PasswordGeneratorMsg PasswordGenerator.Msg
23+ | OpenGeneratePasword
24+ | CloseGeneratePassword
25+ | GeneratePassword Bridge.PasswordGeneratorConfig
1926
2027
2128type alias Callbacks msg =
22− { save : Bridge.FullCipher -> msg }
29+ { save : Bridge.FullCipher -> msg
30+ , generatePassword : Bridge.PasswordGeneratorConfig -> msg
31+ }
2332
2433
2534page : Callbacks emsg -> Page Bridge.FullCipher Model Msg emsg
@@ -45,23 +54,60 @@ page callbacks liftMsg =
4554 else
4655 model.fullCipher
4756 }
57+
58+ GlobalEvents.GeneratedPassword password ->
59+ let
60+ fullCipher =
61+ model.fullCipher
62+ in
63+ { model
64+ | passwordGenerator = Nothing
65+ , fullCipher =
66+ { fullCipher
67+ | cipher =
68+ case fullCipher.cipher of
69+ Bridge.LoginCipher c ->
70+ Bridge.LoginCipher
71+ { c | password = Just password }
72+
73+ c ->
74+ c
75+ }
76+ }
4877 }
4978
5079
5180init : Bridge.FullCipher -> ( Model, Cmd Msg )
5281init cipher =
5382 ( { fullCipher = cipher
83+ , passwordGenerator = Nothing
5484 }
5585 , Cmd.none
5686 )
5787
5888
5989update : Callbacks emsg -> (Msg -> emsg) -> Msg -> Model -> ( Result String Model, Cmd emsg )
60−update _ _ msg model =
90+update { generatePassword } _ msg model =
6191 case msg of
6292 EditCipher fullCipher ->
6393 ( Ok { model | fullCipher = fullCipher }, Cmd.none )
6494
95+ PasswordGeneratorMsg m ->
96+ ( Ok { model | passwordGenerator = Maybe.map (PasswordGenerator.update m) model.passwordGenerator }
97+ , Cmd.none
98+ )
99+
100+ OpenGeneratePasword ->
101+ ( Ok { model | passwordGenerator = Just PasswordGenerator.init }
102+ , Cmd.none
103+ )
104+
105+ CloseGeneratePassword ->
106+ ( Ok { model | passwordGenerator = Nothing }, Cmd.none )
107+
108+ GeneratePassword cfg ->
109+ ( Ok model, pureCmd (generatePassword cfg) )
110+
65111
66112subscriptions : Model -> Sub Msg
67113subscriptions _ =
@@ -83,18 +129,34 @@ row { name, nameIcon, attrs } =
83129
84130
85131view : Model -> List (Html Msg)
86−view { fullCipher } =
87− [ row
88− { name = "Entry name"
89− , nameIcon = ""
90− , attrs =
91− [ Attr.type_ "text"
92− , Attr.value fullCipher.name
93− , Ev.onInput (\x -> { fullCipher | name = x } |> EditCipher)
94− , Attr.attribute "autocomplete" "off"
95− ]
96− }
97− ]
132+view { fullCipher, passwordGenerator } =
133+ maybeList passwordGenerator
134+ (\x ->
135+ modal
136+ { title = "Password generator"
137+ , close = CloseGeneratePassword
138+ , footer =
139+ [ button
140+ [ Attr.class "p-button--positive"
141+ , Ev.onClick <| GeneratePassword x.passwordConfig
142+ ]
143+ [ text "Generate password" ]
144+ ]
145+ , body =
146+ PasswordGenerator.view x |> List.map (Html.map PasswordGeneratorMsg)
147+ }
148+ )
149+ ++ [ row
150+ { name = "Entry name"
151+ , nameIcon = ""
152+ , attrs =
153+ [ Attr.type_ "text"
154+ , Attr.value fullCipher.name
155+ , Ev.onInput (\x -> { fullCipher | name = x } |> EditCipher)
156+ , Attr.attribute "autocomplete" "off"
157+ ]
158+ }
159+ ]
98160 ++ (case fullCipher.cipher of
99161 Bridge.LoginCipher cipher ->
100162 let
@@ -114,16 +176,19 @@ view { fullCipher } =
114176 , Attr.attribute "autocomplete" "username"
115177 ]
116178 }
117− , row
118− { name = "Password"
119− , nameIcon = "security"
120− , attrs =
179+ , p []
180+ [ heading "security" "Password"
181+ , br [] []
182+ , input
121183 [ Attr.type_ "text"
122184 , Attr.value (Maybe.withDefault "" password)
123185 , Ev.onInput (\x -> edit { cipher | password = Just x })
124186 , Attr.attribute "autocomplete" "new-password"
187+ , Attr.style "width" "calc(100% - 3.5rem)"
125188 ]
126− }
189+ []
190+ , iconButton "restart" OpenGeneratePasword
191+ ]
127192 ]
128193 ++ (uris
129194 |> List.indexedMap
@@ -367,26 +432,7 @@ view { fullCipher } =
367432 )
368433
369434
370−hiddenPassword : String
371−hiddenPassword =
372− String.repeat 12 "●"
373−
374−
375−hiddenCvv : String
376−hiddenCvv =
377− String.repeat 3 "●"
378−
379−
380435iconButton : String -> msg -> Html msg
381436iconButton name msg =
382437 button [ Attr.class "p-button is-inline is-dense has-icon", Ev.onClick msg ]
383438 [ i [ Attr.class ("p-icon--" ++ name) ] [] ]
384−
385−
386−hiddenButtonIcon : Bool -> String
387−hiddenButtonIcon hidden =
388− if hidden then
389− "show"
390−
391− else
392− "hide"
frontend/Pages/PasswordGenerator.elmnew file+158
@@ -0,0 +1,158 @@
1+module Pages.PasswordGenerator exposing (Model, Msg, init, update, view)
2+
3+import Bridge
4+import Html exposing (..)
5+import Html.Attributes as Attr
6+import Html.Events as Ev
7+import Switch exposing (switch)
8+
9+
10+type Msg
11+ = Noop
12+ | UpdateNumberCount Int
13+ | ToggleNumber Bool
14+ | ToggleAmbiguousCharacters Bool
15+ | ToggleCapitals Bool
16+ | UpdateNumberOfCapitals Int
17+ | UpdateLength Int
18+ | ToggleSpecialCharacters Bool
19+ | UpdateSpecialCharactersCount Int
20+
21+
22+type alias Model =
23+ { passwordConfig : Bridge.PasswordGeneratorConfig
24+ }
25+
26+
27+update : Msg -> Model -> Model
28+update msg { passwordConfig } =
29+ case msg of
30+ Noop ->
31+ { passwordConfig = passwordConfig }
32+
33+ UpdateNumberCount count ->
34+ { passwordConfig = { passwordConfig | minNumber = count } }
35+
36+ ToggleNumber enabled ->
37+ { passwordConfig = { passwordConfig | includeNumber = enabled } }
38+
39+ ToggleAmbiguousCharacters enabled ->
40+ { passwordConfig = { passwordConfig | ambiguous = enabled } }
41+
42+ ToggleCapitals enabled ->
43+ { passwordConfig = { passwordConfig | capitalize = enabled } }
44+
45+ UpdateNumberOfCapitals count ->
46+ { passwordConfig = { passwordConfig | minUppercase = count } }
47+
48+ UpdateLength count ->
49+ { passwordConfig = { passwordConfig | length = count } }
50+
51+ ToggleSpecialCharacters enabled ->
52+ { passwordConfig = { passwordConfig | special = enabled } }
53+
54+ UpdateSpecialCharactersCount count ->
55+ { passwordConfig = { passwordConfig | minSpecial = count } }
56+
57+
58+init : Model
59+init =
60+ { passwordConfig =
61+ { ambiguous = True
62+ , capitalize = True
63+ , includeNumber = True
64+ , length = 16
65+ , lowercase = True
66+ , minLowercase = 1
67+ , minNumber = 1
68+ , minSpecial = 1
69+ , minUppercase = 1
70+ , numWords = 1
71+ , number = True
72+ , special = True
73+ , type_ = "password"
74+ , uppercase = True
75+ , wordSeparator = "-"
76+ }
77+ }
78+
79+
80+view : Model -> List (Html Msg)
81+view { passwordConfig } =
82+ [ passwordGeneratorGroup
83+ [ switch
84+ { trigger = always Noop
85+ , label = "Length"
86+ , value = True
87+ , hidden = True
88+ }
89+ , input
90+ [ Attr.type_ "number"
91+ , Attr.class "is-dense"
92+ , Attr.value (String.fromInt passwordConfig.length)
93+ , Ev.onInput (String.toInt >> Maybe.withDefault 1 >> UpdateLength)
94+ , Attr.min "6"
95+ ]
96+ []
97+ ]
98+ , switchRow
99+ { label = "Numbers"
100+ , enabled = passwordConfig.includeNumber
101+ , toggle = ToggleNumber
102+ , trigger = UpdateNumberCount
103+ , value = passwordConfig.minNumber
104+ }
105+ , switchRow
106+ { label = "Uppercase Characters"
107+ , enabled = passwordConfig.capitalize
108+ , toggle = ToggleCapitals
109+ , trigger = UpdateNumberOfCapitals
110+ , value = passwordConfig.minUppercase
111+ }
112+ , switchRow
113+ { label = "Special Characters"
114+ , enabled = passwordConfig.special
115+ , toggle = ToggleSpecialCharacters
116+ , trigger = UpdateSpecialCharactersCount
117+ , value = passwordConfig.minSpecial
118+ }
119+ , switch
120+ { trigger = ToggleAmbiguousCharacters
121+ , label = "Ambiguous Characters"
122+ , value = passwordConfig.ambiguous
123+ , hidden = False
124+ }
125+ ]
126+
127+
128+switchRow :
129+ { toggle : Bool -> Msg
130+ , label : String
131+ , enabled : Bool
132+ , value : Int
133+ , trigger : Int -> Msg
134+ }
135+ -> Html Msg
136+switchRow { toggle, label, enabled, value, trigger } =
137+ passwordGeneratorGroup
138+ [ switch
139+ { trigger = toggle
140+ , label = label
141+ , value = enabled
142+ , hidden = False
143+ }
144+ , input
145+ [ Attr.type_ "number"
146+ , Attr.class "is-dense"
147+ , Attr.value (String.fromInt value)
148+ , Ev.onInput (String.toInt >> Maybe.withDefault 1 >> trigger)
149+ , Attr.min "1"
150+ , Attr.disabled (not enabled)
151+ ]
152+ []
153+ ]
154+
155+
156+passwordGeneratorGroup : List (Html msg) -> Html msg
157+passwordGeneratorGroup =
158+ div [ Attr.class "password-generator-group" ]
frontend/Switch.elmnew file+35
@@ -0,0 +1,35 @@
1+module Switch exposing (..)
2+
3+import Html exposing (..)
4+import Html.Attributes as Attr
5+import Html.Events as Ev
6+import Utils exposing (..)
7+
8+
9+type alias SwitchConfig msg =
10+ { trigger : Bool -> msg
11+ , label : String
12+ , value : Bool
13+ , hidden : Bool
14+ }
15+
16+
17+switch : SwitchConfig msg -> Html msg
18+switch cfg =
19+ label
20+ [ Attr.class "p-switch"
21+ ]
22+ [ input
23+ [ Attr.type_ "checkbox"
24+ , Attr.class "p-switch__input"
25+ , Ev.onCheck cfg.trigger
26+ , Attr.checked cfg.value
27+ ]
28+ []
29+ , span
30+ (Attr.class "p-switch__slider"
31+ :: optional cfg.hidden (Attr.style "visibility" "hidden")
32+ )
33+ []
34+ , span [ Attr.class "p-switch__label" ] [ text cfg.label ]
35+ ]
scratchpad/password_generator.htmlnew file+59
@@ -0,0 +1,59 @@
1+<!DOCTYPE html>
2+<html lang="en">
3+
4+<head>
5+ <meta charset="UTF-8">
6+ <meta http-equiv="X-UA-Compatible" content="IE=edge">
7+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
8+ <link rel="stylesheet" href="../vanilla/style.css">
9+ <link rel="stylesheet" href="../style.css">
10+ <title>Password generator</title>
11+
12+ <style>
13+ body {
14+ margin: 10px;
15+ }
16+
17+ </style>
18+
19+</head>
20+
21+<body>
22+
23+ <div class="password-generator-group">
24+ <p>Password length</p>
25+ <input type="number" class="p-slider__input">
26+ </div>
27+
28+
29+ <div class="password-generator-group">
30+ <label class="p-switch">
31+ <input type="checkbox" class="p-switch__input">
32+ <span class="p-switch__slider"></span>
33+ <span class="p-switch__label">Numbers</span>
34+ </label>
35+ <input type="number" class="p-slider__input">
36+ </div>
37+
38+
39+ <input type="range" min="0" max="100" value="50" step="1">
40+ <input type="range" min="0" max="100" value="50" step="1"
41+ style="background-image: -webkit-gradient(linear, 0% 0%, 100% 0%, color-stop(0.5, rgb(0, 102, 204)), color-stop(0.5, rgb(217, 217, 217)));">
42+ <h1>h</h1>
43+ <div class="p-slider__wrapper">
44+ <input type="range" min="0" max="100" value="50" step="1" id="slider4" disabled=""
45+ aria-label="Example of a disabled slider, range 0 to 100">
46+ <input class="p-slider__input" type="text" maxlength="3" id="slider4-input" disabled="" tabindex="0">
47+ </div>
48+ <div class="p-slider__wrapper">
49+ <input type="range" min="0" max="100" value="50" step="1" id="slider3" aria-label="Example slider, range 0 to 100">
50+ <input class="p-slider__input" type="number" maxlength="3" id="slider3-input" tabindex="0">
51+ </div>
52+
53+ <form>
54+ <input type="range" min="0" max="100" value="50" step="1">
55+ </form>
56+
57+</body>
58+
59+</html>
style.css+25−13
@@ -1,13 +1,11 @@
1−
21:root {
3− --color-light: #F7F7F7;
2+ --color-light: #f7f7f7;
43}
54
65main {
76 margin: 1rem;
87}
98
10−
119nav.p-tabs__list {
1210 top: 0;
1311 position: sticky;
@@ -28,13 +26,13 @@ nav h3 {
2826
2927@keyframes floating-notification-animation {
3028 from {
31− transform: translateY(-60px) ;
32− opacity: 0
29+ transform: translateY(-60px);
30+ opacity: 0;
3331 }
3432
3533 to {
3634 transform: none;
37− opacity: 1
35+ opacity: 1;
3836 }
3937}
4038
@@ -57,17 +55,17 @@ nav h3 {
5755 align-content: center;
5856}
5957
60−.spinner{
58+.spinner {
6159 padding: 1rem;
6260}
6361
6462.cipher-row-container {
65− display: flex;
66− width: 100%;
67− align-items: baseline;
68− justify-content: space-between;
69− flex-direction: row;
70− flex-wrap: wrap;
63+ display: flex;
64+ width: 100%;
65+ align-items: baseline;
66+ justify-content: space-between;
67+ flex-direction: row;
68+ flex-wrap: wrap;
7169}
7270
7371.cipher-row p {
@@ -95,3 +93,17 @@ p {
9593textarea {
9694 resize: none;
9795}
96+
97+.password-generator-group {
98+ display: flex;
99+ flex-direction: row;
100+ align-items: baseline;
101+ justify-content: space-between;
102+}
103+
104+.password-generator-group > input[type="number"] {
105+ width: 5%;
106+ margin-bottom: 0px;
107+ min-width: 5rem;
108+ text-align: center;
109+}