on master help sign in
Got password generator working
595331biko
.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
15import { I18nService } from "../../deps/bw/libs/shared/dist/src/services/i18n.service";
15import { I18nService } from "../../deps/bw/libs/shared/dist/src/services/i18n.service";
16import { StateService as BaseStateService } from "../../deps/bw/libs/shared/dist/src/services/state.service";
16import { StateService as BaseStateService } from "../../deps/bw/libs/shared/dist/src/services/state.service";
17import { PasswordTokenRequest } from "../../deps/bw/libs/shared/dist/src/models/request/identityToken/passwordTokenRequest";
17import { 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";
19import { CipherRequest } from "../../deps/bw/libs/common/src/models/request/cipherRequest";
19import { CipherRequest } from "../../deps/bw/libs/common/src/models/request/cipherRequest";
20import { Cipher } from "../../deps/bw/libs/common/src/models/domain/cipher";
20import { Cipher } from "../../deps/bw/libs/common/src/models/domain/cipher";
21import { CipherData } from "../../deps/bw/libs/common/src/models/data/cipherData";
21import { CipherData } from "../../deps/bw/libs/common/src/models/data/cipherData";
22+import { PasswordGenerationService } from "../../deps/bw/libs/common/src/services/passwordGeneration.service";
22
23
23function sanitize(obj) {
24function sanitize(obj) {
24 return JSON.parse(JSON.stringify(obj));
25 return JSON.parse(JSON.stringify(obj));
@@ -80,7 +81,8 @@ export function getServices() {
80 }
81 }
81 },
82 },
82 crypto: bg.cryptoService,
83 crypto: bg.cryptoService,
83− cryptoFunctions: bg.cryptoFunctionService
84+ cryptoFunctions: bg.cryptoFunctionService,
85+ passwordGeneration: bg.passwordGenerationService
84 }
86 }
85}
87}
86
88
@@ -162,6 +164,11 @@ class MainBackground {
162 this.appIdService,
164 this.appIdService,
163 (expired) => this.logout(expired)
165 (expired) => this.logout(expired)
164 );
166 );
167+ this.passwordGenerationService = new PasswordGenerationService(
168+ this.cryptoService,
169+ this.policyService,
170+ this.stateService
171+ );
165 }
172 }
166
173
167 async bootstrap() {
174 async bootstrap() {
backend/BW.purs+7
@@ -1,8 +1,10 @@
1module BW where
1module BW where
2
2
3import BW.Types
3import BW.Types
4+import Bridge
4import Prelude
5import Prelude
5
6
7+import Bridge as Bridge
6import Control.Promise (Promise)
8import Control.Promise (Promise)
7import Data.Argonaut (class DecodeJson, class EncodeJson, decodeJson, encodeJson)
9import Data.Argonaut (class DecodeJson, class EncodeJson, decodeJson, encodeJson)
8import Data.ArrayBuffer.Typed as AB
10import Data.ArrayBuffer.Typed as AB
@@ -34,6 +36,10 @@ type ApiService
34 , putCipher :: CipherResponse -> Promise CipherResponse
36 , putCipher :: CipherResponse -> Promise CipherResponse
35 }
37 }
36
38
39+type PasswordGeneration =
40+ { generatePassword :: Bridge.PasswordGeneratorConfig -> Promise String
41+ }
42+
37cryptoFunctionsTypeSha1 = "sha1" :: CryptoFunctionsType
43cryptoFunctionsTypeSha1 = "sha1" :: CryptoFunctionsType
38
44
39cryptoFunctionsTypeSha256 = "sha256" :: CryptoFunctionsType
45cryptoFunctionsTypeSha256 = "sha256" :: CryptoFunctionsType
@@ -77,6 +83,7 @@ type Services
77 = { crypto :: CryptoService
83 = { crypto :: CryptoService
78 , getApi :: Urls -> JNullable IdentityTokenResponse -> Promise ApiService
84 , getApi :: Urls -> JNullable IdentityTokenResponse -> Promise ApiService
79 , cryptoFunctions :: CryptoFunctions
85 , cryptoFunctions :: CryptoFunctions
86+ , passwordGeneration :: PasswordGeneration
80 }
87 }
81
88
82foreign import getServices :: Effect Services
89foreign import getServices :: Effect Services
backend/Bridge.purs+30
@@ -448,6 +448,7 @@ derive instance ordCmd_Login :: Ord Cmd_Login
448
448
449data Cmd =
449data Cmd =
450 Copy String
450 Copy String
451+ | GeneratePassword PasswordGeneratorConfig
451 | Init
452 | Init
452 | Login Cmd_Login
453 | Login Cmd_Login
453 | NeedCiphersList
454 | NeedCiphersList
@@ -484,6 +485,34 @@ derive instance genericFullCipher :: Generic FullCipher _
484derive instance eqFullCipher :: Eq FullCipher
485derive instance eqFullCipher :: Eq FullCipher
485derive instance ordFullCipher :: Ord FullCipher
486derive instance ordFullCipher :: Ord FullCipher
486
487
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+
487newtype Sub_LoadCiphers =
516newtype Sub_LoadCiphers =
488 Sub_LoadCiphers {
517 Sub_LoadCiphers {
489 cipherType :: CipherType
518 cipherType :: CipherType
@@ -532,6 +561,7 @@ data Sub =
532 CaptchaDone
561 CaptchaDone
533 | CipherChanged FullCipher
562 | CipherChanged FullCipher
534 | Error String
563 | Error String
564+ | GeneratedPassword String
535 | LoadCipher FullCipher
565 | LoadCipher FullCipher
536 | LoadCiphers Sub_LoadCiphers_List
566 | LoadCiphers Sub_LoadCiphers_List
537 | LoginSuccessful
567 | LoginSuccessful
backend/Main.purs+5
@@ -190,6 +190,11 @@ main = do
190 send $ Bridge.CipherChanged newCipher
190 send $ Bridge.CipherChanged newCipher
191 performSync
191 performSync
192 pure unit
192 pure unit
193+ Bridge.GeneratePassword cfg ->
194+ runWithDecryptionKey do
195+ password <- liftPromise $ services.passwordGeneration.generatePassword cfg
196+ send $ Bridge.GeneratedPassword password
197+ pure unit
193
198
194processCipher ::
199processCipher ::
195 forall r.
200 forall r.
bridge.yaml+19
@@ -17,6 +17,7 @@ Sub:
17- LoadCipher: FullCipher
17- LoadCipher: FullCipher
18- RecieveEmail: String
18- RecieveEmail: String
19- CipherChanged: FullCipher
19- CipherChanged: FullCipher
20+- GeneratedPassword: String
20
21
21Cmd:
22Cmd:
22- Init
23- Init
@@ -32,6 +33,7 @@ Cmd:
32- Copy: String
33- Copy: String
33- Open: String
34- Open: String
34- UpdateCipher: FullCipher
35- UpdateCipher: FullCipher
36+- GeneratePassword: PasswordGeneratorConfig
35
37
36FullCipher:
38FullCipher:
37 reprompt: Int
39 reprompt: Int
@@ -104,3 +106,20 @@ CipherType:
104- CardType
106- CardType
105- NoteType
107- NoteType
106- IdentityType
108- 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 =
502
502
503type Cmd =
503type Cmd =
504 Copy String
504 Copy String
505+ | GeneratePassword PasswordGeneratorConfig
505 | Init
506 | Init
506 | Login Cmd_Login
507 | Login Cmd_Login
507 | NeedCiphersList
508 | NeedCiphersList
@@ -516,6 +517,7 @@ jsonDecCmd : Json.Decode.Decoder ( Cmd )
516jsonDecCmd =
517jsonDecCmd =
517 let jsonDecDictCmd = Dict.fromList
518 let jsonDecDictCmd = Dict.fromList
518 [ ("Copy", Json.Decode.lazy (\_ -> Json.Decode.map Copy (Json.Decode.string)))
519 [ ("Copy", Json.Decode.lazy (\_ -> Json.Decode.map Copy (Json.Decode.string)))
520+ , ("GeneratePassword", Json.Decode.lazy (\_ -> Json.Decode.map GeneratePassword (jsonDecPasswordGeneratorConfig)))
519 , ("Init", Json.Decode.lazy (\_ -> Json.Decode.succeed Init))
521 , ("Init", Json.Decode.lazy (\_ -> Json.Decode.succeed Init))
520 , ("Login", Json.Decode.lazy (\_ -> Json.Decode.map Login (jsonDecCmd_Login)))
522 , ("Login", Json.Decode.lazy (\_ -> Json.Decode.map Login (jsonDecCmd_Login)))
521 , ("NeedCiphersList", Json.Decode.lazy (\_ -> Json.Decode.succeed NeedCiphersList))
523 , ("NeedCiphersList", Json.Decode.lazy (\_ -> Json.Decode.succeed NeedCiphersList))
@@ -533,6 +535,7 @@ jsonEncCmd : Cmd -> Value
533jsonEncCmd val =
535jsonEncCmd val =
534 let keyval v = case v of
536 let keyval v = case v of
535 Copy v1 -> ("Copy", encodeValue (Json.Encode.string v1))
537 Copy v1 -> ("Copy", encodeValue (Json.Encode.string v1))
538+ GeneratePassword v1 -> ("GeneratePassword", encodeValue (jsonEncPasswordGeneratorConfig v1))
536 Init -> ("Init", encodeValue (Json.Encode.list identity []))
539 Init -> ("Init", encodeValue (Json.Encode.list identity []))
537 Login v1 -> ("Login", encodeValue (jsonEncCmd_Login v1))
540 Login v1 -> ("Login", encodeValue (jsonEncCmd_Login v1))
538 NeedCiphersList -> ("NeedCiphersList", encodeValue (Json.Encode.list identity []))
541 NeedCiphersList -> ("NeedCiphersList", encodeValue (Json.Encode.list identity []))
@@ -575,6 +578,65 @@ jsonEncFullCipher val =
575
578
576
579
577
580
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+
578type alias Sub_LoadCiphers =
640type alias Sub_LoadCiphers =
579 { cipherType: CipherType
641 { cipherType: CipherType
580 , date: String
642 , date: String
@@ -636,6 +698,7 @@ type Sub =
636 CaptchaDone
698 CaptchaDone
637 | CipherChanged FullCipher
699 | CipherChanged FullCipher
638 | Error String
700 | Error String
701+ | GeneratedPassword String
639 | LoadCipher FullCipher
702 | LoadCipher FullCipher
640 | LoadCiphers Sub_LoadCiphers_List
703 | LoadCiphers Sub_LoadCiphers_List
641 | LoginSuccessful
704 | LoginSuccessful
@@ -651,6 +714,7 @@ jsonDecSub =
651 [ ("CaptchaDone", Json.Decode.lazy (\_ -> Json.Decode.succeed CaptchaDone))
714 [ ("CaptchaDone", Json.Decode.lazy (\_ -> Json.Decode.succeed CaptchaDone))
652 , ("CipherChanged", Json.Decode.lazy (\_ -> Json.Decode.map CipherChanged (jsonDecFullCipher)))
715 , ("CipherChanged", Json.Decode.lazy (\_ -> Json.Decode.map CipherChanged (jsonDecFullCipher)))
653 , ("Error", Json.Decode.lazy (\_ -> Json.Decode.map Error (Json.Decode.string)))
716 , ("Error", Json.Decode.lazy (\_ -> Json.Decode.map Error (Json.Decode.string)))
717+ , ("GeneratedPassword", Json.Decode.lazy (\_ -> Json.Decode.map GeneratedPassword (Json.Decode.string)))
654 , ("LoadCipher", Json.Decode.lazy (\_ -> Json.Decode.map LoadCipher (jsonDecFullCipher)))
718 , ("LoadCipher", Json.Decode.lazy (\_ -> Json.Decode.map LoadCipher (jsonDecFullCipher)))
655 , ("LoadCiphers", Json.Decode.lazy (\_ -> Json.Decode.map LoadCiphers (jsonDecSub_LoadCiphers_List)))
719 , ("LoadCiphers", Json.Decode.lazy (\_ -> Json.Decode.map LoadCiphers (jsonDecSub_LoadCiphers_List)))
656 , ("LoginSuccessful", Json.Decode.lazy (\_ -> Json.Decode.succeed LoginSuccessful))
720 , ("LoginSuccessful", Json.Decode.lazy (\_ -> Json.Decode.succeed LoginSuccessful))
@@ -669,6 +733,7 @@ jsonEncSub val =
669 CaptchaDone -> ("CaptchaDone", encodeValue (Json.Encode.list identity []))
733 CaptchaDone -> ("CaptchaDone", encodeValue (Json.Encode.list identity []))
670 CipherChanged v1 -> ("CipherChanged", encodeValue (jsonEncFullCipher v1))
734 CipherChanged v1 -> ("CipherChanged", encodeValue (jsonEncFullCipher v1))
671 Error v1 -> ("Error", encodeValue (Json.Encode.string v1))
735 Error v1 -> ("Error", encodeValue (Json.Encode.string v1))
736+ GeneratedPassword v1 -> ("GeneratedPassword", encodeValue (Json.Encode.string v1))
672 LoadCipher v1 -> ("LoadCipher", encodeValue (jsonEncFullCipher v1))
737 LoadCipher v1 -> ("LoadCipher", encodeValue (jsonEncFullCipher v1))
673 LoadCiphers v1 -> ("LoadCiphers", encodeValue (jsonEncSub_LoadCiphers_List v1))
738 LoadCiphers v1 -> ("LoadCiphers", encodeValue (jsonEncSub_LoadCiphers_List v1))
674 LoginSuccessful -> ("LoginSuccessful", encodeValue (Json.Encode.list identity []))
739 LoginSuccessful -> ("LoginSuccessful", encodeValue (Json.Encode.list identity []))
frontend/GlobalEvents.elm+1
@@ -5,3 +5,4 @@ import Bridge
5
5
6type Event
6type Event
7 = UpdateCipher Bridge.FullCipher
7 = UpdateCipher Bridge.FullCipher
8+ | GeneratedPassword String
frontend/Main.elm+8−1
@@ -59,6 +59,7 @@ type Msg
59 | EditCipher Bridge.FullCipher
59 | EditCipher Bridge.FullCipher
60 | UpdateCipher Bridge.FullCipher
60 | UpdateCipher Bridge.FullCipher
61 | FireGlobalEvent Event
61 | FireGlobalEvent Event
62+ | GeneratePassword Bridge.PasswordGeneratorConfig
62
63
63
64
64type alias Model =
65type alias Model =
@@ -217,6 +218,9 @@ subscriptions model =
217
218
218 Bridge.CipherChanged c ->
219 Bridge.CipherChanged c ->
219 ShowInfo "Entry updated" ("The entry \"" ++ c.name ++ "\" has been successfully updated.")
220 ShowInfo "Entry updated" ("The entry \"" ++ c.name ++ "\" has been successfully updated.")
221+
222+ Bridge.GeneratedPassword password ->
223+ GlobalEvents.GeneratedPassword password |> FireGlobalEvent
220 )
224 )
221 ]
225 ]
222 ++ optional (List.isEmpty model.notifications |> not) (Time.every 1000 (\t -> ClearNotification { currentTime = t }))
226 ++ optional (List.isEmpty model.notifications |> not) (Time.every 1000 (\t -> ClearNotification { currentTime = t }))
@@ -519,6 +523,9 @@ update msg model =
519 , Cmd.none
523 , Cmd.none
520 )
524 )
521
525
526+ GeneratePassword cfg ->
527+ ( model, FFI.sendBridge (Bridge.GeneratePassword cfg) )
528+
522
529
523loginCallbacks : Login.Callbacks Msg
530loginCallbacks : Login.Callbacks Msg
524loginCallbacks =
531loginCallbacks =
@@ -537,7 +544,7 @@ cipherCallbacks =
537
544
538editCipherCallbacks : EditCipher.Callbacks Msg
545editCipherCallbacks : EditCipher.Callbacks Msg
539editCipherCallbacks =
546editCipherCallbacks =
540− { save = UpdateCipher }
547+ { save = UpdateCipher, generatePassword = GeneratePassword }
541
548
542
549
543captchaCallbacks : Captcha.Callbacks
550captchaCallbacks : 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 =
53 else
53 else
54 model.cipher
54 model.cipher
55 }
55 }
56+
57+ _ ->
58+ model
56 }
59 }
57
60
58
61
frontend/Pages/Ciphers.elm+3
@@ -135,6 +135,9 @@ page callbacks liftMsg =
135 cipher
135 cipher
136 )
136 )
137 }
137 }
138+
139+ _ ->
140+ model
138 }
141 }
139
142
140
143
frontend/Pages/EditCipher.elm+84−38
@@ -5,21 +5,30 @@ import GlobalEvents
5import Html exposing (..)
5import Html exposing (..)
6import Html.Attributes as Attr
6import Html.Attributes as Attr
7import Html.Events as Ev
7import Html.Events as Ev
8+import Modal exposing (modal)
8import Page exposing (..)
9import Page exposing (..)
10+import Pages.PasswordGenerator as PasswordGenerator
9import Utils exposing (..)
11import Utils exposing (..)
10
12
11
13
12type alias Model =
14type alias Model =
13 { fullCipher : Bridge.FullCipher
15 { fullCipher : Bridge.FullCipher
16+ , passwordGenerator : Maybe PasswordGenerator.Model
14 }
17 }
15
18
16
19
17type Msg
20type Msg
18 = EditCipher Bridge.FullCipher
21 = EditCipher Bridge.FullCipher
22+ | PasswordGeneratorMsg PasswordGenerator.Msg
23+ | OpenGeneratePasword
24+ | CloseGeneratePassword
25+ | GeneratePassword Bridge.PasswordGeneratorConfig
19
26
20
27
21type alias Callbacks msg =
28type alias Callbacks msg =
22− { save : Bridge.FullCipher -> msg }
29+ { save : Bridge.FullCipher -> msg
30+ , generatePassword : Bridge.PasswordGeneratorConfig -> msg
31+ }
23
32
24
33
25page : Callbacks emsg -> Page Bridge.FullCipher Model Msg emsg
34page : Callbacks emsg -> Page Bridge.FullCipher Model Msg emsg
@@ -45,23 +54,60 @@ page callbacks liftMsg =
45 else
54 else
46 model.fullCipher
55 model.fullCipher
47 }
56 }
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+ }
48 }
77 }
49
78
50
79
51init : Bridge.FullCipher -> ( Model, Cmd Msg )
80init : Bridge.FullCipher -> ( Model, Cmd Msg )
52init cipher =
81init cipher =
53 ( { fullCipher = cipher
82 ( { fullCipher = cipher
83+ , passwordGenerator = Nothing
54 }
84 }
55 , Cmd.none
85 , Cmd.none
56 )
86 )
57
87
58
88
59update : Callbacks emsg -> (Msg -> emsg) -> Msg -> Model -> ( Result String Model, Cmd emsg )
89update : Callbacks emsg -> (Msg -> emsg) -> Msg -> Model -> ( Result String Model, Cmd emsg )
60−update _ _ msg model =
90+update { generatePassword } _ msg model =
61 case msg of
91 case msg of
62 EditCipher fullCipher ->
92 EditCipher fullCipher ->
63 ( Ok { model | fullCipher = fullCipher }, Cmd.none )
93 ( Ok { model | fullCipher = fullCipher }, Cmd.none )
64
94
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+
65
111
66subscriptions : Model -> Sub Msg
112subscriptions : Model -> Sub Msg
67subscriptions _ =
113subscriptions _ =
@@ -83,18 +129,34 @@ row { name, nameIcon, attrs } =
83
129
84
130
85view : Model -> List (Html Msg)
131view : Model -> List (Html Msg)
86−view { fullCipher } =
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+ )
87− [ row
149+ ++ [ row
88− { name = "Entry name"
150+ { name = "Entry name"
89− , nameIcon = ""
151+ , nameIcon = ""
90− , attrs =
152+ , attrs =
91− [ Attr.type_ "text"
153+ [ Attr.type_ "text"
92− , Attr.value fullCipher.name
154+ , Attr.value fullCipher.name
93− , Ev.onInput (\x -> { fullCipher | name = x } |> EditCipher)
155+ , Ev.onInput (\x -> { fullCipher | name = x } |> EditCipher)
94− , Attr.attribute "autocomplete" "off"
156+ , Attr.attribute "autocomplete" "off"
95− ]
157+ ]
96− }
158+ }
97− ]
159+ ]
98 ++ (case fullCipher.cipher of
160 ++ (case fullCipher.cipher of
99 Bridge.LoginCipher cipher ->
161 Bridge.LoginCipher cipher ->
100 let
162 let
@@ -114,16 +176,19 @@ view { fullCipher } =
114 , Attr.attribute "autocomplete" "username"
176 , Attr.attribute "autocomplete" "username"
115 ]
177 ]
116 }
178 }
117− , row
179+ , p []
118− { name = "Password"
119− , nameIcon = "security"
180+ [ heading "security" "Password"
120− , attrs =
181+ , br [] []
182+ , input
121 [ Attr.type_ "text"
183 [ Attr.type_ "text"
122 , Attr.value (Maybe.withDefault "" password)
184 , Attr.value (Maybe.withDefault "" password)
123 , Ev.onInput (\x -> edit { cipher | password = Just x })
185 , Ev.onInput (\x -> edit { cipher | password = Just x })
124 , Attr.attribute "autocomplete" "new-password"
186 , Attr.attribute "autocomplete" "new-password"
187+ , Attr.style "width" "calc(100% - 3.5rem)"
125 ]
188 ]
126− }
189+ []
190+ , iconButton "restart" OpenGeneratePasword
191+ ]
127 ]
192 ]
128 ++ (uris
193 ++ (uris
129 |> List.indexedMap
194 |> List.indexedMap
@@ -367,26 +432,7 @@ view { fullCipher } =
367 )
432 )
368
433
369
434
370−hiddenPassword : String
371−hiddenPassword =
372− String.repeat 12 "●"
373−
374−
375−hiddenCvv : String
376−hiddenCvv =
377− String.repeat 3 "●"
378−
379−
380iconButton : String -> msg -> Html msg
435iconButton : String -> msg -> Html msg
381iconButton name msg =
436iconButton name msg =
382 button [ Attr.class "p-button is-inline is-dense has-icon", Ev.onClick msg ]
437 button [ Attr.class "p-button is-inline is-dense has-icon", Ev.onClick msg ]
383 [ i [ Attr.class ("p-icon--" ++ name) ] [] ]
438 [ 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−
2:root {
1:root {
3− --color-light: #F7F7F7;
2+ --color-light: #f7f7f7;
4}
3}
5
4
6main {
5main {
7 margin: 1rem;
6 margin: 1rem;
8}
7}
9
8
10−
11nav.p-tabs__list {
9nav.p-tabs__list {
12 top: 0;
10 top: 0;
13 position: sticky;
11 position: sticky;
@@ -28,13 +26,13 @@ nav h3 {
28
26
29@keyframes floating-notification-animation {
27@keyframes floating-notification-animation {
30 from {
28 from {
31− transform: translateY(-60px) ;
29+ transform: translateY(-60px);
32− opacity: 0
30+ opacity: 0;
33 }
31 }
34
32
35 to {
33 to {
36 transform: none;
34 transform: none;
37− opacity: 1
35+ opacity: 1;
38 }
36 }
39}
37}
40
38
@@ -57,17 +55,17 @@ nav h3 {
57 align-content: center;
55 align-content: center;
58}
56}
59
57
60−.spinner{
58+.spinner {
61 padding: 1rem;
59 padding: 1rem;
62}
60}
63
61
64.cipher-row-container {
62.cipher-row-container {
65− display: flex;
63+ display: flex;
66− width: 100%;
64+ width: 100%;
67− align-items: baseline;
65+ align-items: baseline;
68− justify-content: space-between;
66+ justify-content: space-between;
69− flex-direction: row;
67+ flex-direction: row;
70− flex-wrap: wrap;
68+ flex-wrap: wrap;
71}
69}
72
70
73.cipher-row p {
71.cipher-row p {
@@ -95,3 +93,17 @@ p {
95textarea {
93textarea {
96 resize: none;
94 resize: none;
97}
95}
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+}