@@ -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+ }