on master help sign in
2FA login (#15)

* Wip * wip * wip * login seems to work * Auto-select single 2fa provider and don't send email twice * Got individual pages running * Fixed 2FA provider button centering * got rid of the page abstraction * Cleaned things up a bit * Cleaned things up yet more * Removed unused messages * Handle unsupported 2FA providers Co-authored-by: iko <ilyakooo0>

commit1c22324a499bad7d264f03a6ddf8f6d048561f30 authoriko <ilyakooo0@gmail.com> dateSat 5 Nov 2022, 12:54 (4 years ago) parent15f8ccb
29 files changed+1424−682
.gitignore+1 backend/BW.js+6−1 backend/BW.purs+2−1 backend/BW/Logic.purs+48−19 backend/BW/Types.purs+60−1 backend/Bridge.purs+80−1 backend/FFI.js+16−1 backend/Main.purs+52−8 bridge.yaml+116−94 dev/elm-live.sh+1−1 dev/generate-bridge.sh+1−1 dev/start-chrome.sh+5−1 elm.json+1 frontend/Bridge.elm+115−3 frontend/Logic/SecondFactor.elm+31 frontend/Main.elm+278−186 frontend/Page.elm+20−17 frontend/Pages/Captcha.elm+7−33 frontend/Pages/Cipher.elm+62−51 frontend/Pages/Ciphers.elm+123−128 frontend/Pages/EditCipher.elm+61−61 frontend/Pages/Login.elm+38−42 frontend/Pages/MasterPassword.elm+19−30 frontend/Pages/SecondFactor.elm+91 frontend/Pages/SecondFactorSelect.elm+163 scratchpad/password_generator.html+16−1 spago.dhall+3 style.css+7 vanilla+1−1
.gitignore+1
@@ -10,3 +10,4 @@ output
1010backend.js
1111elm.js
1212output-es
13+elm-stuff
backend/BW.js+6−1
@@ -73,7 +73,11 @@ export function getServices() {
7373 req.email,
7474 req.masterPasswordHash,
7575 req.captchaResponse,
76− new TokenRequestTwoFactor(req.twoFactor),
76+ new TokenRequestTwoFactor(
77+ req.twoFactor.provider,
78+ req.twoFactor.token,
79+ req.twoFactor.remember
80+ ),
7781 req.device
7882 )
7983 ),
@@ -81,6 +85,7 @@ export function getServices() {
8185 postCipherCreate: (cipher) => api.postCipherCreate(makeCipherCreateRequest(cipher)),
8286 putCipher: (cipher) => api.putCipher(cipher.id, makeCipherRequest(cipher)),
8387 deleteCipher: (id) => api.deleteCipher(id),
88+ postTwoFactorEmail: (req) => api.postTwoFactorEmail(req),
8489 }
8590 },
8691 crypto: bg.cryptoService,
backend/BW.purs+2−1
@@ -35,11 +35,12 @@ type ApiService
3535 = { postPrelogin :: PreloginRequest -> Promise PreloginResponse
3636 , getProfile :: Unit -> Promise ProfileResponse
3737 -- TODO: There can really be more request and response types. Maybe handle this later.
38− , postIdentityToken :: PasswordTokenRequest -> Promise (IdentityCaptchaResponse |+| IdentityTokenResponse)
38+ , postIdentityToken :: PasswordTokenRequest -> Promise (IdentityCaptchaResponse |+| IdentityTwoFactorResponse |+| IdentityTokenResponse)
3939 , getSync :: Unit -> Promise SyncResponse
4040 , postCipherCreate :: CipherResponse -> Promise CipherResponse
4141 , putCipher :: CipherResponse -> Promise CipherResponse
4242 , deleteCipher :: String -> Promise Unit
43+ , postTwoFactorEmail :: TwoFactorEmailRequest -> Promise Unit
4344 }
4445
4546type PasswordGeneration =
backend/BW/Logic.purs+48−19
@@ -12,7 +12,7 @@ import Data.Function.Uncurried (runFn2, runFn3, runFn4)
1212import Data.JNullable (JNullable, fromJNullable, jnull, nullify)
1313import Data.JNullable as JNullable
1414import Data.JOpt (JOpt(..), fromJOpt)
15−import Data.Maybe (Maybe(..), fromMaybe)
15+import Data.Maybe (Maybe(..), fromMaybe, maybe)
1616import Data.Newtype (class Newtype, unwrap, wrap)
1717import Data.String as String
1818import Data.SymmetricCryptoKey (SymmetricCryptoKey)
@@ -80,10 +80,12 @@ makeDecryptionKey masterKey str = do
8080
8181getLogInRequestToken ::
8282 forall r.
83− PreloginResponse ->
84− Email ->
85− Password ->
86− Maybe String ->
83+ { prelogin :: PreloginResponse
84+ , email :: Email
85+ , password :: Password
86+ , captchaResponse :: Maybe String
87+ , secondFactor :: Bridge.Cmd_Login_secondFactor_Maybe
88+ } ->
8789 Run
8890 ( api :: Reader ApiService
8991 , crypto :: Reader CryptoService
@@ -91,25 +93,32 @@ getLogInRequestToken ::
9193 , aff :: Aff
9294 | r
9395 )
94− (IdentityCaptchaResponse |+| IdentityTokenResponse)
95−getLogInRequestToken prelogin email password captchaResponse = do
96− key <- makePreloginKey prelogin email password
97− crypto <- askAt (Proxy :: _ "crypto")
96+ (IdentityCaptchaResponse |+| IdentityTwoFactorResponse |+| IdentityTokenResponse)
97+getLogInRequestToken { prelogin
98+, email
99+, password
100+, captchaResponse
101+, secondFactor: Bridge.Cmd_Login_secondFactor_Maybe secondFactor
102+} = do
103+ let
104+ twoFactor =
105+ maybe
106+ { provider: twoFactorProviderTypeEmail
107+ , token: ""
108+ , remember: false
109+ }
110+ ( \(Bridge.Cmd_Login_secondFactor x@{ provider }) ->
111+ x { provider = bridgeToSecondFactorType provider }
112+ )
113+ secondFactor
114+ StringHash hashedPassword <- bwPasswordStringHash prelogin email password
98115 api :: ApiService <- askAt (Proxy :: _ "api")
99− _localHashedPassword <-
100− liftPromise
101− $ runFn3 crypto.hashPassword password key (nullify hashPurposeLocalAuthorization)
102− StringHash hashedPassword <- liftPromise $ runFn3 crypto.hashPassword password key jnull
103116 liftPromise
104117 $ api.postIdentityToken
105118 { email: email
106119 , masterPasswordHash: hashedPassword
107120 , captchaResponse: fromMaybe "" captchaResponse
108− , twoFactor:
109− { provider: twoFactorProviderTypeEmail
110− , token: ""
111− , remember: false
112− }
121+ , twoFactor
113122 , device:
114123 { type: deviceTypeUnknownBrowser
115124 , name: "Temporary device name"
@@ -118,6 +127,26 @@ getLogInRequestToken prelogin email password captchaResponse = do
118127 }
119128 }
120129
130+bwPasswordStringHash ::
131+ forall r.
132+ PreloginResponse ->
133+ Email ->
134+ Password ->
135+ Run
136+ ( crypto :: Reader CryptoService
137+ , effect :: Effect
138+ , aff :: Aff
139+ | r
140+ )
141+ StringHash
142+bwPasswordStringHash prelogin email password = do
143+ key <- makePreloginKey prelogin email password
144+ crypto <- askAt (Proxy :: _ "crypto")
145+ _localHashedPassword <-
146+ liftPromise
147+ $ runFn3 crypto.hashPassword password key (nullify hashPurposeLocalAuthorization)
148+ liftPromise $ runFn3 crypto.hashPassword password key jnull
149+
121150hashPassword ::
122151 forall r.
123152 Password ->
@@ -164,7 +193,7 @@ encrypt input = do
164193 key <- askAt (Proxy :: _ "key")
165194 map EncString.toString $ liftPromise $ runFn2 crypto.encrypt input key
166195
167−liftPromise ∷ forall m a. MonadAff m ⇒ Promise a → m a
196+liftPromise :: forall m a. MonadAff m => Promise a -> m a
168197liftPromise = liftAff <<< Promise.toAff
169198
170199encodeCipher ::
backend/BW/Types.purs+60−1
@@ -2,11 +2,21 @@ module BW.Types where
22
33import Prelude
44
5+import Bridge as Bridge
56import Data.Argonaut (class DecodeJson, class EncodeJson)
67import Data.JNullable (JNullable)
78import Data.JOpt (JOpt)
9+import Data.Maybe (Maybe(..))
810import Data.ShowableJson (ShowableJson)
911import Data.Timestamp (Timestamp)
12+import Data.Traversable (traverse)
13+import Foreign (Foreign)
14+import Foreign as Foreign
15+import Foreign.Object (Object)
16+import Literals.Undefined (Undefined)
17+import Type.Prelude (Proxy(..))
18+import Untagged.TypeCheck (class HasRuntimeType, hasRuntimeType)
19+import Untagged.Union (type (|+|))
1020
1121newtype EncryptedString
1222 = EncryptedString String
@@ -28,7 +38,7 @@ type PreloginResponse
2838 }
2939
3040type PreloginRequest
31− = { email :: String
41+ = { email :: Email
3242 }
3343
3444type ProfileResponse
@@ -307,6 +317,38 @@ twoFactorProviderTypeWebAuthn = 7 :: TwoFactorProviderType
307317type TwoFactorProviderType
308318 = Int
309319
320+secondFactorTypeToBridge ::
321+ TwoFactorProviderType ->
322+ Maybe Bridge.TwoFactorProviderType
323+secondFactorTypeToBridge x = case x of
324+ n
325+ | n == twoFactorProviderTypeAuthenticator -> Just Bridge.Authenticator
326+ n
327+ | n == twoFactorProviderTypeEmail -> Just Bridge.Email
328+ n
329+ | n == twoFactorProviderTypeDuo -> Just Bridge.Duo
330+ n
331+ | n == twoFactorProviderTypeYubikey -> Just Bridge.Yubikey
332+ n
333+ | n == twoFactorProviderTypeU2f -> Just Bridge.U2f
334+ n
335+ | n == twoFactorProviderTypeRemember -> Just Bridge.Remember
336+ n
337+ | n == twoFactorProviderTypeOrganizationDuo -> Just Bridge.OrganizationDuo
338+ n
339+ | n == twoFactorProviderTypeWebAuthn -> Just Bridge.WebAuthn
340+ _ -> Nothing
341+
342+bridgeToSecondFactorType :: Bridge.TwoFactorProviderType -> TwoFactorProviderType
343+bridgeToSecondFactorType Bridge.Authenticator = twoFactorProviderTypeAuthenticator
344+bridgeToSecondFactorType Bridge.Email = twoFactorProviderTypeEmail
345+bridgeToSecondFactorType Bridge.Duo = twoFactorProviderTypeDuo
346+bridgeToSecondFactorType Bridge.Yubikey = twoFactorProviderTypeYubikey
347+bridgeToSecondFactorType Bridge.U2f = twoFactorProviderTypeU2f
348+bridgeToSecondFactorType Bridge.Remember = twoFactorProviderTypeRemember
349+bridgeToSecondFactorType Bridge.OrganizationDuo = twoFactorProviderTypeOrganizationDuo
350+bridgeToSecondFactorType Bridge.WebAuthn = twoFactorProviderTypeWebAuthn
351+
310352newtype Password
311353 = Password String
312354
@@ -358,6 +400,18 @@ type IdentityTokenResponse
358400 , keyConnectorUrl :: JNullable String
359401 }
360402
403+newtype TwoFactorProviderTypes = TwoFactorProviderTypes (Array TwoFactorProviderType)
404+
405+instance HasRuntimeType TwoFactorProviderTypes where
406+ hasRuntimeType Proxy = Foreign.isArray
407+
408+type IdentityTwoFactorResponse =
409+ { twoFactorProviders :: TwoFactorProviderTypes
410+ -- it is a Map type and untaged unions gets confused
411+ -- , twoFactorProviders2 :: Object (Object String)
412+ , captchaToken :: String |+| Undefined
413+ }
414+
361415newtype Email
362416 = Email String
363417
@@ -669,3 +723,8 @@ type SendTextApi
669723 = { text :: JNullable String
670724 , hidden :: Boolean
671725 }
726+
727+type TwoFactorEmailRequest =
728+ { email :: Email
729+ , masterPasswordHash :: StringHash
730+ }
backend/Bridge.purs+80−1
@@ -443,10 +443,57 @@ derive instance genericCipherType :: Generic CipherType _
443443derive instance eqCipherType :: Eq CipherType
444444derive instance ordCipherType :: Ord CipherType
445445
446+newtype Cmd_ChooseSecondFactor =
447+ Cmd_ChooseSecondFactor {
448+ email :: String
449+ , factor :: TwoFactorProviderType
450+ , password :: String
451+ , requestFromServer :: Boolean
452+ , server :: String
453+ }
454+
455+derive instance newtypeCmd_ChooseSecondFactor :: Newtype Cmd_ChooseSecondFactor _
456+instance encodeJsonCmd_ChooseSecondFactor :: EncodeJson Cmd_ChooseSecondFactor where
457+ encodeJson = genericEncodeAeson Argonaut.defaultOptions
458+instance decodeJsonCmd_ChooseSecondFactor :: DecodeJson Cmd_ChooseSecondFactor where
459+ decodeJson = genericDecodeAeson Argonaut.defaultOptions
460+derive instance genericCmd_ChooseSecondFactor :: Generic Cmd_ChooseSecondFactor _
461+derive instance eqCmd_ChooseSecondFactor :: Eq Cmd_ChooseSecondFactor
462+derive instance ordCmd_ChooseSecondFactor :: Ord Cmd_ChooseSecondFactor
463+
464+newtype Cmd_Login_secondFactor =
465+ Cmd_Login_secondFactor {
466+ provider :: TwoFactorProviderType
467+ , remember :: Boolean
468+ , token :: String
469+ }
470+
471+derive instance newtypeCmd_Login_secondFactor :: Newtype Cmd_Login_secondFactor _
472+instance encodeJsonCmd_Login_secondFactor :: EncodeJson Cmd_Login_secondFactor where
473+ encodeJson = genericEncodeAeson Argonaut.defaultOptions
474+instance decodeJsonCmd_Login_secondFactor :: DecodeJson Cmd_Login_secondFactor where
475+ decodeJson = genericDecodeAeson Argonaut.defaultOptions
476+derive instance genericCmd_Login_secondFactor :: Generic Cmd_Login_secondFactor _
477+derive instance eqCmd_Login_secondFactor :: Eq Cmd_Login_secondFactor
478+derive instance ordCmd_Login_secondFactor :: Ord Cmd_Login_secondFactor
479+
480+newtype Cmd_Login_secondFactor_Maybe =
481+ Cmd_Login_secondFactor_Maybe (Maybe Cmd_Login_secondFactor)
482+
483+derive instance newtypeCmd_Login_secondFactor_Maybe :: Newtype Cmd_Login_secondFactor_Maybe _
484+instance encodeJsonCmd_Login_secondFactor_Maybe :: EncodeJson Cmd_Login_secondFactor_Maybe where
485+ encodeJson = genericEncodeAeson Argonaut.defaultOptions
486+instance decodeJsonCmd_Login_secondFactor_Maybe :: DecodeJson Cmd_Login_secondFactor_Maybe where
487+ decodeJson = genericDecodeAeson Argonaut.defaultOptions
488+derive instance genericCmd_Login_secondFactor_Maybe :: Generic Cmd_Login_secondFactor_Maybe _
489+derive instance eqCmd_Login_secondFactor_Maybe :: Eq Cmd_Login_secondFactor_Maybe
490+derive instance ordCmd_Login_secondFactor_Maybe :: Ord Cmd_Login_secondFactor_Maybe
491+
446492newtype Cmd_Login =
447493 Cmd_Login {
448494 email :: String
449495 , password :: String
496+ , secondFactor :: Cmd_Login_secondFactor_Maybe
450497 , server :: String
451498 }
452499
@@ -460,7 +507,8 @@ derive instance eqCmd_Login :: Eq Cmd_Login
460507derive instance ordCmd_Login :: Ord Cmd_Login
461508
462509data Cmd =
463− Copy String
510+ ChooseSecondFactor Cmd_ChooseSecondFactor
511+ | Copy String
464512 | CreateCipher FullCipher
465513 | DeleteCipher FullCipher
466514 | GeneratePassword PasswordGeneratorConfig
@@ -573,6 +621,18 @@ derive instance genericSub_NeedsMasterPassword :: Generic Sub_NeedsMasterPasswor
573621derive instance eqSub_NeedsMasterPassword :: Eq Sub_NeedsMasterPassword
574622derive instance ordSub_NeedsMasterPassword :: Ord Sub_NeedsMasterPassword
575623
624+newtype Sub_NeedsSecondFactor_List =
625+ Sub_NeedsSecondFactor_List (Array TwoFactorProviderType)
626+
627+derive instance newtypeSub_NeedsSecondFactor_List :: Newtype Sub_NeedsSecondFactor_List _
628+instance encodeJsonSub_NeedsSecondFactor_List :: EncodeJson Sub_NeedsSecondFactor_List where
629+ encodeJson = genericEncodeAeson Argonaut.defaultOptions
630+instance decodeJsonSub_NeedsSecondFactor_List :: DecodeJson Sub_NeedsSecondFactor_List where
631+ decodeJson = genericDecodeAeson Argonaut.defaultOptions
632+derive instance genericSub_NeedsSecondFactor_List :: Generic Sub_NeedsSecondFactor_List _
633+derive instance eqSub_NeedsSecondFactor_List :: Eq Sub_NeedsSecondFactor_List
634+derive instance ordSub_NeedsSecondFactor_List :: Ord Sub_NeedsSecondFactor_List
635+
576636newtype Sub_Totp =
577637 Sub_Totp {
578638 code :: String
@@ -601,6 +661,7 @@ data Sub =
601661 | NeedsCaptcha String
602662 | NeedsLogin
603663 | NeedsMasterPassword Sub_NeedsMasterPassword
664+ | NeedsSecondFactor Sub_NeedsSecondFactor_List
604665 | RecieveEmail String
605666 | Reset
606667 | Totp Sub_Totp
@@ -614,3 +675,21 @@ derive instance genericSub :: Generic Sub _
614675derive instance eqSub :: Eq Sub
615676derive instance ordSub :: Ord Sub
616677
678+data TwoFactorProviderType =
679+ Authenticator
680+ | Duo
681+ | Email
682+ | OrganizationDuo
683+ | Remember
684+ | U2f
685+ | WebAuthn
686+ | Yubikey
687+
688+instance encodeJsonTwoFactorProviderType :: EncodeJson TwoFactorProviderType where
689+ encodeJson = genericEncodeAeson Argonaut.defaultOptions
690+instance decodeJsonTwoFactorProviderType :: DecodeJson TwoFactorProviderType where
691+ decodeJson = genericDecodeAeson Argonaut.defaultOptions
692+derive instance genericTwoFactorProviderType :: Generic TwoFactorProviderType _
693+derive instance eqTwoFactorProviderType :: Eq TwoFactorProviderType
694+derive instance ordTwoFactorProviderType :: Ord TwoFactorProviderType
695+
backend/FFI.js+16−1
@@ -1,6 +1,21 @@
1+function searchForInit(obj) {
2+ if (obj.init) {
3+ return obj.init
4+ } else {
5+ for (let key in obj) {
6+ if (obj.hasOwnProperty(key)) {
7+ const result = searchForInit(obj[key])
8+ if (result) {
9+ return result
10+ }
11+ }
12+ }
13+ }
14+}
15+
116export function startElm(element) {
217 return function () {
3− return Elm.Main.init({
18+ return searchForInit(Elm)({
419 node: document.getElementById(element)
520 })
621 }
backend/Main.purs+52−8
@@ -5,11 +5,12 @@ module Main
55import Prelude
66import BW (ApiService, CryptoFunctions, Services, CryptoService)
77import BW as WB
8−import BW.Logic (decodeCipher, decrypt, encodeCipher, hashPassword, liftPromise)
8+import BW.Logic (bwPasswordStringHash, decodeCipher, decrypt, encodeCipher, hashPassword, liftPromise)
99import BW.Logic as Logic
10−import BW.Types (CipherResponse, Email(..), Password(..), Urls, SyncResponse, cipherTypeCard, cipherTypeIdentity, cipherTypeLogin, cipherTypeSecureNote)
10+import BW.Types (CipherResponse, Email(..), Password(..), TwoFactorProviderTypes(..), Urls, cipherTypeCard, cipherTypeIdentity, cipherTypeLogin, cipherTypeSecureNote, secondFactorTypeToBridge)
1111import Bridge as Bridge
1212import Control.Monad.Error.Class (catchError, throwError)
13+import Control.Monad.Error.Class as Error
1314import Data.Argonaut (class DecodeJson)
1415import Data.Array as Array
1516import Data.Clipboard as Clipboard
@@ -104,8 +105,10 @@ main = do
104105 runReaderAt (Proxy :: _ "key") key $ act
105106 Nothing -> requestMasterPassword
106107 Elm.subscribe app \cmd -> case cmd of
107− Bridge.Login (Bridge.Cmd_Login { email, server, password }) -> do
108− captchaToken <- Ref.read hCaptchaTokenRef
108+ Bridge.Login (Bridge.Cmd_Login { email: email', server, password, secondFactor }) -> do
109+ let
110+ email = Email email'
111+ captchaResponse <- Ref.read hCaptchaTokenRef
109112 Ref.write Nothing hCaptchaTokenRef
110113 let
111114 urls = baseUrl server
@@ -119,15 +122,21 @@ main = do
119122 ( do
120123 resp <-
121124 runBaseAff' $ run $ runReaderAt (Proxy :: _ "api") unauthedApi
122− $ Logic.getLogInRequestToken prelogin (Email email) (Password password) captchaToken
125+ $ Logic.getLogInRequestToken
126+ { prelogin
127+ , email
128+ , password: Password password
129+ , captchaResponse
130+ , secondFactor
131+ }
123132 pure $ Right resp
124133 )
125134 (const $ pure $ Left unit)
126− case map toEither1 loginResponse of
135+ case map (toEither1 >>> map toEither1) $ loginResponse of
127136 Left _ -> send Bridge.WrongPassword
128137 Right (Left { siteKey }) -> send $ Bridge.NeedsCaptcha siteKey
129− Right (Right token) -> do
130− masterKey <- Logic.makePreloginKey prelogin (Email email) (Password password)
138+ Right (Right (Right token)) -> do
139+ masterKey <- Logic.makePreloginKey prelogin email (Password password)
131140 api <- liftPromise $ services.getApi urls (nullify token)
132141 sync <- liftPromise $ api.getSync unit
133142 hash <- hashPassword (Password password)
@@ -140,6 +149,21 @@ main = do
140149 Storage.store storage SyncKey sync
141150 Storage.store storage TokenKey token
142151 send Bridge.LoginSuccessful
152+ Right (Right (Left { twoFactorProviders: TwoFactorProviderTypes rawProviders, captchaToken })) -> do
153+ case toEither1 captchaToken of
154+ Left captchaTokenString ->
155+ liftEffect
156+ $ Ref.write (Just captchaTokenString) hCaptchaTokenRef
157+ Right _ -> pure unit
158+ providers <-
159+ liftEffect
160+ $ traverse
161+ ( \x ->
162+ secondFactorTypeToBridge x
163+ # Error.liftMaybe (error $ "Unknown second factor provider: " <> show x)
164+ )
165+ rawProviders
166+ send $ Bridge.NeedsSecondFactor $ Bridge.Sub_NeedsSecondFactor_List providers
143167 Bridge.NeedCiphersList ->
144168 runWithDecryptionKey do
145169 sendCiphers
@@ -224,6 +248,26 @@ main = do
224248 { interval, code, source: totp
225249 }
226250 pure unit
251+ Bridge.ChooseSecondFactor
252+ ( Bridge.Cmd_ChooseSecondFactor
253+ { email: email', factor, server, password, requestFromServer
254+ }
255+ ) ->
256+ when requestFromServer
257+ let
258+ email = Email email'
259+ in
260+ case factor of
261+ Bridge.Email ->
262+ runAff do
263+ let
264+ urls = baseUrl server
265+ unauthedApi <- liftPromise $ services.getApi urls jnull
266+ prelogin <- liftPromise $ unauthedApi.postPrelogin { email }
267+ masterPasswordHash <- bwPasswordStringHash prelogin email (Password password)
268+ liftPromise $ unauthedApi.postTwoFactorEmail { email, masterPasswordHash }
269+ pure unit
270+ _ -> pure unit
227271
228272processCipher ::
229273 forall r.
bridge.yaml+116−94
@@ -1,136 +1,158 @@
11Sub:
2−- NeedsLogin
3−- NeedsCaptcha: String
4−- CaptchaDone
5−- Error: String
6−- LoadCiphers:
7− List:
2+ - NeedsLogin
3+ - NeedsCaptcha: String
4+ - NeedsSecondFactor: { List: TwoFactorProviderType }
5+ - CaptchaDone
6+ - Error: String
7+ - LoadCiphers:
8+ List:
89 name: String
910 date: String
1011 id: String
1112 cipherType: CipherType
12−- LoginSuccessful
13−- WrongPassword
14−- NeedsMasterPassword:
15− server: String
16− login: String
17−- Reset
18−- LoadCipher: FullCipher
19−- RecieveEmail: String
20−- CipherChanged: FullCipher
21−- GeneratedPassword: String
22−- CipherDeleted: FullCipher
23−- Totp:
24− source: String
25− code: String
26− interval: Int
13+ - LoginSuccessful
14+ - WrongPassword
15+ - NeedsMasterPassword:
16+ server: String
17+ login: String
18+ - Reset
19+ - LoadCipher: FullCipher
20+ - RecieveEmail: String
21+ - CipherChanged: FullCipher
22+ - GeneratedPassword: String
23+ - CipherDeleted: FullCipher
24+ - Totp:
25+ source: String
26+ code: String
27+ interval: Int
2728
2829Cmd:
29−- Init
30−- Login:
31− email: String
32− server: String
33− password: String
34−- NeedCiphersList
35−- SendMasterPassword: String
36−- NeedsReset
37−- RequestCipher: String
38−- NeedEmail
39−- Copy: String
40−- Open: String
41−- UpdateCipher: FullCipher
42−- CreateCipher: FullCipher
43−- GeneratePassword: PasswordGeneratorConfig
44−- DeleteCipher: FullCipher
45−- RequestTotp: String
30+ - Init
31+ - Login:
32+ email: String
33+ server: String
34+ password: String
35+ secondFactor:
36+ Maybe:
37+ provider: TwoFactorProviderType
38+ token: String
39+ remember: Bool
40+ - ChooseSecondFactor:
41+ factor: TwoFactorProviderType
42+ email: String
43+ server: String
44+ password: String
45+ requestFromServer: Bool
46+ - NeedCiphersList
47+ - SendMasterPassword: String
48+ - NeedsReset
49+ - RequestCipher: String
50+ - NeedEmail
51+ - Copy: String
52+ - Open: String
53+ - UpdateCipher: FullCipher
54+ - CreateCipher: FullCipher
55+ - GeneratePassword: PasswordGeneratorConfig
56+ - DeleteCipher: FullCipher
57+ - RequestTotp: String
4658
4759FullCipher:
48− reprompt: Int
49− favorite: Bool
50− id: String
51− name: String
52− cipher: Cipher
60+ reprompt: Int
61+ favorite: Bool
62+ id: String
63+ name: String
64+ cipher: Cipher
5365
5466Cipher:
55−- LoginCipher:
56− uris: {List: String}
57− username:
67+ - LoginCipher:
68+ uris: { List: String }
69+ username:
5870 Maybe: String
59− password:
71+ password:
6072 Maybe: String
61− totp:
73+ totp:
6274 Maybe: String
63−- NoteCipher: String
64−- CardCipher:
65− cardholderName:
75+ - NoteCipher: String
76+ - CardCipher:
77+ cardholderName:
6678 Maybe: String
67− brand:
79+ brand:
6880 Maybe: String
69− number:
81+ number:
7082 Maybe: String
71− expMonth:
83+ expMonth:
7284 Maybe: String
73− expYear:
85+ expYear:
7486 Maybe: String
75− code:
87+ code:
7688 Maybe: String
77−- IdentityCipher:
78− title:
89+ - IdentityCipher:
90+ title:
7991 Maybe: String
80− firstName:
92+ firstName:
8193 Maybe: String
82− middleName:
94+ middleName:
8395 Maybe: String
84− lastName:
96+ lastName:
8597 Maybe: String
86− address1:
98+ address1:
8799 Maybe: String
88− address2:
100+ address2:
89101 Maybe: String
90− address3:
102+ address3:
91103 Maybe: String
92− city:
104+ city:
93105 Maybe: String
94− state:
106+ state:
95107 Maybe: String
96− postalCode:
108+ postalCode:
97109 Maybe: String
98− country:
110+ country:
99111 Maybe: String
100− company:
112+ company:
101113 Maybe: String
102− email:
114+ email:
103115 Maybe: String
104− phone:
116+ phone:
105117 Maybe: String
106− ssn:
118+ ssn:
107119 Maybe: String
108− username:
120+ username:
109121 Maybe: String
110− passportNumber:
122+ passportNumber:
111123 Maybe: String
112− licenseNumber:
124+ licenseNumber:
113125 Maybe: String
114126
115127CipherType:
116−- LoginType
117−- CardType
118−- NoteType
119−- IdentityType
128+ - LoginType
129+ - CardType
130+ - NoteType
131+ - IdentityType
120132
121133PasswordGeneratorConfig:
122− length: Int
123− ambiguous: Bool
124− number: Bool
125− minNumber: Int
126− uppercase: Bool
127− minUppercase: Int
128− lowercase: Bool
129− minLowercase: Int
130− special: Bool
131− minSpecial: Int
132− type: String
133− numWords: Int
134− wordSeparator: String
135− capitalize: Bool
136− includeNumber: Bool
134+ length: Int
135+ ambiguous: Bool
136+ number: Bool
137+ minNumber: Int
138+ uppercase: Bool
139+ minUppercase: Int
140+ lowercase: Bool
141+ minLowercase: Int
142+ special: Bool
143+ minSpecial: Int
144+ type: String
145+ numWords: Int
146+ wordSeparator: String
147+ capitalize: Bool
148+ includeNumber: Bool
149+
150+TwoFactorProviderType:
151+ - Authenticator
152+ - Email
153+ - Duo
154+ - Yubikey
155+ - U2f
156+ - Remember
157+ - OrganizationDuo
158+ - WebAuthn
dev/elm-live.sh+1−1
@@ -1,3 +1,3 @@
11#!/bin/sh
22
3−nix run github:NixOS/nixpkgs#elmPackages.elm-live -- frontend/Main.elm --start-page=index.html --proxy-prefix=/api --proxy-host=http://localhost:6009 --pushstate -- --debug --output=elm.js
3+nix run github:NixOS/nixpkgs?rev=d9a1414346059619d9e13ab93e749bbb82e5252a#elmPackages.elm-live -- $1 --start-page=index.html --proxy-prefix=/api --proxy-host=http://localhost:6009 --pushstate -- --debug --output=elm.js
dev/generate-bridge.sh+1−1
@@ -1,3 +1,3 @@
11#!/bin/sh
22
3−nix run github:ilyakooo0/elm-ps-bridge -- --input bridge.yaml --elmOutput=Frontend/Bridge.elm --psOutput=backend/Bridge.purs
3+nix run github:ilyakooo0/elm-ps-bridge -- --input bridge.yaml --elmOutput=frontend/Bridge.elm --psOutput=backend/Bridge.purs
dev/start-chrome.sh+5−1
@@ -1,3 +1,7 @@
11#!/bin/sh
22
3−/Applications/Chromium.app/Contents/MacOS/Chromium --args --user-data-dir="/tmp/chrome_dev_test" --disable-web-security
3+if test -f /Applications/Chromium.app/Contents/MacOS/Chromium; then
4+ /Applications/Chromium.app/Contents/MacOS/Chromium --args --user-data-dir="/tmp/chrome_dev_test" --disable-web-security
5+else
6+ flatpak run com.github.Eloston.UngoogledChromium --args --user-data-dir="/tmp/chrome_dev_test" --disable-web-security
7+fi
elm.json+1
@@ -13,6 +13,7 @@
1313 "elm/html": "1.0.0",
1414 "elm/json": "1.1.3",
1515 "elm/time": "1.0.0",
16+ "mdgriffith/elm-ui": "1.1.8",
1617 "mgold/elm-nonempty-list": "4.2.0"
1718 },
1819 "indirect": {
frontend/Bridge.elm+115−3
@@ -491,17 +491,82 @@ jsonEncCipherType val =
491491
492492
493493
494+type alias Cmd_ChooseSecondFactor =
495+ { email: String
496+ , factor: TwoFactorProviderType
497+ , password: String
498+ , requestFromServer: Bool
499+ , server: String
500+ }
501+
502+jsonDecCmd_ChooseSecondFactor : Json.Decode.Decoder ( Cmd_ChooseSecondFactor )
503+jsonDecCmd_ChooseSecondFactor =
504+ Json.Decode.succeed (\pemail pfactor ppassword prequestFromServer pserver -> {email = pemail, factor = pfactor, password = ppassword, requestFromServer = prequestFromServer, server = pserver})
505+ |> required "email" (Json.Decode.string)
506+ |> required "factor" (jsonDecTwoFactorProviderType)
507+ |> required "password" (Json.Decode.string)
508+ |> required "requestFromServer" (Json.Decode.bool)
509+ |> required "server" (Json.Decode.string)
510+
511+jsonEncCmd_ChooseSecondFactor : Cmd_ChooseSecondFactor -> Value
512+jsonEncCmd_ChooseSecondFactor val =
513+ Json.Encode.object
514+ [ ("email", Json.Encode.string val.email)
515+ , ("factor", jsonEncTwoFactorProviderType val.factor)
516+ , ("password", Json.Encode.string val.password)
517+ , ("requestFromServer", Json.Encode.bool val.requestFromServer)
518+ , ("server", Json.Encode.string val.server)
519+ ]
520+
521+
522+
523+type alias Cmd_Login_secondFactor =
524+ { provider: TwoFactorProviderType
525+ , remember: Bool
526+ , token: String
527+ }
528+
529+jsonDecCmd_Login_secondFactor : Json.Decode.Decoder ( Cmd_Login_secondFactor )
530+jsonDecCmd_Login_secondFactor =
531+ Json.Decode.succeed (\pprovider premember ptoken -> {provider = pprovider, remember = premember, token = ptoken})
532+ |> required "provider" (jsonDecTwoFactorProviderType)
533+ |> required "remember" (Json.Decode.bool)
534+ |> required "token" (Json.Decode.string)
535+
536+jsonEncCmd_Login_secondFactor : Cmd_Login_secondFactor -> Value
537+jsonEncCmd_Login_secondFactor val =
538+ Json.Encode.object
539+ [ ("provider", jsonEncTwoFactorProviderType val.provider)
540+ , ("remember", Json.Encode.bool val.remember)
541+ , ("token", Json.Encode.string val.token)
542+ ]
543+
544+
545+
546+type alias Cmd_Login_secondFactor_Maybe = (Maybe Cmd_Login_secondFactor)
547+
548+jsonDecCmd_Login_secondFactor_Maybe : Json.Decode.Decoder ( Cmd_Login_secondFactor_Maybe )
549+jsonDecCmd_Login_secondFactor_Maybe =
550+ Json.Decode.maybe (jsonDecCmd_Login_secondFactor)
551+
552+jsonEncCmd_Login_secondFactor_Maybe : Cmd_Login_secondFactor_Maybe -> Value
553+jsonEncCmd_Login_secondFactor_Maybe val = (maybeEncode (jsonEncCmd_Login_secondFactor)) val
554+
555+
556+
494557type alias Cmd_Login =
495558 { email: String
496559 , password: String
560+ , secondFactor: Cmd_Login_secondFactor_Maybe
497561 , server: String
498562 }
499563
500564jsonDecCmd_Login : Json.Decode.Decoder ( Cmd_Login )
501565jsonDecCmd_Login =
502− Json.Decode.succeed (\pemail ppassword pserver -> {email = pemail, password = ppassword, server = pserver})
566+ Json.Decode.succeed (\pemail ppassword psecondFactor pserver -> {email = pemail, password = ppassword, secondFactor = psecondFactor, server = pserver})
503567 |> required "email" (Json.Decode.string)
504568 |> required "password" (Json.Decode.string)
569+ |> required "secondFactor" (jsonDecCmd_Login_secondFactor_Maybe)
505570 |> required "server" (Json.Decode.string)
506571
507572jsonEncCmd_Login : Cmd_Login -> Value
@@ -509,13 +574,15 @@ jsonEncCmd_Login val =
509574 Json.Encode.object
510575 [ ("email", Json.Encode.string val.email)
511576 , ("password", Json.Encode.string val.password)
577+ , ("secondFactor", jsonEncCmd_Login_secondFactor_Maybe val.secondFactor)
512578 , ("server", Json.Encode.string val.server)
513579 ]
514580
515581
516582
517583type Cmd =
518− Copy String
584+ ChooseSecondFactor Cmd_ChooseSecondFactor
585+ | Copy String
519586 | CreateCipher FullCipher
520587 | DeleteCipher FullCipher
521588 | GeneratePassword PasswordGeneratorConfig
@@ -533,7 +600,8 @@ type Cmd =
533600jsonDecCmd : Json.Decode.Decoder ( Cmd )
534601jsonDecCmd =
535602 let jsonDecDictCmd = Dict.fromList
536− [ ("Copy", Json.Decode.lazy (\_ -> Json.Decode.map Copy (Json.Decode.string)))
603+ [ ("ChooseSecondFactor", Json.Decode.lazy (\_ -> Json.Decode.map ChooseSecondFactor (jsonDecCmd_ChooseSecondFactor)))
604+ , ("Copy", Json.Decode.lazy (\_ -> Json.Decode.map Copy (Json.Decode.string)))
537605 , ("CreateCipher", Json.Decode.lazy (\_ -> Json.Decode.map CreateCipher (jsonDecFullCipher)))
538606 , ("DeleteCipher", Json.Decode.lazy (\_ -> Json.Decode.map DeleteCipher (jsonDecFullCipher)))
539607 , ("GeneratePassword", Json.Decode.lazy (\_ -> Json.Decode.map GeneratePassword (jsonDecPasswordGeneratorConfig)))
@@ -554,6 +622,7 @@ jsonDecCmd =
554622jsonEncCmd : Cmd -> Value
555623jsonEncCmd val =
556624 let keyval v = case v of
625+ ChooseSecondFactor v1 -> ("ChooseSecondFactor", encodeValue (jsonEncCmd_ChooseSecondFactor v1))
557626 Copy v1 -> ("Copy", encodeValue (Json.Encode.string v1))
558627 CreateCipher v1 -> ("CreateCipher", encodeValue (jsonEncFullCipher v1))
559628 DeleteCipher v1 -> ("DeleteCipher", encodeValue (jsonEncFullCipher v1))
@@ -717,6 +786,17 @@ jsonEncSub_NeedsMasterPassword val =
717786
718787
719788
789+type alias Sub_NeedsSecondFactor_List = (List TwoFactorProviderType)
790+
791+jsonDecSub_NeedsSecondFactor_List : Json.Decode.Decoder ( Sub_NeedsSecondFactor_List )
792+jsonDecSub_NeedsSecondFactor_List =
793+ Json.Decode.list (jsonDecTwoFactorProviderType)
794+
795+jsonEncSub_NeedsSecondFactor_List : Sub_NeedsSecondFactor_List -> Value
796+jsonEncSub_NeedsSecondFactor_List val = (Json.Encode.list jsonEncTwoFactorProviderType) val
797+
798+
799+
720800type alias Sub_Totp =
721801 { code: String
722802 , interval: Int
@@ -752,6 +832,7 @@ type Sub =
752832 | NeedsCaptcha String
753833 | NeedsLogin
754834 | NeedsMasterPassword Sub_NeedsMasterPassword
835+ | NeedsSecondFactor Sub_NeedsSecondFactor_List
755836 | RecieveEmail String
756837 | Reset
757838 | Totp Sub_Totp
@@ -771,6 +852,7 @@ jsonDecSub =
771852 , ("NeedsCaptcha", Json.Decode.lazy (\_ -> Json.Decode.map NeedsCaptcha (Json.Decode.string)))
772853 , ("NeedsLogin", Json.Decode.lazy (\_ -> Json.Decode.succeed NeedsLogin))
773854 , ("NeedsMasterPassword", Json.Decode.lazy (\_ -> Json.Decode.map NeedsMasterPassword (jsonDecSub_NeedsMasterPassword)))
855+ , ("NeedsSecondFactor", Json.Decode.lazy (\_ -> Json.Decode.map NeedsSecondFactor (jsonDecSub_NeedsSecondFactor_List)))
774856 , ("RecieveEmail", Json.Decode.lazy (\_ -> Json.Decode.map RecieveEmail (Json.Decode.string)))
775857 , ("Reset", Json.Decode.lazy (\_ -> Json.Decode.succeed Reset))
776858 , ("Totp", Json.Decode.lazy (\_ -> Json.Decode.map Totp (jsonDecSub_Totp)))
@@ -793,9 +875,39 @@ jsonEncSub val =
793875 NeedsCaptcha v1 -> ("NeedsCaptcha", encodeValue (Json.Encode.string v1))
794876 NeedsLogin -> ("NeedsLogin", encodeValue (Json.Encode.list identity []))
795877 NeedsMasterPassword v1 -> ("NeedsMasterPassword", encodeValue (jsonEncSub_NeedsMasterPassword v1))
878+ NeedsSecondFactor v1 -> ("NeedsSecondFactor", encodeValue (jsonEncSub_NeedsSecondFactor_List v1))
796879 RecieveEmail v1 -> ("RecieveEmail", encodeValue (Json.Encode.string v1))
797880 Reset -> ("Reset", encodeValue (Json.Encode.list identity []))
798881 Totp v1 -> ("Totp", encodeValue (jsonEncSub_Totp v1))
799882 WrongPassword -> ("WrongPassword", encodeValue (Json.Encode.list identity []))
800883 in encodeSumTaggedObject "tag" "contents" keyval val
801884
885+
886+
887+type TwoFactorProviderType =
888+ Authenticator
889+ | Duo
890+ | Email
891+ | OrganizationDuo
892+ | Remember
893+ | U2f
894+ | WebAuthn
895+ | Yubikey
896+
897+jsonDecTwoFactorProviderType : Json.Decode.Decoder ( TwoFactorProviderType )
898+jsonDecTwoFactorProviderType =
899+ let jsonDecDictTwoFactorProviderType = Dict.fromList [("Authenticator", Authenticator), ("Duo", Duo), ("Email", Email), ("OrganizationDuo", OrganizationDuo), ("Remember", Remember), ("U2f", U2f), ("WebAuthn", WebAuthn), ("Yubikey", Yubikey)]
900+ in decodeSumUnaries "TwoFactorProviderType" jsonDecDictTwoFactorProviderType
901+
902+jsonEncTwoFactorProviderType : TwoFactorProviderType -> Value
903+jsonEncTwoFactorProviderType val =
904+ case val of
905+ Authenticator -> Json.Encode.string "Authenticator"
906+ Duo -> Json.Encode.string "Duo"
907+ Email -> Json.Encode.string "Email"
908+ OrganizationDuo -> Json.Encode.string "OrganizationDuo"
909+ Remember -> Json.Encode.string "Remember"
910+ U2f -> Json.Encode.string "U2f"
911+ WebAuthn -> Json.Encode.string "WebAuthn"
912+ Yubikey -> Json.Encode.string "Yubikey"
913+
frontend/Logic/SecondFactor.elmnew file+31
@@ -0,0 +1,31 @@
1+module Logic.SecondFactor exposing (secondFactorName)
2+
3+import Bridge
4+
5+
6+secondFactorName : Bridge.TwoFactorProviderType -> String
7+secondFactorName factor =
8+ case factor of
9+ Bridge.Authenticator ->
10+ "Authenticator"
11+
12+ Bridge.Duo ->
13+ "Duo"
14+
15+ Bridge.Email ->
16+ "Email"
17+
18+ Bridge.OrganizationDuo ->
19+ "Organization Duo"
20+
21+ Bridge.Remember ->
22+ "Remember (IDK what this is)"
23+
24+ Bridge.U2f ->
25+ "U2f"
26+
27+ Bridge.WebAuthn ->
28+ "WebAuthn"
29+
30+ Bridge.Yubikey ->
31+ "Yubikey"
frontend/Main.elm+278−186
@@ -1,4 +1,4 @@
1−module Main exposing (..)
1+module Main exposing (main)
22
33import Bridge
44import Browser
@@ -16,6 +16,8 @@ import Pages.Loader exposing (loader)
1616import Pages.Login as Login
1717import Pages.MasterPassword as MasterPassword
1818import Pages.Navigation as Navigation exposing (TopButton(..))
19+import Pages.SecondFactor as SecondFactor
20+import Pages.SecondFactorSelect as SecondFactorSelect
1921import Task
2022import Time
2123import Types exposing (CipherId)
@@ -28,31 +30,23 @@ notificationLingerSeconds =
2830
2931
3032type Msg
31− = Noop
32− | RecieveMessage String
33− | ShowLoginPage
33+ = ShowLoginPage
3434 | ShowError String String
3535 | ShowInfo String String
3636 | CloseNotification
37− | SubmitLogin { email : String, password : String, server : String }
38− | LoginMsg Login.Msg
39− | CiphersMsg Ciphers.Msg
37+ | SubmitLogin Bridge.Cmd_Login
4038 | LoadCiphers Bridge.Sub_LoadCiphers_List
41− | OpenCiphersScreen
39+ | LoginSuccessful
4240 | Reset
4341 | NeedsReset
44− | MasterPasswordMsg MasterPassword.Msg
4542 | SendMasterPassword { password : String }
4643 | ShowMasterPasswordPage { server : String, login : String }
47− | CipherMsg Cipher.Msg
4844 | ShowCipherPage Bridge.FullCipher
4945 | RequestCipher CipherId
5046 | PopView
5147 | RecieveEmail String
5248 | Copy String
5349 | Open String
54− | CaptchaMsg Captcha.Msg
55− | EditCipherMsg EditCipher.Msg
5650 | ShowCaptcha Captcha.HCaptchSiteKey
5751 | ClearNotification { currentTime : Time.Posix }
5852 | UpdateLastNotificatioTime Time.Posix
@@ -65,6 +59,22 @@ type Msg
6559 | WrongPassword
6660 | DeleteCipher Bridge.FullCipher
6761 | RequestTotp String
62+ | ShowSecondFactorSelect (List Bridge.TwoFactorProviderType)
63+ | SelectSecondFactor
64+ { provider : Bridge.TwoFactorProviderType
65+ , requestFromServer : Bool
66+ }
67+ | PageMsg PageMsg
68+
69+
70+type PageMsg
71+ = LoginMsg Login.Msg
72+ | CiphersMsg Ciphers.Msg
73+ | MasterPasswordMsg MasterPassword.Msg
74+ | CipherMsg Cipher.Msg
75+ | EditCipherMsg EditCipher.Msg
76+ | SecondFactorSelectMsg SecondFactorSelect.Msg
77+ | SecondFactorMsg SecondFactor.Msg
6878
6979
7080type alias Model =
@@ -75,19 +85,45 @@ type alias Model =
7585 }
7686
7787
88+findLoginDetails :
89+ List PageModel
90+ ->
91+ Maybe
92+ { server : String
93+ , email : String
94+ , password : String
95+ }
96+findLoginDetails xs =
97+ case xs of
98+ (LoginModel { server, email, password }) :: _ ->
99+ Just
100+ { server = server
101+ , email = email
102+ , password = password
103+ }
104+
105+ _ :: rest ->
106+ findLoginDetails rest
107+
108+ [] ->
109+ Nothing
110+
111+
78112appendNotification : Notification.Config -> Model -> ( Model, Cmd Msg )
79113appendNotification cfg model =
80114 ( { model | notifications = cfg :: model.notifications }, Task.perform UpdateLastNotificatioTime Time.now )
81115
82116
83117type PageModel
84− = LoginModel Login.Model
118+ = LoginModel (Login.Model Msg)
85119 | LoadingPage
86− | CiphersModel Ciphers.Model
120+ | CiphersModel (Ciphers.Model Msg)
87121 | EditCipherModel (EditCipher.Model Msg)
88− | MasterPasswordModel MasterPassword.Model
89− | CipherModel Cipher.Model
122+ | MasterPasswordModel (MasterPassword.Model Msg)
123+ | CipherModel (Cipher.Model Msg)
90124 | CaptchaModel Captcha.Model
125+ | SecondFactorSelectModel (SecondFactorSelect.Model Msg)
126+ | SecondFactorModel (SecondFactor.Model Msg)
91127
92128
93129showPage :
@@ -102,31 +138,43 @@ showPage email page =
102138 let
103139 simpleBackButton =
104140 BackButton { action = PopView, icon = Nothing }
141+
142+ liftPageWith :
143+ (pageMsg -> PageMsg)
144+ ->
145+ { title : List (Html pageMsg)
146+ , body : List (Html pageMsg)
147+ , topButton : Maybe (Navigation.TopButton Msg)
148+ }
149+ ->
150+ { title : List (Html Msg)
151+ , body : List (Html Msg)
152+ , topButton : Maybe (Navigation.TopButton Msg)
153+ }
154+ liftPageWith lift { title, body, topButton } =
155+ { title = title |> List.map (Html.map (lift >> PageMsg))
156+ , body = body |> List.map (Html.map (lift >> PageMsg))
157+ , topButton = topButton
158+ }
105159 in
106160 case page of
107161 LoginModel model ->
108− let
109− p =
110− Login.page loginCallbacks LoginMsg
111− in
112− { title = p.title model
113− , body = p.view model
114− , topButton = Nothing
115− }
162+ liftPageWith LoginMsg
163+ { title = [ text Login.title ]
164+ , body = Login.view model
165+ , topButton = Nothing
166+ }
116167
117168 CiphersModel model ->
118− let
119− p =
120− Ciphers.page ciphersCallbacks CiphersMsg
121− in
122− { title = p.title model
123− , body = p.view model
124− , topButton =
125− Ciphers.menuConfig email model
126− |> Navigation.mapMenuConfig CiphersMsg
127− |> Navigation.MenuButton
128− |> Just
129− }
169+ liftPageWith CiphersMsg
170+ { title = Ciphers.title model
171+ , body = Ciphers.view model
172+ , topButton =
173+ Ciphers.menuConfig email model
174+ |> Navigation.mapMenuConfig (CiphersMsg >> PageMsg)
175+ |> Navigation.MenuButton
176+ |> Just
177+ }
130178
131179 LoadingPage ->
132180 { title = []
@@ -135,44 +183,45 @@ showPage email page =
135183 }
136184
137185 MasterPasswordModel model ->
138− let
139− p =
140− MasterPassword.page masterPasswordCallbacks MasterPasswordMsg
141− in
142− { title = p.title model
143− , body = p.view model
144− , topButton = Nothing
145− }
186+ liftPageWith MasterPasswordMsg
187+ { title = [ text MasterPassword.title ]
188+ , body = MasterPassword.view model
189+ , topButton = Nothing
190+ }
146191
147192 CipherModel model ->
148− let
149− p =
150− Cipher.page cipherCallbacks CipherMsg
151− in
152− { title = p.title model
153− , body = p.view model
154− , topButton = Just simpleBackButton
155− }
193+ liftPageWith CipherMsg
194+ { title = Cipher.title model
195+ , body = Cipher.view model
196+ , topButton = Just simpleBackButton
197+ }
156198
157199 CaptchaModel model ->
158− let
159− p =
160− Captcha.page captchaCallbacks CaptchaMsg
161− in
162− { title = p.title model
163− , body = p.view model
200+ { title = [ text Captcha.title ]
201+ , body = Captcha.view model
164202 , topButton = Just simpleBackButton
165203 }
166204
167205 EditCipherModel model ->
168− let
169− p =
170− EditCipher.page EditCipherMsg
171− in
172− { title = p.title model
173− , body = p.view model
174− , topButton = Just (BackButton { action = PopView, icon = Just "close" })
175− }
206+ liftPageWith EditCipherMsg
207+ { title = EditCipher.title model
208+ , body = EditCipher.view model
209+ , topButton = Just (BackButton { action = PopView, icon = Just "close" })
210+ }
211+
212+ SecondFactorSelectModel model ->
213+ liftPageWith SecondFactorSelectMsg
214+ { title = [ text SecondFactorSelect.title ]
215+ , body = SecondFactorSelect.view model
216+ , topButton = Just simpleBackButton
217+ }
218+
219+ SecondFactorModel model ->
220+ liftPageWith SecondFactorMsg
221+ { title = [ SecondFactor.title model |> text ]
222+ , body = [ SecondFactor.view model ]
223+ , topButton = Just simpleBackButton
224+ }
176225
177226
178227main : Program () Model Msg
@@ -201,7 +250,7 @@ subscriptions model =
201250 LoadCiphers ciphers
202251
203252 Bridge.LoginSuccessful ->
204− OpenCiphersScreen
253+ LoginSuccessful
205254
206255 Bridge.Reset ->
207256 Reset
@@ -235,6 +284,17 @@ subscriptions model =
235284
236285 Bridge.Totp totp ->
237286 GlobalEvents.DecodedTotp totp |> FireGlobalEvent
287+
288+ Bridge.NeedsSecondFactor secondFactors ->
289+ case secondFactors of
290+ [ provider ] ->
291+ SelectSecondFactor
292+ { provider = provider
293+ , requestFromServer = False
294+ }
295+
296+ _ ->
297+ ShowSecondFactorSelect secondFactors
238298 )
239299 ]
240300 ++ optional (List.isEmpty model.notifications |> not) (Time.every 1000 (\t -> ClearNotification { currentTime = t }))
@@ -243,26 +303,32 @@ subscriptions model =
243303 |> List.map
244304 (\x ->
245305 case x of
246− LoginModel m ->
247− (Login.page loginCallbacks LoginMsg).subscriptions m
306+ LoginModel _ ->
307+ Sub.none
248308
249309 LoadingPage ->
250310 Sub.none
251311
252− CiphersModel m ->
253− (Ciphers.page ciphersCallbacks CiphersMsg).subscriptions m
312+ CiphersModel _ ->
313+ Sub.none
254314
255− EditCipherModel m ->
256− (EditCipher.page EditCipherMsg).subscriptions m
315+ EditCipherModel _ ->
316+ Sub.none
257317
258− MasterPasswordModel m ->
259− (MasterPassword.page masterPasswordCallbacks MasterPasswordMsg).subscriptions m
318+ MasterPasswordModel _ ->
319+ Sub.none
260320
261321 CipherModel m ->
262− (Cipher.page cipherCallbacks CipherMsg).subscriptions m
322+ Cipher.subscriptions m
323+
324+ CaptchaModel _ ->
325+ Sub.none
263326
264− CaptchaModel m ->
265− (Captcha.page captchaCallbacks CaptchaMsg).subscriptions m
327+ SecondFactorSelectModel _ ->
328+ Sub.none
329+
330+ SecondFactorModel _ ->
331+ Sub.none
266332 )
267333 )
268334 )
@@ -283,7 +349,10 @@ view : Model -> Html Msg
283349view model =
284350 div []
285351 (maybeList (List.head model.notifications) (Lazy.lazy2 Notification.notification CloseNotification)
286− ++ [ Lazy.lazy2 Navigation.showNavigationView model.pageStack (showPage (Maybe.withDefault "" model.userEmail)) ]
352+ ++ [ Lazy.lazy2 Navigation.showNavigationView
353+ model.pageStack
354+ (showPage (Maybe.withDefault "" model.userEmail))
355+ ]
287356 )
288357
289358
@@ -295,12 +364,6 @@ mapHead f (Nonempty x xx) =
295364doNotStoreInHistory : PageModel -> Bool
296365doNotStoreInHistory page =
297366 case page of
298− LoginModel _ ->
299− True
300−
301− MasterPasswordModel _ ->
302− True
303−
304367 LoadingPage ->
305368 True
306369
@@ -311,19 +374,9 @@ doNotStoreInHistory page =
311374update : Msg -> Model -> ( Model, Cmd Msg )
312375update msg model =
313376 let
314− processPage : (pageModel -> PageModel) -> ( Result String pageModel, Cmd Msg ) -> ( Model, Cmd Msg )
315− processPage liftModel ( resultModel, cmd ) =
316− case resultModel of
317− Ok mdl ->
318− ( { model | pageStack = mapHead (always (liftModel mdl)) model.pageStack }, cmd )
319−
320− Err err ->
321− appendNotification
322− { title = "An error had occured"
323− , message = err
324− , severity = Notification.Error
325− }
326− model
377+ updatePageStackHead : PageModel -> Model
378+ updatePageStackHead mdl =
379+ { model | pageStack = mapHead (always mdl) model.pageStack }
327380
328381 appendPageStack : PageModel -> Model
329382 appendPageStack mdl =
@@ -332,19 +385,8 @@ update msg model =
332385 keepStackWith : PageModel -> Model
333386 keepStackWith mdl =
334387 { model | pageStack = Nonempty.cons mdl model.pageStack }
335−
336− currentPage =
337− model.pageStack |> Nonempty.head
338388 in
339389 case msg of
340− RecieveMessage x ->
341− appendNotification
342− { title = "Got a message!"
343− , message = x
344− , severity = Notification.Info
345− }
346− model
347−
348390 ShowError title err ->
349391 appendNotification
350392 { title = title
@@ -371,25 +413,10 @@ update msg model =
371413 , FFI.sendBridge (Bridge.Login data)
372414 )
373415
374− LoginMsg imsg ->
375− case currentPage of
376− LoginModel page ->
377− (Login.page loginCallbacks LoginMsg).update imsg page |> processPage LoginModel
378−
379− _ ->
380− ( model, Cmd.none )
381−
382− CiphersMsg imsg ->
383− case currentPage of
384− CiphersModel page ->
385− (Ciphers.page ciphersCallbacks CiphersMsg).update imsg page |> processPage CiphersModel
386−
387− _ ->
388− ( model, Cmd.none )
389−
390416 ShowLoginPage ->
391− (Login.page loginCallbacks LoginMsg).init ()
392− |> Tuple.mapFirst (\pageModel -> appendPageStack <| LoginModel pageModel)
417+ ( Login.init loginCallbacks |> LoginModel |> appendPageStack
418+ , Cmd.none
419+ )
393420
394421 LoadCiphers ciphers ->
395422 let
@@ -406,8 +433,8 @@ update msg model =
406433 False
407434 )
408435
409− ( pageModel, cmd ) =
410− (Ciphers.page ciphersCallbacks CiphersMsg).init ciphers
436+ pageModel =
437+ Ciphers.init ciphersCallbacks ciphers
411438 in
412439 ( if ciphersShown then
413440 { model
@@ -426,18 +453,23 @@ update msg model =
426453
427454 else
428455 appendPageStack <| CiphersModel pageModel
429− , cmd
456+ , Cmd.none
430457 )
431458
432− OpenCiphersScreen ->
433− ( appendPageStack <| LoadingPage, Cmd.batch [ sendBridge Bridge.NeedCiphersList, sendBridge Bridge.NeedEmail ] )
459+ LoginSuccessful ->
460+ ( { model | pageStack = Nonempty.singleton LoadingPage }
461+ , Cmd.batch [ sendBridge Bridge.NeedCiphersList, sendBridge Bridge.NeedEmail ]
462+ )
434463
435464 Reset ->
436465 init
437466
438467 ShowMasterPasswordPage { server, login } ->
439− (MasterPassword.page masterPasswordCallbacks MasterPasswordMsg).init { server = server, login = login }
440− |> Tuple.mapFirst (\pageModel -> appendPageStack <| MasterPasswordModel pageModel)
468+ ( MasterPassword.init masterPasswordCallbacks { server = server, login = login }
469+ |> MasterPasswordModel
470+ |> appendPageStack
471+ , Cmd.none
472+ )
441473
442474 NeedsReset ->
443475 ( appendPageStack <| LoadingPage, sendBridge Bridge.NeedsReset )
@@ -445,32 +477,8 @@ update msg model =
445477 SendMasterPassword { password } ->
446478 ( appendPageStack <| LoadingPage, sendBridge (Bridge.SendMasterPassword password) )
447479
448− MasterPasswordMsg imsg ->
449− case currentPage of
450− MasterPasswordModel page ->
451− (MasterPassword.page masterPasswordCallbacks MasterPasswordMsg).update imsg page |> processPage MasterPasswordModel
452−
453− _ ->
454− ( model, Cmd.none )
455−
456− CipherMsg imsg ->
457− case currentPage of
458− CipherModel page ->
459− (Cipher.page cipherCallbacks CipherMsg).update imsg page |> processPage CipherModel
460−
461− _ ->
462− ( model, Cmd.none )
463−
464− EditCipherMsg imsg ->
465− case currentPage of
466− EditCipherModel page ->
467− (EditCipher.page EditCipherMsg).update imsg page |> processPage EditCipherModel
468−
469− _ ->
470− ( model, Cmd.none )
471−
472480 ShowCipherPage cipher ->
473− (Cipher.page cipherCallbacks CipherMsg).init cipher
481+ Cipher.init cipherCallbacks cipher
474482 |> Tuple.mapFirst (\pageModel -> appendPageStack <| CipherModel pageModel)
475483
476484 RequestCipher id ->
@@ -491,16 +499,7 @@ update msg model =
491499 ( model, FFI.sendBridge (Bridge.Open uri) )
492500
493501 ShowCaptcha uri ->
494− (Captcha.page captchaCallbacks CaptchaMsg).init uri
495− |> Tuple.mapFirst (\pageModel -> keepStackWith <| CaptchaModel pageModel)
496−
497− CaptchaMsg imsg ->
498− case currentPage of
499− CaptchaModel page ->
500− (Captcha.page captchaCallbacks CaptchaMsg).update imsg page |> processPage CaptchaModel
501−
502− _ ->
503− ( model, Cmd.none )
502+ ( Captcha.init uri |> CaptchaModel |> keepStackWith, Cmd.none )
504503
505504 ClearNotification { currentTime } ->
506505 if Time.posixToMillis model.lastNotificationTime + notificationLingerSeconds * 1000 > Time.posixToMillis currentTime then
@@ -514,11 +513,8 @@ update msg model =
514513 UpdateLastNotificatioTime time ->
515514 ( { model | lastNotificationTime = time }, Cmd.none )
516515
517− Noop ->
518− ( model, Cmd.none )
519−
520516 EditCipher cipher ->
521− (EditCipher.page EditCipherMsg).init { fullCipher = cipher, callbacks = editCipherCallbacks }
517+ EditCipher.init { fullCipher = cipher, callbacks = editCipherCallbacks }
522518 |> Tuple.mapFirst (\pageModel -> keepStackWith <| EditCipherModel pageModel)
523519
524520 UpdateCipher cipher ->
@@ -549,22 +545,28 @@ update msg model =
549545 LoadingPage
550546
551547 LoginModel m ->
552− (Login.page loginCallbacks LoginMsg).event m ev |> LoginModel
548+ LoginModel m
553549
554550 CiphersModel m ->
555− (Ciphers.page ciphersCallbacks CiphersMsg).event m ev |> CiphersModel
551+ Ciphers.event m ev |> CiphersModel
556552
557553 MasterPasswordModel m ->
558− (MasterPassword.page masterPasswordCallbacks MasterPasswordMsg).event m ev |> MasterPasswordModel
554+ MasterPasswordModel m
559555
560556 CipherModel m ->
561− (Cipher.page cipherCallbacks CipherMsg).event m ev |> CipherModel
557+ Cipher.event m ev |> CipherModel
562558
563559 EditCipherModel m ->
564− (EditCipher.page EditCipherMsg).event m ev |> EditCipherModel
560+ EditCipher.event m ev |> EditCipherModel
565561
566562 CaptchaModel m ->
567− (Captcha.page captchaCallbacks CaptchaMsg).event m ev |> CaptchaModel
563+ CaptchaModel m
564+
565+ SecondFactorSelectModel m ->
566+ SecondFactorSelectModel m
567+
568+ SecondFactorModel m ->
569+ SecondFactorModel m
568570 )
569571 }
570572 , Cmd.none
@@ -574,7 +576,7 @@ update msg model =
574576 ( model, FFI.sendBridge (Bridge.GeneratePassword cfg) )
575577
576578 OpenNewCipherEditPage t ->
577− (EditCipher.page EditCipherMsg).init
579+ EditCipher.init
578580 { callbacks = createCipherCallbacks
579581 , fullCipher =
580582 { reprompt = 0
@@ -649,10 +651,96 @@ update msg model =
649651 RequestTotp totp ->
650652 ( model, FFI.sendBridge (Bridge.RequestTotp totp) )
651653
654+ ShowSecondFactorSelect secondFactors ->
655+ ( SecondFactorSelect.init secondFactorSelectCallbacks secondFactors
656+ |> SecondFactorSelectModel
657+ |> appendPageStack
658+ , Cmd.none
659+ )
660+
661+ SelectSecondFactor { provider, requestFromServer } ->
662+ case Nonempty.toList model.pageStack |> findLoginDetails of
663+ Just { email, password, server } ->
664+ ( SecondFactor.init
665+ { provider = provider
666+ , callbacks =
667+ { submit =
668+ \{ token, remember } ->
669+ SubmitLogin
670+ { email = email
671+ , password = password
672+ , server = server
673+ , secondFactor =
674+ Just
675+ { provider = provider
676+ , token = token
677+ , remember = remember
678+ }
679+ }
680+ }
681+ }
682+ |> SecondFactorModel
683+ |> appendPageStack
684+ , Bridge.ChooseSecondFactor
685+ { factor = provider
686+ , email = email
687+ , password = password
688+ , server = server
689+ , requestFromServer = requestFromServer
690+ }
691+ |> FFI.sendBridge
692+ )
693+
694+ Nothing ->
695+ ( model, Cmd.none )
696+
697+ PageMsg pageMsg ->
698+ case ( pageMsg, model.pageStack |> Nonempty.head ) of
699+ ( SecondFactorMsg imsg, SecondFactorModel m ) ->
700+ SecondFactor.update m imsg
701+ |> Tuple.mapBoth
702+ (SecondFactorModel >> updatePageStackHead)
703+ (Maybe.map pureCmd >> Maybe.withDefault Cmd.none)
704+
705+ ( LoginMsg imsg, LoginModel page ) ->
706+ Login.update imsg page
707+ |> Tuple.mapFirst (LoginModel >> updatePageStackHead)
708+
709+ ( CiphersMsg imsg, CiphersModel page ) ->
710+ Ciphers.update imsg page
711+ |> Tuple.mapFirst (CiphersModel >> updatePageStackHead)
712+
713+ ( MasterPasswordMsg imsg, MasterPasswordModel page ) ->
714+ MasterPassword.update imsg page
715+ |> Tuple.mapFirst (MasterPasswordModel >> updatePageStackHead)
716+
717+ ( CipherMsg imsg, CipherModel page ) ->
718+ Cipher.update imsg page
719+ |> Tuple.mapFirst (CipherModel >> updatePageStackHead)
720+
721+ ( EditCipherMsg imsg, EditCipherModel page ) ->
722+ EditCipher.update imsg page
723+ |> Tuple.mapFirst (EditCipherModel >> updatePageStackHead)
724+
725+ ( SecondFactorSelectMsg imsg, SecondFactorSelectModel m ) ->
726+ SecondFactorSelect.update imsg m
727+ |> Tuple.mapFirst (SecondFactorSelectModel >> updatePageStackHead)
728+
729+ _ ->
730+ ( model, Cmd.none )
731+
652732
653733loginCallbacks : Login.Callbacks Msg
654734loginCallbacks =
655− { submit = SubmitLogin }
735+ { submit =
736+ \{ email, server, password } ->
737+ SubmitLogin
738+ { email = email
739+ , password = password
740+ , server = server
741+ , secondFactor = Nothing
742+ }
743+ }
656744
657745
658746ciphersCallbacks : Ciphers.Callbacks Msg
@@ -660,6 +748,7 @@ ciphersCallbacks =
660748 { selected = RequestCipher
661749 , logOut = NeedsReset
662750 , createNewCipher = OpenNewCipherEditPage
751+ , lift = CiphersMsg >> PageMsg
663752 }
664753
665754
@@ -678,13 +767,16 @@ createCipherCallbacks =
678767 { save = CreateCipher, generatePassword = GeneratePassword, delete = Nothing }
679768
680769
681−captchaCallbacks : Captcha.Callbacks
682−captchaCallbacks =
683− {}
684−
685−
686770masterPasswordCallbacks : MasterPassword.Callbacks Msg
687771masterPasswordCallbacks =
688772 { submit = SendMasterPassword
689773 , reset = NeedsReset
690774 }
775+
776+
777+secondFactorSelectCallbacks : SecondFactorSelect.Callbacks Msg
778+secondFactorSelectCallbacks =
779+ { selectFactor =
780+ \provider ->
781+ SelectSecondFactor { provider = provider, requestFromServer = True }
782+ }
frontend/Page.elm+20−17
@@ -1,24 +1,27 @@
11module Page exposing (..)
22
3−import GlobalEvents exposing (Event)
3+import Browser
44import Html exposing (..)
55import Html.Events exposing (..)
66
77
8−type alias Env =
9− {}
10−
11−
12−type alias Page init model imsg emsg =
13− (imsg -> emsg)
14− -> InitializedPage init model imsg emsg
15−
16−
17−type alias InitializedPage init model imsg emsg =
18− { init : init -> ( model, Cmd emsg )
19− , view : model -> List (Html emsg)
20− , update : imsg -> model -> ( Result String model, Cmd emsg )
21− , subscriptions : model -> Sub emsg
22− , title : model -> List (Html emsg)
23− , event : model -> Event -> model
8+page :
9+ { init : model
10+ , view : model -> List (Html msg)
11+ , update : msg -> model -> ( model, Cmd (Maybe msg) )
2412 }
13+ -> Program () model (Maybe msg)
14+page { init, view, update } =
15+ Browser.element
16+ { subscriptions = always Sub.none
17+ , init = \_ -> ( init, Cmd.none )
18+ , view = view >> Html.div [] >> Html.map Just
19+ , update =
20+ \mmsg model ->
21+ case mmsg of
22+ Nothing ->
23+ ( model, Cmd.none )
24+
25+ Just msg ->
26+ update msg model
27+ }
frontend/Pages/Captcha.elm+7−33
@@ -1,4 +1,4 @@
1−module Pages.Captcha exposing (Callbacks, HCaptchSiteKey, Model, Msg, page)
1+module Pages.Captcha exposing (HCaptchSiteKey, Model, init, title, view)
22
33import Html exposing (..)
44import Html.Attributes as Attr
@@ -15,43 +15,17 @@ type alias Model =
1515 }
1616
1717
18−type Msg
19− = Noop
18+title : String
19+title =
20+ "Captcha"
2021
2122
22−type alias Callbacks =
23− {}
24−
25−
26−page : Callbacks -> Page HCaptchSiteKey Model Msg emsg
27−page callbacks liftMsg =
28− { init = \siteKey -> Tuple.mapSecond (Cmd.map liftMsg) (init siteKey)
29− , view = \model -> view model |> List.map (Html.map liftMsg)
30− , update = \msg model -> update callbacks liftMsg msg model
31− , subscriptions = \model -> subscriptions model |> Sub.map liftMsg
32− , title = always [ text "Captcha" ]
33− , event = \model _ -> model
34− }
35−
36−
37−init : HCaptchSiteKey -> ( Model, Cmd Msg )
23+init : HCaptchSiteKey -> Model
3824init siteKey =
39− ( { siteKey = siteKey }, Cmd.none )
40−
41−
42−update : Callbacks -> (Msg -> emsg) -> Msg -> Model -> ( Result String Model, Cmd emsg )
43−update _ _ msg model =
44− case msg of
45− Noop ->
46− ( Ok model, Cmd.none )
47−
48−
49−subscriptions : Model -> Sub Msg
50−subscriptions model =
51− Sub.none
25+ { siteKey = siteKey }
5226
5327
54−view : Model -> List (Html Msg)
28+view : Model -> List (Html msg)
5529view { siteKey } =
5630 [ div [ Attr.class "u-embedded-media" ]
5731 [ iframe [ Attr.src ("https://iko.soy/warden/hcaptcha.html?" ++ siteKey) ] [] ]
frontend/Pages/Cipher.elm+62−51
@@ -1,4 +1,14 @@
1−module Pages.Cipher exposing (..)
1+module Pages.Cipher exposing
2+ ( Callbacks
3+ , Model
4+ , Msg
5+ , event
6+ , init
7+ , subscriptions
8+ , title
9+ , update
10+ , view
11+ )
212
313import Bridge
414import GlobalEvents
@@ -10,11 +20,12 @@ import Time
1020import Utils exposing (..)
1121
1222
13−type alias Model =
23+type alias Model emsg =
1424 { cipher : Bridge.FullCipher
1525 , passwordHidden : Bool
1626 , cvvHidden : Bool
1727 , decodedTotp : Maybe { code : String, interval : Int }
28+ , callbacks : Callbacks emsg
1829 }
1930
2031
@@ -23,6 +34,7 @@ type Msg
2334 | ToggleCVVVisiblity
2435 | Copy String
2536 | Open String
37+ | Edit Bridge.FullCipher
2638
2739
2840type alias Callbacks msg =
@@ -33,59 +45,55 @@ type alias Callbacks msg =
3345 }
3446
3547
36−page : Callbacks emsg -> Page Bridge.FullCipher Model Msg emsg
37−page callbacks liftMsg =
38− { init = \data -> init callbacks data
39− , view = \model -> view model |> List.map (Html.map liftMsg)
40− , update = \msg model -> update callbacks liftMsg msg model
41− , subscriptions = \model -> subscriptions model callbacks
42− , title =
43− \{ cipher } ->
44− [ text cipher.name
45− , span [ Attr.class "u-float-right" ] [ iconButton "edit" (callbacks.edit cipher) ]
46− ]
47− , event =
48− \model ev ->
49− case ev of
50− GlobalEvents.UpdateCipher c ->
51− { model
52− | cipher =
53− if c.id == model.cipher.id then
54− c
55−
56− else
57− model.cipher
58− }
59−
60− GlobalEvents.DecodedTotp { source, code, interval } ->
61− case model.cipher.cipher of
62− Bridge.LoginCipher { totp } ->
63− if totp == Just source then
64− { model | decodedTotp = Just { code = code, interval = interval } }
65−
66− else
67− model
68−
69− _ ->
70− model
48+event : Model emsg -> GlobalEvents.Event -> Model emsg
49+event model ev =
50+ case ev of
51+ GlobalEvents.UpdateCipher c ->
52+ { model
53+ | cipher =
54+ if c.id == model.cipher.id then
55+ c
56+
57+ else
58+ model.cipher
59+ }
60+
61+ GlobalEvents.DecodedTotp { source, code, interval } ->
62+ case model.cipher.cipher of
63+ Bridge.LoginCipher { totp } ->
64+ if totp == Just source then
65+ { model | decodedTotp = Just { code = code, interval = interval } }
66+
67+ else
68+ model
7169
7270 _ ->
7371 model
74− }
7572
73+ _ ->
74+ model
75+
76+
77+title : Model emsg -> List (Html Msg)
78+title { cipher } =
79+ [ text cipher.name
80+ , span [ Attr.class "u-float-right" ] [ iconButton "edit" (Edit cipher) ]
81+ ]
7682
77−init : Callbacks emsg -> Bridge.FullCipher -> ( Model, Cmd emsg )
78−init { needTotp } cipher =
83+
84+init : Callbacks emsg -> Bridge.FullCipher -> ( Model emsg, Cmd emsg )
85+init callbacks cipher =
7986 ( { cipher = cipher
8087 , passwordHidden = True
8188 , cvvHidden = True
8289 , decodedTotp = Nothing
90+ , callbacks = callbacks
8391 }
8492 , case cipher.cipher of
8593 Bridge.LoginCipher { totp } ->
8694 case totp of
8795 Just x ->
88− needTotp x |> pureCmd
96+ callbacks.needTotp x |> pureCmd
8997
9098 Nothing ->
9199 Cmd.none
@@ -95,29 +103,32 @@ init { needTotp } cipher =
95103 )
96104
97105
98−update : Callbacks emsg -> (Msg -> emsg) -> Msg -> Model -> ( Result String Model, Cmd emsg )
99−update { copy, open } _ msg model =
106+update : Msg -> Model emsg -> ( Model emsg, Cmd emsg )
107+update msg model =
100108 case msg of
101109 TogglePasswordVisiblity ->
102− ( Ok { model | passwordHidden = not model.passwordHidden }, Cmd.none )
110+ ( { model | passwordHidden = not model.passwordHidden }, Cmd.none )
103111
104112 ToggleCVVVisiblity ->
105− ( Ok { model | cvvHidden = not model.cvvHidden }, Cmd.none )
113+ ( { model | cvvHidden = not model.cvvHidden }, Cmd.none )
106114
107115 Copy text ->
108− ( Ok model, copy text |> pureCmd )
116+ ( model, model.callbacks.copy text |> pureCmd )
109117
110118 Open uri ->
111− ( Ok model, open uri |> pureCmd )
119+ ( model, model.callbacks.open uri |> pureCmd )
120+
121+ Edit cipher ->
122+ ( model, model.callbacks.edit cipher |> pureCmd )
112123
113124
114−subscriptions : Model -> Callbacks msg -> Sub msg
115−subscriptions { cipher } { needTotp } =
125+subscriptions : Model msg -> Sub msg
126+subscriptions { cipher, callbacks } =
116127 case cipher.cipher of
117128 Bridge.LoginCipher { totp } ->
118129 case totp of
119130 Just x ->
120− Time.every 1000 (always (needTotp x))
131+ Time.every 1000 (always (callbacks.needTotp x))
121132
122133 Nothing ->
123134 Sub.none
@@ -136,7 +147,7 @@ row { name, value, nameIcon, icons } =
136147 ]
137148
138149
139−view : Model -> List (Html Msg)
150+view : Model emsg -> List (Html Msg)
140151view { passwordHidden, cipher, cvvHidden, decodedTotp } =
141152 (case cipher.cipher of
142153 Bridge.LoginCipher { username, password, uris, totp } ->
frontend/Pages/Ciphers.elm+123−128
@@ -1,4 +1,4 @@
1−module Pages.Ciphers exposing (Callbacks, Model, Msg, menuConfig, page)
1+module Pages.Ciphers exposing (Callbacks, Model, Msg, event, init, menuConfig, title, update, view)
22
33import Bridge
44import GlobalEvents
@@ -14,7 +14,7 @@ import Search
1414import Utils exposing (..)
1515
1616
17−type alias Model =
17+type alias Model emsg =
1818 { ciphers : Bridge.Sub_LoadCiphers_List
1919 , search : String
2020 , menuVisible : Bool
@@ -22,6 +22,7 @@ type alias Model =
2222 , scroll : InfiniteScroll.Model Msg
2323 , sublist : { end : Int }
2424 , cipherSelectDropdownVisivble : Bool
25+ , callbacks : Callbacks emsg
2526 }
2627
2728
@@ -40,7 +41,7 @@ applyCipherFilter filter list =
4041 list |> List.filter (.cipherType >> (==) t)
4142
4243
43−menuConfig : String -> Model -> Navigation.MenuConfig Msg
44+menuConfig : String -> Model emsg -> Navigation.MenuConfig Msg
4445menuConfig email { ciphersListFilter, menuVisible } =
4546 { title = email
4647 , items =
@@ -86,139 +87,135 @@ type Msg
8687 | InfiniteScrollMsg InfiniteScroll.Msg
8788 | CreateNewCipher Bridge.CipherType
8889 | ToggleSelectCipherTypeVisible
90+ | SelectCipher String
8991
9092
9193type alias Callbacks msg =
9294 { selected : String -> msg
9395 , logOut : msg
9496 , createNewCipher : Bridge.CipherType -> msg
97+ , lift : Msg -> msg
9598 }
9699
97100
98−page : Callbacks emsg -> Page Bridge.Sub_LoadCiphers_List Model Msg emsg
99−page callbacks liftMsg =
100− { init = \ciphers -> Tuple.mapSecond (Cmd.map liftMsg) (init ciphers)
101− , view = view callbacks liftMsg
102− , update = \msg model -> update callbacks liftMsg msg model
103− , subscriptions = \model -> subscriptions model |> Sub.map liftMsg
104− , title =
105− \{ ciphersListFilter, cipherSelectDropdownVisivble } ->
106− [ text
107− (case ciphersListFilter of
108− AllCiphers ->
109− "All items"
110−
111− SpecificCiphers cipherType ->
112− case cipherType of
113− Bridge.CardType ->
114− "Cards"
115−
116− Bridge.IdentityType ->
117− "Identities"
118−
119− Bridge.LoginType ->
120− "Passwords"
121−
122− Bridge.NoteType ->
123− "Notes"
124− )
125− , span [ Attr.class "u-float-right" ]
126− [ span [ Attr.class "p-contextual-menu" ]
127− ([ span [] [ iconButton "plus" (liftMsg ToggleSelectCipherTypeVisible) ]
128− ]
129− ++ optionals cipherSelectDropdownVisivble
130− [ div
131− [ Attr.class "overlay"
132− , Ev.onClick (liftMsg ToggleSelectCipherTypeVisible)
133− ]
134− []
135− , span
136− [ Attr.class "p-contextual-menu__dropdown"
137− , Attr.attribute "aria-hidden" "false"
138− ]
139− (let
140− create t =
141− liftMsg (CreateNewCipher t)
142− in
143− [ button
144− [ Attr.class "p-contextual-menu__link"
145− , Ev.onClick (create Bridge.LoginType)
146− ]
147− [ text "Login" ]
148− , button
149− [ Attr.class "p-contextual-menu__link"
150− , Ev.onClick (create Bridge.CardType)
151− ]
152− [ text "Card" ]
153− , button
154− [ Attr.class "p-contextual-menu__link"
155− , Ev.onClick (create Bridge.NoteType)
156− ]
157− [ text "Note" ]
158− , button
159− [ Attr.class "p-contextual-menu__link"
160− , Ev.onClick (create Bridge.IdentityType)
161− ]
162− [ text "Contact" ]
163− ]
164− )
101+event : Model emsg -> GlobalEvents.Event -> Model emsg
102+event model ev =
103+ case ev of
104+ GlobalEvents.UpdateCipher c ->
105+ { model
106+ | ciphers =
107+ model.ciphers
108+ |> List.map
109+ (\cipher ->
110+ if cipher.id == c.id then
111+ { cipher | name = c.name }
112+
113+ else
114+ cipher
115+ )
116+ }
117+
118+ _ ->
119+ model
120+
121+
122+title : Model emsg -> List (Html Msg)
123+title { ciphersListFilter, cipherSelectDropdownVisivble } =
124+ [ text
125+ (case ciphersListFilter of
126+ AllCiphers ->
127+ "All items"
128+
129+ SpecificCiphers cipherType ->
130+ case cipherType of
131+ Bridge.CardType ->
132+ "Cards"
133+
134+ Bridge.IdentityType ->
135+ "Identities"
136+
137+ Bridge.LoginType ->
138+ "Passwords"
139+
140+ Bridge.NoteType ->
141+ "Notes"
142+ )
143+ , span [ Attr.class "u-float-right" ]
144+ [ span [ Attr.class "p-contextual-menu" ]
145+ ([ span [] [ iconButton "plus" ToggleSelectCipherTypeVisible ]
146+ ]
147+ ++ optionals cipherSelectDropdownVisivble
148+ [ div
149+ [ Attr.class "overlay"
150+ , Ev.onClick ToggleSelectCipherTypeVisible
151+ ]
152+ []
153+ , span
154+ [ Attr.class "p-contextual-menu__dropdown"
155+ , Attr.attribute "aria-hidden" "false"
156+ ]
157+ (let
158+ create t =
159+ CreateNewCipher t
160+ in
161+ [ button
162+ [ Attr.class "p-contextual-menu__link"
163+ , Ev.onClick (create Bridge.LoginType)
165164 ]
166− )
167− ]
168− ]
169− , event =
170− \model ev ->
171− case ev of
172− GlobalEvents.UpdateCipher c ->
173− { model
174− | ciphers =
175− model.ciphers
176− |> List.map
177− (\cipher ->
178− if cipher.id == c.id then
179− { cipher | name = c.name }
180−
181− else
182− cipher
183− )
184− }
185−
186− _ ->
187− model
188− }
165+ [ text "Login" ]
166+ , button
167+ [ Attr.class "p-contextual-menu__link"
168+ , Ev.onClick (create Bridge.CardType)
169+ ]
170+ [ text "Card" ]
171+ , button
172+ [ Attr.class "p-contextual-menu__link"
173+ , Ev.onClick (create Bridge.NoteType)
174+ ]
175+ [ text "Note" ]
176+ , button
177+ [ Attr.class "p-contextual-menu__link"
178+ , Ev.onClick (create Bridge.IdentityType)
179+ ]
180+ [ text "Contact" ]
181+ ]
182+ )
183+ ]
184+ )
185+ ]
186+ ]
189187
190188
191−init : Bridge.Sub_LoadCiphers_List -> ( Model, Cmd Msg )
192−init ciphers =
193− ( { ciphers = ciphers
194− , search = ""
195− , menuVisible = False
196− , ciphersListFilter = AllCiphers
197− , scroll = InfiniteScroll.init (LoadMore >> pureCmd)
198− , sublist = { end = 20 }
199− , cipherSelectDropdownVisivble = False
200− }
201− , Cmd.none
202− )
189+init : Callbacks emsg -> Bridge.Sub_LoadCiphers_List -> Model emsg
190+init callbacks ciphers =
191+ { ciphers = ciphers
192+ , search = ""
193+ , menuVisible = False
194+ , ciphersListFilter = AllCiphers
195+ , scroll = InfiniteScroll.init (LoadMore >> pureCmd)
196+ , sublist = { end = 20 }
197+ , cipherSelectDropdownVisivble = False
198+ , callbacks = callbacks
199+ }
203200
204201
205−update : Callbacks emsg -> (Msg -> emsg) -> Msg -> Model -> ( Result String Model, Cmd emsg )
206−update { logOut, createNewCipher } liftMsg msg model =
202+update : Msg -> Model emsg -> ( Model emsg, Cmd emsg )
203+update msg model =
207204 case msg of
208205 Noop ->
209− ( Ok model, Cmd.none )
206+ ( model, Cmd.none )
210207
211208 UpdateSearch s ->
212− ( Ok { model | search = s }, Cmd.none )
209+ ( { model | search = s }, Cmd.none )
213210
214211 ToggleMenu ->
215− ( Ok { model | menuVisible = not model.menuVisible }, Cmd.none )
212+ ( { model | menuVisible = not model.menuVisible }, Cmd.none )
216213
217214 UpdateFilter filter ->
218− ( Ok { model | ciphersListFilter = filter, menuVisible = False }, Cmd.none )
215+ ( { model | ciphersListFilter = filter, menuVisible = False }, Cmd.none )
219216
220217 LogOut ->
221− ( Ok model, pureCmd logOut )
218+ ( model, pureCmd model.callbacks.logOut )
222219
223220 LoadMore direction ->
224221 let
@@ -230,53 +227,51 @@ update { logOut, createNewCipher } liftMsg msg model =
230227 InfiniteScroll.Bottom ->
231228 { end = min (model.sublist.end + 10) (List.length model.ciphers) }
232229 in
233− ( Ok { model | sublist = newSublist, scroll = InfiniteScroll.stopLoading model.scroll }, Cmd.none )
230+ ( { model | sublist = newSublist, scroll = InfiniteScroll.stopLoading model.scroll }, Cmd.none )
234231
235232 InfiniteScrollMsg msg_ ->
236233 let
237234 ( scroll, cmd ) =
238235 InfiniteScroll.update InfiniteScrollMsg msg_ model.scroll
239236 in
240− ( Ok { model | scroll = scroll }, Cmd.map liftMsg cmd )
237+ ( { model | scroll = scroll }, Cmd.map model.callbacks.lift cmd )
241238
242239 CreateNewCipher t ->
243− ( Ok { model | cipherSelectDropdownVisivble = False }, pureCmd (createNewCipher t) )
240+ ( { model | cipherSelectDropdownVisivble = False }, pureCmd (model.callbacks.createNewCipher t) )
244241
245242 ToggleSelectCipherTypeVisible ->
246− ( Ok { model | cipherSelectDropdownVisivble = not model.cipherSelectDropdownVisivble }
243+ ( { model | cipherSelectDropdownVisivble = not model.cipherSelectDropdownVisivble }
247244 , Cmd.none
248245 )
249246
250−
251−subscriptions : Model -> Sub Msg
252−subscriptions _ =
253− Sub.none
247+ SelectCipher id ->
248+ ( model, model.callbacks.selected id |> pureCmd )
254249
255250
256−view : Callbacks emsg -> (Msg -> emsg) -> Model -> List (Html emsg)
257−view { selected } liftMsg { ciphers, search, ciphersListFilter, sublist } =
251+view : Model emsg -> List (Html Msg)
252+view { ciphers, search, ciphersListFilter, sublist } =
258253 [ input
259254 [ Attr.type_ "search"
260255 , Attr.value search
261− , Ev.onInput (liftMsg << UpdateSearch)
256+ , Ev.onInput UpdateSearch
262257 , Attr.placeholder "Search"
263258 , Attr.attribute "autocomplete" "off"
264259 ]
265260 []
266261 , ciphers
267262 |> applyCipherFilter ciphersListFilter
268− |> Search.searchList search .name (List.take sublist.end >> Lazy.lazy3 showCiphers liftMsg selected)
263+ |> Search.searchList search .name (List.take sublist.end >> Lazy.lazy showCiphers)
269264 ]
270265
271266
272−showCiphers : (Msg -> msg) -> (String -> msg) -> Bridge.Sub_LoadCiphers_List -> Html msg
273−showCiphers liftMsg selected ciphers =
274− Keyed.ul [ Attr.class "p-list--divided", InfiniteScroll.infiniteScroll (InfiniteScrollMsg >> liftMsg), Attr.class "ciphers-list no-scroll-bar" ]
267+showCiphers : Bridge.Sub_LoadCiphers_List -> Html Msg
268+showCiphers ciphers =
269+ Keyed.ul [ Attr.class "p-list--divided", InfiniteScroll.infiniteScroll InfiniteScrollMsg, Attr.class "ciphers-list no-scroll-bar" ]
275270 (ciphers
276271 |> List.map
277272 (\{ name, date, id } ->
278273 ( id
279− , li [ Attr.class "p-list__item cipher-row", Ev.onClick (selected id) ]
274+ , li [ Attr.class "p-list__item cipher-row", Ev.onClick (SelectCipher id) ]
280275 [ div [ Attr.class "cipher-row-container" ]
281276 [ p [] [ text name ]
282277 , p [ Attr.class "p-text--small u-align-text--right u-text--muted" ] [ text date ]
frontend/Pages/EditCipher.elm+61−61
@@ -1,4 +1,4 @@
1−module Pages.EditCipher exposing (..)
1+module Pages.EditCipher exposing (Callbacks, Model, Msg, event, init, title, update, view)
22
33import Bridge
44import GlobalEvents
@@ -8,6 +8,7 @@ import Html.Events as Ev
88import Logic.Cipher exposing (normalizeCipher)
99import Modal exposing (modal)
1010import Page exposing (..)
11+import Pages.MasterPassword exposing (Msg)
1112import Pages.PasswordGenerator as PasswordGenerator
1213import Utils exposing (..)
1314
@@ -26,6 +27,7 @@ type Msg
2627 | CloseGeneratePassword
2728 | GeneratePassword Bridge.PasswordGeneratorConfig
2829 | Delete
30+ | Save
2931
3032
3133type alias Callbacks msg =
@@ -35,56 +37,50 @@ type alias Callbacks msg =
3537 }
3638
3739
38−page : Page { fullCipher : Bridge.FullCipher, callbacks : Callbacks emsg } (Model emsg) Msg emsg
39−page liftMsg =
40− { init = init
41− , view = \model -> view model |> List.map (Html.map liftMsg)
42− , update = \msg model -> update liftMsg msg model
43− , subscriptions = \model -> subscriptions model |> Sub.map liftMsg
44− , title =
45− \{ fullCipher, callbacks } ->
46− [ text fullCipher.name
47− , span [ Attr.class "u-float-right" ]
48− [ iconButton "task-outstanding"
49− (callbacks.save { fullCipher | cipher = normalizeCipher fullCipher.cipher })
50− ]
51− ]
52− , event =
53− \model ev ->
54− case ev of
55− GlobalEvents.UpdateCipher c ->
56− { model
57− | fullCipher =
58− if c.id == model.fullCipher.id then
59− c
60−
61− else
62− model.fullCipher
40+title : Model msg -> List (Html Msg)
41+title { fullCipher } =
42+ [ text fullCipher.name
43+ , span [ Attr.class "u-float-right" ]
44+ [ iconButton "task-outstanding" Save
45+ ]
46+ ]
47+
48+
49+event : Model msg -> GlobalEvents.Event -> Model msg
50+event model ev =
51+ case ev of
52+ GlobalEvents.UpdateCipher c ->
53+ { model
54+ | fullCipher =
55+ if c.id == model.fullCipher.id then
56+ c
57+
58+ else
59+ model.fullCipher
60+ }
61+
62+ GlobalEvents.GeneratedPassword password ->
63+ let
64+ fullCipher =
65+ model.fullCipher
66+ in
67+ { model
68+ | passwordGenerator = Nothing
69+ , fullCipher =
70+ { fullCipher
71+ | cipher =
72+ case fullCipher.cipher of
73+ Bridge.LoginCipher c ->
74+ Bridge.LoginCipher
75+ { c | password = Just password }
76+
77+ c ->
78+ c
6379 }
80+ }
6481
65− GlobalEvents.GeneratedPassword password ->
66− let
67− fullCipher =
68− model.fullCipher
69− in
70− { model
71− | passwordGenerator = Nothing
72− , fullCipher =
73− { fullCipher
74− | cipher =
75− case fullCipher.cipher of
76− Bridge.LoginCipher c ->
77− Bridge.LoginCipher
78− { c | password = Just password }
79−
80− c ->
81− c
82− }
83− }
84−
85− _ ->
86− model
87− }
82+ _ ->
83+ model
8884
8985
9086init : { fullCipher : Bridge.FullCipher, callbacks : Callbacks emsg } -> ( Model emsg, Cmd emsg )
@@ -97,38 +93,42 @@ init { fullCipher, callbacks } =
9793 )
9894
9995
100−update : (Msg -> emsg) -> Msg -> Model emsg -> ( Result String (Model emsg), Cmd emsg )
101−update _ msg model =
96+update : Msg -> Model emsg -> ( Model emsg, Cmd emsg )
97+update msg model =
10298 case msg of
10399 EditCipher fullCipher ->
104− ( Ok { model | fullCipher = fullCipher }, Cmd.none )
100+ ( { model | fullCipher = fullCipher }, Cmd.none )
105101
106102 PasswordGeneratorMsg m ->
107− ( Ok { model | passwordGenerator = Maybe.map (PasswordGenerator.update m) model.passwordGenerator }
103+ ( { model | passwordGenerator = Maybe.map (PasswordGenerator.update m) model.passwordGenerator }
108104 , Cmd.none
109105 )
110106
111107 OpenGeneratePasword ->
112− ( Ok { model | passwordGenerator = Just PasswordGenerator.init }
108+ ( { model | passwordGenerator = Just PasswordGenerator.init }
113109 , Cmd.none
114110 )
115111
116112 CloseGeneratePassword ->
117− ( Ok { model | passwordGenerator = Nothing }, Cmd.none )
113+ ( { model | passwordGenerator = Nothing }, Cmd.none )
118114
119115 GeneratePassword cfg ->
120− ( Ok model, pureCmd (model.callbacks.generatePassword cfg) )
116+ ( model, pureCmd (model.callbacks.generatePassword cfg) )
121117
122118 Delete ->
123− ( Ok model
119+ ( model
124120 , Maybe.withDefault Cmd.none
125121 (Maybe.map (\f -> f model.fullCipher |> pureCmd) model.callbacks.delete)
126122 )
127123
128−
129−subscriptions : Model emsg -> Sub Msg
130−subscriptions _ =
131− Sub.none
124+ Save ->
125+ let
126+ { fullCipher } =
127+ model
128+ in
129+ ( model
130+ , model.callbacks.save { fullCipher | cipher = normalizeCipher fullCipher.cipher } |> pureCmd
131+ )
132132
133133
134134heading : String -> String -> Html msg
frontend/Pages/Login.elm+38−42
@@ -1,4 +1,4 @@
1−module Pages.Login exposing (Callbacks, Model, Msg, page)
1+module Pages.Login exposing (Callbacks, Model, Msg, init, title, update, view)
22
33import Html exposing (..)
44import Html.Attributes as Attr
@@ -7,10 +7,11 @@ import Page exposing (..)
77import Utils exposing (..)
88
99
10−type alias Model =
10+type alias Model emsg =
1111 { server : String
1212 , email : String
1313 , password : String
14+ , callbacks : Callbacks emsg
1415 }
1516
1617
@@ -26,53 +27,49 @@ type alias Callbacks emsg =
2627 }
2728
2829
29−page : Callbacks emsg -> Page () Model Msg emsg
30−page callbacks liftMsg =
31− { init = \() -> Tuple.mapSecond (Cmd.map liftMsg) init
32− , view = \model -> view model |> List.map (Html.map liftMsg)
33− , update = \msg model -> update callbacks liftMsg msg model
34− , subscriptions = \model -> subscriptions model |> Sub.map liftMsg
35− , title = always [ text "Log into bitwarden" ]
36− , event = \model _ -> model
37− }
30+title : String
31+title =
32+ "Log into bitwarden"
3833
3934
40−init : ( Model, Cmd Msg )
41−init =
42− ( { server = "https://vault.bitwarden.com"
43− , email = ""
44− , password = ""
45− }
46− , Cmd.none
47− )
35+init : Callbacks emsg -> Model emsg
36+init callbacks =
37+ { server = "https://vault.bitwarden.com"
38+ , email = ""
39+ , password = ""
40+ , callbacks = callbacks
41+ }
4842
4943
50−update : Callbacks emsg -> (Msg -> emsg) -> Msg -> Model -> ( Result String Model, Cmd emsg )
51−update { submit } liftMsg msg model =
44+update : Msg -> Model emsg -> ( Model emsg, Cmd emsg )
45+update msg model =
5246 case msg of
5347 UpdateEmail email ->
54− ( Ok { model | email = email }, Cmd.none )
48+ ( { model | email = email }, Cmd.none )
5549
5650 UpdateServer server ->
57− ( Ok { model | server = server }, Cmd.none )
51+ ( { model | server = server }, Cmd.none )
5852
5953 UpdatePassword password ->
60− ( Ok { model | password = password }, Cmd.none )
54+ ( { model | password = password }, Cmd.none )
6155
6256 Submit ->
63− ( Ok model, pureCmd (submit model) )
64−
65−
66−subscriptions : Model -> Sub Msg
67−subscriptions model =
68− Sub.none
69−
70−
71−view : Model -> List (Html Msg)
57+ ( model
58+ , pureCmd
59+ (model.callbacks.submit
60+ { email = model.email
61+ , password = model.password
62+ , server = model.server
63+ }
64+ )
65+ )
66+
67+
68+view : Model emsg -> List (Html Msg)
7269view model =
7370 [ form [ Ev.onSubmit Submit ]
74− ([ label [] [ text "Server" ]
75− , input
71+ [ label [] [ text "Server" ]
72+ , input
7673 [ Attr.type_ "url"
7774 , Attr.value model.server
7875 , Ev.onInput UpdateServer
@@ -80,8 +77,8 @@ view model =
8077 , Attr.required True
8178 ]
8279 []
83− , label [] [ text "Email" ]
84− , input
80+ , label [] [ text "Email" ]
81+ , input
8582 [ Attr.type_ "email"
8683 , Attr.value model.email
8784 , Ev.onInput UpdateEmail
@@ -89,8 +86,8 @@ view model =
8986 , Attr.required True
9087 ]
9188 []
92− , label [] [ text "Password" ]
93− , input
89+ , label [] [ text "Password" ]
90+ , input
9491 [ Attr.type_ "password"
9592 , Attr.value model.password
9693 , Ev.onInput UpdatePassword
@@ -98,7 +95,6 @@ view model =
9895 , Attr.required True
9996 ]
10097 []
101− ]
102− ++ [ alignRight [ button [ Attr.type_ "submit" ] [ text "Log in" ] ] ]
103− )
98+ , alignRight [ button [ Attr.type_ "submit" ] [ text "Log in" ] ]
99+ ]
104100 ]
frontend/Pages/MasterPassword.elm+19−30
@@ -1,4 +1,4 @@
1−module Pages.MasterPassword exposing (..)
1+module Pages.MasterPassword exposing (Callbacks, Model, Msg, init, title, update, view)
22
33import Html exposing (..)
44import Html.Attributes as Attr
@@ -7,10 +7,11 @@ import Page exposing (..)
77import Utils exposing (..)
88
99
10−type alias Model =
10+type alias Model emsg =
1111 { server : String
1212 , email : String
1313 , password : String
14+ , callbacks : Callbacks emsg
1415 }
1516
1617
@@ -26,46 +27,34 @@ type alias Callbacks emsg =
2627 }
2728
2829
29−page : Callbacks emsg -> Page { server : String, login : String } Model Msg emsg
30−page callbacks liftMsg =
31− { init = \cfg -> Tuple.mapSecond (Cmd.map liftMsg) (init cfg)
32− , view = \model -> view model |> List.map (Html.map liftMsg)
33− , update = \msg model -> update callbacks liftMsg msg model
34− , subscriptions = \model -> subscriptions model |> Sub.map liftMsg
35− , title = always [ text "Enter master password" ]
36− , event = \model _ -> model
37− }
30+title : String
31+title =
32+ "Enter master password"
3833
3934
40−init : { server : String, login : String } -> ( Model, Cmd Msg )
41−init { server, login } =
42− ( { server = server
43− , email = login
44− , password = ""
45− }
46− , Cmd.none
47− )
35+init : Callbacks emsg -> { server : String, login : String } -> Model emsg
36+init callbacks { server, login } =
37+ { server = server
38+ , email = login
39+ , password = ""
40+ , callbacks = callbacks
41+ }
4842
4943
50−update : Callbacks emsg -> (Msg -> emsg) -> Msg -> Model -> ( Result String Model, Cmd emsg )
51−update { submit, reset } _ msg model =
44+update : Msg -> Model emsg -> ( Model emsg, Cmd emsg )
45+update msg model =
5246 case msg of
5347 UpdatePassword password ->
54− ( Ok { model | password = password }, Cmd.none )
48+ ( { model | password = password }, Cmd.none )
5549
5650 Submit ->
57− ( Ok model, pureCmd (submit { password = model.password }) )
51+ ( model, pureCmd (model.callbacks.submit { password = model.password }) )
5852
5953 Reset ->
60− ( Ok model, pureCmd reset )
61−
62−
63−subscriptions : Model -> Sub Msg
64−subscriptions _ =
65− Sub.none
54+ ( model, pureCmd model.callbacks.reset )
6655
6756
68−view : Model -> List (Html Msg)
57+view : Model emsg -> List (Html Msg)
6958view model =
7059 [ alignRight [ button [ Attr.class "p-button--negative", Ev.onClick Reset ] [ text "Log out" ] ]
7160 , table [ Attr.class "p-table--mobile-card u-no-margin" ]
frontend/Pages/SecondFactor.elmnew file+91
@@ -0,0 +1,91 @@
1+module Pages.SecondFactor exposing
2+ ( Callbacks
3+ , Model
4+ , Msg
5+ , init
6+ , title
7+ , update
8+ , view
9+ )
10+
11+import Bridge
12+import Html exposing (..)
13+import Html.Attributes as Attr
14+import Html.Events as Ev
15+import Logic.SecondFactor exposing (secondFactorName)
16+import Utils exposing (..)
17+
18+
19+type alias Callbacks msg =
20+ { submit : { token : String, remember : Bool } -> msg
21+ }
22+
23+
24+type Msg
25+ = Submit
26+ | UpdateValue String
27+ | UpdateRemember Bool
28+
29+
30+type alias Model msg =
31+ { token : String
32+ , remember : Bool
33+ , factorType : Bridge.TwoFactorProviderType
34+ , callbacks : Callbacks msg
35+ }
36+
37+
38+init :
39+ { provider : Bridge.TwoFactorProviderType
40+ , callbacks : Callbacks msg
41+ }
42+ -> Model msg
43+init { provider, callbacks } =
44+ { token = ""
45+ , remember = True
46+ , factorType = provider
47+ , callbacks = callbacks
48+ }
49+
50+
51+title : Model msg -> String
52+title { factorType } =
53+ secondFactorName factorType
54+
55+
56+update : Model emsg -> Msg -> ( Model emsg, Maybe emsg )
57+update model msg =
58+ case msg of
59+ Submit ->
60+ ( model, model.callbacks.submit { token = model.token, remember = model.remember } |> Just )
61+
62+ UpdateValue token ->
63+ ( { model | token = token }, Nothing )
64+
65+ UpdateRemember remember ->
66+ ( { model | remember = remember }, Nothing )
67+
68+
69+view : Model emsg -> Html Msg
70+view { factorType, token, remember } =
71+ form [ Ev.onSubmit Submit ]
72+ [ label [] [ secondFactorName factorType ++ " code" |> text ]
73+ , input
74+ [ Attr.type_ "number"
75+ , Attr.value token
76+ , Attr.required True
77+ , Ev.onInput UpdateValue
78+ , Attr.attribute "autocomplete" "one-time-code"
79+ ]
80+ []
81+ , label []
82+ [ input
83+ [ Attr.type_ "checkbox"
84+ , Attr.checked remember
85+ , Ev.onCheck UpdateRemember
86+ ]
87+ []
88+ , text "Remember this device"
89+ ]
90+ , alignRight [ button [ Attr.type_ "submit" ] [ text "Submit" ] ]
91+ ]
frontend/Pages/SecondFactorSelect.elmnew file+163
@@ -0,0 +1,163 @@
1+module Pages.SecondFactorSelect exposing
2+ ( Callbacks
3+ , Model
4+ , Msg
5+ , init
6+ , main
7+ , title
8+ , update
9+ , view
10+ )
11+
12+import Bridge
13+import Element as El
14+import Element.Font as Font
15+import Element.Input as Input
16+import Html
17+import Html.Attributes as Attr
18+import Logic.SecondFactor exposing (secondFactorName)
19+import Page exposing (..)
20+import Utils exposing (..)
21+
22+
23+main : Program () (Model (Maybe Msg)) (Maybe Msg)
24+main =
25+ Page.page
26+ { init =
27+ init { selectFactor = always Nothing }
28+ [ Bridge.Duo
29+ , Bridge.Email
30+ ]
31+ , view = view
32+ , update = update
33+ }
34+
35+
36+type alias Model emsg =
37+ { availableFactors : List Bridge.TwoFactorProviderType
38+ , callbacks : Callbacks emsg
39+ }
40+
41+
42+type Msg
43+ = SelectFactor Bridge.TwoFactorProviderType
44+
45+
46+type alias Callbacks emsg =
47+ { selectFactor : Bridge.TwoFactorProviderType -> emsg
48+ }
49+
50+
51+title : String
52+title =
53+ "Second factor"
54+
55+
56+init : Callbacks emsg -> List Bridge.TwoFactorProviderType -> Model emsg
57+init callbacks providers =
58+ { availableFactors = providers
59+ , callbacks = callbacks
60+ }
61+
62+
63+update : Msg -> Model emsg -> ( Model emsg, Cmd emsg )
64+update msg model =
65+ case msg of
66+ SelectFactor factor ->
67+ ( model, model.callbacks.selectFactor factor |> pureCmd )
68+
69+
70+view : Model emsg -> List (Html.Html Msg)
71+view { availableFactors } =
72+ let
73+ ( supportedProviders, unsupportedProviders ) =
74+ List.partition providerIsSupported availableFactors
75+ in
76+ [ El.layout [ El.padding 8 ]
77+ (El.column
78+ [ El.width El.fill
79+ , El.spacing 8
80+ ]
81+ ([ if List.isEmpty supportedProviders then
82+ El.column []
83+ [ El.paragraph []
84+ [ El.text "Unfortunately, your account has no compatible 2FA providers."
85+ ]
86+ , El.el
87+ [ El.height (El.px 16)
88+ ]
89+ El.none
90+ ]
91+
92+ else
93+ El.column [ El.centerX ]
94+ (supportedProviders
95+ |> List.map
96+ (\factor ->
97+ Input.button [ El.centerX ]
98+ { label =
99+ Html.button [ Attr.class "p-button" ] [ secondFactorName factor |> Html.text ]
100+ |> El.html
101+ , onPress = SelectFactor factor |> Just
102+ }
103+ )
104+ )
105+ ]
106+ ++ (if List.isEmpty unsupportedProviders |> not then
107+ [ Html.div
108+ [ Attr.class "p-notification--information"
109+ ]
110+ [ Html.div [ Attr.class "p-notification__content" ]
111+ [ Html.h5 [ Attr.class "p-notification__title" ]
112+ [ Html.text "The following 2FA providers are not supported"
113+ ]
114+ , Html.p [ Attr.class "notification__message" ]
115+ [ Html.ul []
116+ (List.map
117+ (secondFactorName >> Html.text >> List.singleton >> Html.li [])
118+ unsupportedProviders
119+ )
120+ ]
121+ ]
122+ ]
123+ |> El.html
124+ |> El.el
125+ [ Font.size 16
126+ , El.width El.fill
127+ ]
128+ ]
129+
130+ else
131+ []
132+ )
133+ )
134+ )
135+ ]
136+
137+
138+providerIsSupported : Bridge.TwoFactorProviderType -> Bool
139+providerIsSupported provider =
140+ case provider of
141+ Bridge.Authenticator ->
142+ True
143+
144+ Bridge.Duo ->
145+ False
146+
147+ Bridge.Email ->
148+ True
149+
150+ Bridge.OrganizationDuo ->
151+ False
152+
153+ Bridge.Remember ->
154+ False
155+
156+ Bridge.U2f ->
157+ False
158+
159+ Bridge.WebAuthn ->
160+ False
161+
162+ Bridge.Yubikey ->
163+ False
scratchpad/password_generator.html+16−1
@@ -5,7 +5,7 @@
55 <meta charset="UTF-8">
66 <meta http-equiv="X-UA-Compatible" content="IE=edge">
77 <meta name="viewport" content="width=device-width, initial-scale=1.0">
8− <link rel="stylesheet" href="../vanilla/style.css">
8+ <link rel="stylesheet" href="../vanilla/dist/style.css">
99 <link rel="stylesheet" href="../style.css">
1010 <title>Password generator</title>
1111
@@ -20,6 +20,14 @@
2020
2121<body>
2222
23+ <div class="select-second-factor-container">
24+ <button class="p-button">Hello</button>
25+ <button class="p-button">Bar</button>
26+ <button class="p-button">Bar</button>
27+ <button class="p-button">Bardi</button>
28+ <button class="p-button">Wolrd</button>
29+ </div>
30+
2331 <div class="password-generator-group">
2432 <p>Password length</p>
2533 <input type="number" class="p-slider__input">
@@ -35,6 +43,13 @@
3543 <input type="number" class="p-slider__input">
3644 </div>
3745
46+ <form action="">
47+ <label><input type="checkbox" name="" id="">Hahahah</label>
48+ <input type="checkbox">
49+
50+ </form>
51+
52+
3853
3954 <input type="range" min="0" max="100" value="50" step="1">
4055 <input type="range" min="0" max="100" value="50" step="1"
spago.dhall+3
@@ -28,7 +28,10 @@ to generate this file without the comments in this block.
2828 , "either"
2929 , "exceptions"
3030 , "foldable-traversable"
31+ , "foreign"
32+ , "foreign-object"
3133 , "functions"
34+ , "literals"
3235 , "maybe"
3336 , "newtype"
3437 , "nullable"
style.css+7
@@ -132,6 +132,13 @@ textarea {
132132 bottom: 0;
133133}
134134
135+.select-second-factor-container {
136+ display: flex;
137+ flex-direction: column;
138+ align-items: center;
139+ justify-content: center;
140+}
141+
135142.p-tabs__list {
136143 overflow: initial;
137144}
vanilla+1−1

Submodule changed.