on master help sign in
wip
e21d0baiko
backend/Bridge.purs+22−13
@@ -39,25 +39,34 @@ derive instance genericCmd :: Generic Cmd _
39derive instance eqCmd :: Eq Cmd
39derive instance eqCmd :: Eq Cmd
40derive instance ordCmd :: Ord Cmd
40derive instance ordCmd :: Ord Cmd
41
41
42−newtype Sub_Hello =
42+newtype Sub_LoadCiphers =
43− Sub_Hello {
43+ Sub_LoadCiphers {
44− first :: String
44+ date :: String
45− , second :: String
45+ , name :: String
46 }
46 }
47
47
48−instance encodeJsonSub_Hello :: EncodeJson Sub_Hello where
48+instance encodeJsonSub_LoadCiphers :: EncodeJson Sub_LoadCiphers where
49 encodeJson = genericEncodeAeson Argonaut.defaultOptions
49 encodeJson = genericEncodeAeson Argonaut.defaultOptions
50−instance decodeJsonSub_Hello :: DecodeJson Sub_Hello where
50+instance decodeJsonSub_LoadCiphers :: DecodeJson Sub_LoadCiphers where
51 decodeJson = genericDecodeAeson Argonaut.defaultOptions
51 decodeJson = genericDecodeAeson Argonaut.defaultOptions
52−derive instance genericSub_Hello :: Generic Sub_Hello _
52+derive instance genericSub_LoadCiphers :: Generic Sub_LoadCiphers _
53−derive instance eqSub_Hello :: Eq Sub_Hello
53+derive instance eqSub_LoadCiphers :: Eq Sub_LoadCiphers
54−derive instance ordSub_Hello :: Ord Sub_Hello
54+derive instance ordSub_LoadCiphers :: Ord Sub_LoadCiphers
55+
56+newtype Sub_LoadCiphers_List =
57+ Sub_LoadCiphers_List (Array Sub_LoadCiphers)
58+
59+instance encodeJsonSub_LoadCiphers_List :: EncodeJson Sub_LoadCiphers_List where
60+ encodeJson = genericEncodeAeson Argonaut.defaultOptions
61+instance decodeJsonSub_LoadCiphers_List :: DecodeJson Sub_LoadCiphers_List where
62+ decodeJson = genericDecodeAeson Argonaut.defaultOptions
63+derive instance genericSub_LoadCiphers_List :: Generic Sub_LoadCiphers_List _
64+derive instance eqSub_LoadCiphers_List :: Eq Sub_LoadCiphers_List
65+derive instance ordSub_LoadCiphers_List :: Ord Sub_LoadCiphers_List
55
66
56data Sub =
67data Sub =
57− Empty
58− | Error String
68+ Error String
59− | GotEverything String
69+ | LoadCiphers Sub_LoadCiphers_List
60− | Hello Sub_Hello
61 | NeedsLogin
70 | NeedsLogin
62
71
63instance encodeJsonSub :: EncodeJson Sub where
72instance encodeJsonSub :: EncodeJson Sub where
backend/Main.purs+24−11
@@ -2,21 +2,27 @@ module Main
2 ( main
2 ( main
3 ) where
3 ) where
4
4
5+import Bridge
5import Prelude
6import Prelude
7+import BW (CryptoService)
6import BW as WB
8import BW as WB
7−import BW.Logic (decrypt)
9+import BW.Logic (decrypt, liftPromise)
8import BW.Logic as Logic
10import BW.Logic as Logic
9−import BW.Types (Email(..), EncryptedString(..), Password(..), Urls)
11+import BW.Types (Email(..), EncryptedString(..), Password(..), Urls, CipherResponse)
10import Bridge as Bridge
12import Bridge as Bridge
13+import Control.El (class HasL, Al, L(..), l)
11import Control.Monad.Reader (runReaderT)
14import Control.Monad.Reader (runReaderT)
12import Control.Promise (Promise)
15import Control.Promise (Promise)
13import Control.Promise as Promise
16import Control.Promise as Promise
14import Data.Argonaut (class EncodeJson, encodeJson)
17import Data.Argonaut (class EncodeJson, encodeJson)
15import Data.Argonaut as Json
18import Data.Argonaut as Json
19+import Data.Array as A
16import Data.Either (Either(..))
20import Data.Either (Either(..))
17import Data.Maybe (Maybe(..))
21import Data.Maybe (Maybe(..))
18import Data.Nullable (Nullable, null, toNullable)
22import Data.Nullable (Nullable, null, toNullable)
19import Data.Nullable as Nullable
23import Data.Nullable as Nullable
24+import Data.SymmetricCryptoKey (SymmetricCryptoKey)
25+import Data.Timestamp as Timestamp
20import Data.Traversable (traverse)
26import Data.Traversable (traverse)
21import Effect (Effect)
27import Effect (Effect)
22import Effect.Aff (Aff, runAff_, try)
28import Effect.Aff (Aff, runAff_, try)
@@ -41,7 +47,7 @@ main = do
41 urls = baseUrl server
47 urls = baseUrl server
42 runElmAff app do
48 runElmAff app do
43 unauthedApi <- liftPromise $ services.getApi urls null
49 unauthedApi <- liftPromise $ services.getApi urls null
44− { masterKey, token } <-
50+ { masterKey, token, key } <-
45 flip runReaderT { api: unauthedApi, crypto: services.crypto } do
51 flip runReaderT { api: unauthedApi, crypto: services.crypto } do
46 token <-
52 token <-
47 try (Logic.getLogInRequestToken (Email email) (Password password))
53 try (Logic.getLogInRequestToken (Email email) (Password password))
@@ -49,21 +55,28 @@ main = do
49 Left _ -> liftEffect $ Exc.throw "Could not log in. Please check your data and try again."
55 Left _ -> liftEffect $ Exc.throw "Could not log in. Please check your data and try again."
50 Right t -> pure t
56 Right t -> pure t
51 masterKey <- Logic.makePreloginKey (Email email) (Password password)
57 masterKey <- Logic.makePreloginKey (Email email) (Password password)
58+ key <- Logic.makeDecryptionKey masterKey token.key
52− pure { token, masterKey }
59+ pure { token, masterKey, key }
53 api <- liftPromise $ services.getApi urls (nullify token)
60 api <- liftPromise $ services.getApi urls (nullify token)
54 _ <-
61 _ <-
55− flip runReaderT { api: api, crypto: services.crypto } do
62+ flip runReaderT { api: api, crypto: services.crypto, key: key } do
56 sync <- liftPromise $ api.getSync unit
63 sync <- liftPromise $ api.getSync unit
57− key <- Logic.makeDecryptionKey masterKey sync.profile.key
64+ processedCiphers <- traverse processCipher sync.ciphers
58− log $ show key.keyB64
65+ liftEffect $ Elm.send app $ LoadCiphers $ Bridge.Sub_LoadCiphers_List (A.take 3 processedCiphers)
59− ciphersNames <- traverse (decrypt key) $ map _.name sync.ciphers
60− liftEffect $ Elm.send app $ Bridge.Hello $ Bridge.Sub_Hello { first: "hello", second: show ciphersNames }
61 pure unit
66 pure unit
62 pure unit
67 pure unit
63 Bridge.Init -> Elm.send app (Bridge.NeedsLogin)
68 Bridge.Init -> Elm.send app (Bridge.NeedsLogin)
64
69
65−liftPromise ∷ forall m a. MonadAff m ⇒ Promise a → m a
70+processCipher ::
66−liftPromise = Aff.liftAff <<< Promise.toAff
71+ forall r.
72+ HasL "crypto" CryptoService r =>
73+ HasL "key" SymmetricCryptoKey r =>
74+ CipherResponse -> Al r Bridge.Sub_LoadCiphers
75+processCipher cipher = do
76+ name <- decrypt cipher.name
77+ pure
78+ $ Bridge.Sub_LoadCiphers
79+ { name: name, date: Timestamp.toLocalDateTimeString cipher.revisionDate }
67
80
68runElmAff :: FFI.Elm -> Aff Unit -> Effect Unit
81runElmAff :: FFI.Elm -> Aff Unit -> Effect Unit
69runElmAff app =
82runElmAff app =
bridge.yaml+4−5
@@ -1,11 +1,10 @@
1Sub:
1Sub:
2−- Hello:
3− first: String
4− second: String
5−- GotEverything: String
6−- Empty
7- NeedsLogin
2- NeedsLogin
8- Error: String
3- Error: String
4+- LoadCiphers:
5+ List:
6+ name: String
7+ date: String
9
8
10Cmd:
9Cmd:
11- Init
10- Init
bw/spago.dhall+1
@@ -6,6 +6,7 @@
6 , "argonaut-generic"
6 , "argonaut-generic"
7 , "arraybuffer-types"
7 , "arraybuffer-types"
8 , "console"
8 , "console"
9+ , "datetime-parsing"
9 , "effect"
10 , "effect"
10 , "el"
11 , "el"
11 , "foreign"
12 , "foreign"
bw/src/BW/Logic.purs+6−3
@@ -5,6 +5,7 @@ import BW.Types
5import Control.El
5import Control.El
6import Prelude
6import Prelude
7import Control.Monad.Error.Class (throwError)
7import Control.Monad.Error.Class (throwError)
8+import Control.Promise (Promise)
8import Control.Promise as Promise
9import Control.Promise as Promise
9import Data.EncString as EncString
10import Data.EncString as EncString
10import Data.Function.Uncurried (runFn2, runFn3, runFn4)
11import Data.Function.Uncurried (runFn2, runFn3, runFn4)
@@ -13,7 +14,7 @@ import Data.Nullable (null, toNullable)
13import Data.String as String
14import Data.String as String
14import Data.SymmetricCryptoKey (SymmetricCryptoKey)
15import Data.SymmetricCryptoKey (SymmetricCryptoKey)
15import Data.SymmetricCryptoKey as SymmetricCryptoKey
16import Data.SymmetricCryptoKey as SymmetricCryptoKey
16−import Effect.Aff.Class (liftAff)
17+import Effect.Aff.Class (class MonadAff, liftAff)
17import Effect.Class (liftEffect)
18import Effect.Class (liftEffect)
18import Effect.Class.Console (log)
19import Effect.Class.Console (log)
19import Effect.Exception (error)
20import Effect.Exception (error)
@@ -87,11 +88,13 @@ getLogInRequestToken email password = do
87decrypt ::
88decrypt ::
88 forall r.
89 forall r.
89 HasL "crypto" CryptoService r =>
90 HasL "crypto" CryptoService r =>
90− SymmetricCryptoKey ->
91+ HasL "key" SymmetricCryptoKey r =>
91 EncryptedString ->
92 EncryptedString ->
92 Al r String
93 Al r String
93−decrypt key input = do
94+decrypt input = do
94 crypto <- l (L :: L "crypto")
95 crypto <- l (L :: L "crypto")
96+ key <- l (L :: L "key")
95 liftPromise $ runFn2 crypto.decryptToUtf8 (EncString.fromString input) key
97 liftPromise $ runFn2 crypto.decryptToUtf8 (EncString.fromString input) key
96
98
99+liftPromise ∷ forall m a. MonadAff m ⇒ Promise a → m a
97liftPromise = liftAff <<< Promise.toAff
100liftPromise = liftAff <<< Promise.toAff
bw/src/BW/Types.purs+3−1
@@ -1,10 +1,12 @@
1module BW.Types where
1module BW.Types where
2
2
3import Prelude
3import Prelude
4+
4import Data.ArrayBuffer.Types (ArrayBuffer)
5import Data.ArrayBuffer.Types (ArrayBuffer)
5import Data.Date (Date)
6import Data.Date (Date)
6import Data.Nullable (Nullable)
7import Data.Nullable (Nullable)
7import Data.ShowableJson (ShowableJson)
8import Data.ShowableJson (ShowableJson)
9+import Data.Timestamp (Timestamp(..))
8import Data.Undefined.NoProblem (Opt)
10import Data.Undefined.NoProblem (Opt)
9
11
10newtype EncryptedString
12newtype EncryptedString
@@ -385,7 +387,7 @@ type CipherResponse
385 , edit :: Boolean
387 , edit :: Boolean
386 , viewPassword :: Boolean
388 , viewPassword :: Boolean
387 , organizationUseTotp :: Boolean
389 , organizationUseTotp :: Boolean
388− , revisionDate :: String
390+ , revisionDate :: Timestamp
389 , attachments :: Nullable (Array AttachmentResponse)
391 , attachments :: Nullable (Array AttachmentResponse)
390 , passwordHistory :: Nullable (Array PasswordHistoryResponse)
392 , passwordHistory :: Nullable (Array PasswordHistoryResponse)
391 , collectionIds :: Nullable (Array String)
393 , collectionIds :: Nullable (Array String)
bw/src/Data/EncString.purs+1−1
@@ -1,7 +1,7 @@
1module Data.EncString where
1module Data.EncString where
2
2
3import Prelude
3import Prelude
4−import BW.Types (EncryptedString(..))
4+import BW.Types (EncryptedString)
5
5
6foreign import data EncString :: Type
6foreign import data EncString :: Type
7
7
bw/src/Data/Timestamp.jsnew file+4
@@ -0,0 +1,4 @@
1+export function toLocalDateTimeString(str) {
2+ const x = new Date(str);
3+ return x.toLocaleDateString() + " " + x.toLocaleTimeString();
4+}
bw/src/Data/Timestamp.pursnew file+21
@@ -0,0 +1,21 @@
1+module Data.Timestamp where
2+
3+import Prelude
4+import Data.Bifunctor (lmap)
5+import Data.DateTime (DateTime)
6+import Data.DateTime.Parsing as DateTime
7+import Data.Either (Either(..), either)
8+import Data.Maybe (maybe)
9+import Effect (Effect)
10+import Effect.Exception (throw)
11+
12+newtype Timestamp
13+ = Timestamp String
14+
15+toDateTime :: Timestamp -> Effect DateTime
16+toDateTime (Timestamp ts) =
17+ either throw pure
18+ $ lmap (const $ "Couldn't parse date: " <> ts) (DateTime.fromString ts)
19+ >>= (DateTime.toUTC >>> maybe (Left $ "Couldn't process date " <> ts) Right)
20+
21+foreign import toLocalDateTimeString :: Timestamp -> String
frontend/Bridge.elm+28−23
@@ -53,40 +53,47 @@ jsonEncCmd val =
53
53
54
54
55
55
56−type alias Sub_Hello =
56+type alias Sub_LoadCiphers =
57− { first: String
57+ { date: String
58− , second: String
58+ , name: String
59 }
59 }
60
60
61−jsonDecSub_Hello : Json.Decode.Decoder ( Sub_Hello )
61+jsonDecSub_LoadCiphers : Json.Decode.Decoder ( Sub_LoadCiphers )
62−jsonDecSub_Hello =
62+jsonDecSub_LoadCiphers =
63− Json.Decode.succeed (\pfirst psecond -> {first = pfirst, second = psecond})
63+ Json.Decode.succeed (\pdate pname -> {date = pdate, name = pname})
64− |> required "first" (Json.Decode.string)
64+ |> required "date" (Json.Decode.string)
65− |> required "second" (Json.Decode.string)
65+ |> required "name" (Json.Decode.string)
66
66
67−jsonEncSub_Hello : Sub_Hello -> Value
67+jsonEncSub_LoadCiphers : Sub_LoadCiphers -> Value
68−jsonEncSub_Hello val =
68+jsonEncSub_LoadCiphers val =
69 Json.Encode.object
69 Json.Encode.object
70− [ ("first", Json.Encode.string val.first)
70+ [ ("date", Json.Encode.string val.date)
71− , ("second", Json.Encode.string val.second)
71+ , ("name", Json.Encode.string val.name)
72 ]
72 ]
73
73
74
74
75
75
76+type alias Sub_LoadCiphers_List = (List Sub_LoadCiphers)
77+
78+jsonDecSub_LoadCiphers_List : Json.Decode.Decoder ( Sub_LoadCiphers_List )
79+jsonDecSub_LoadCiphers_List =
80+ Json.Decode.list (jsonDecSub_LoadCiphers)
81+
82+jsonEncSub_LoadCiphers_List : Sub_LoadCiphers_List -> Value
83+jsonEncSub_LoadCiphers_List val = (Json.Encode.list jsonEncSub_LoadCiphers) val
84+
85+
86+
76type Sub =
87type Sub =
77− Empty
78− | Error String
88+ Error String
79− | GotEverything String
89+ | LoadCiphers Sub_LoadCiphers_List
80− | Hello Sub_Hello
81 | NeedsLogin
90 | NeedsLogin
82
91
83jsonDecSub : Json.Decode.Decoder ( Sub )
92jsonDecSub : Json.Decode.Decoder ( Sub )
84jsonDecSub =
93jsonDecSub =
85 let jsonDecDictSub = Dict.fromList
94 let jsonDecDictSub = Dict.fromList
86− [ ("Empty", Json.Decode.lazy (\_ -> Json.Decode.succeed Empty))
87− , ("Error", Json.Decode.lazy (\_ -> Json.Decode.map Error (Json.Decode.string)))
95+ [ ("Error", Json.Decode.lazy (\_ -> Json.Decode.map Error (Json.Decode.string)))
88− , ("GotEverything", Json.Decode.lazy (\_ -> Json.Decode.map GotEverything (Json.Decode.string)))
96+ , ("LoadCiphers", Json.Decode.lazy (\_ -> Json.Decode.map LoadCiphers (jsonDecSub_LoadCiphers_List)))
89− , ("Hello", Json.Decode.lazy (\_ -> Json.Decode.map Hello (jsonDecSub_Hello)))
90 , ("NeedsLogin", Json.Decode.lazy (\_ -> Json.Decode.succeed NeedsLogin))
97 , ("NeedsLogin", Json.Decode.lazy (\_ -> Json.Decode.succeed NeedsLogin))
91 ]
98 ]
92 jsonDecObjectSetSub = Set.fromList []
99 jsonDecObjectSetSub = Set.fromList []
@@ -95,10 +102,8 @@ jsonDecSub =
95jsonEncSub : Sub -> Value
102jsonEncSub : Sub -> Value
96jsonEncSub val =
103jsonEncSub val =
97 let keyval v = case v of
104 let keyval v = case v of
98− Empty -> ("Empty", encodeValue (Json.Encode.list identity []))
99 Error v1 -> ("Error", encodeValue (Json.Encode.string v1))
105 Error v1 -> ("Error", encodeValue (Json.Encode.string v1))
100− GotEverything v1 -> ("GotEverything", encodeValue (Json.Encode.string v1))
106+ LoadCiphers v1 -> ("LoadCiphers", encodeValue (jsonEncSub_LoadCiphers_List v1))
101− Hello v1 -> ("Hello", encodeValue (jsonEncSub_Hello v1))
102 NeedsLogin -> ("NeedsLogin", encodeValue (Json.Encode.list identity []))
107 NeedsLogin -> ("NeedsLogin", encodeValue (Json.Encode.list identity []))
103 in encodeSumTaggedObject "tag" "contents" keyval val
108 in encodeSumTaggedObject "tag" "contents" keyval val
104
109
frontend/Main.elm+51−9
@@ -4,7 +4,9 @@ import Bridge
4import Browser
4import Browser
5import FFI exposing (getBridge)
5import FFI exposing (getBridge)
6import Html exposing (..)
6import Html exposing (..)
7+import Html.Attributes as Attr
7import Notification
8import Notification
9+import Pages.Ciphers as Ciphers
8import Pages.Loader exposing (loader)
10import Pages.Loader exposing (loader)
9import Pages.Login as Login
11import Pages.Login as Login
10import Pages.Navigation as Navigation
12import Pages.Navigation as Navigation
@@ -18,6 +20,8 @@ type Msg
18 | CloseNotification
20 | CloseNotification
19 | SubmitLogin { email : String, password : String, server : String }
21 | SubmitLogin { email : String, password : String, server : String }
20 | LoginMsg Login.Msg
22 | LoginMsg Login.Msg
23+ | CiphersMsg Ciphers.Msg
24+ | LoadCiphers Bridge.Sub_LoadCiphers_List
21
25
22
26
23type alias Model =
27type alias Model =
@@ -34,6 +38,7 @@ appendNotification cfg model =
34type PageModel
38type PageModel
35 = LoginModel Login.Model
39 = LoginModel Login.Model
36 | LoadingPage
40 | LoadingPage
41+ | CiphersModel Ciphers.Model
37
42
38
43
39showPage : PageModel -> ( String, List (Html Msg) )
44showPage : PageModel -> ( String, List (Html Msg) )
@@ -46,6 +51,13 @@ showPage page =
46 in
51 in
47 ( p.title model, p.view model )
52 ( p.title model, p.view model )
48
53
54+ CiphersModel model ->
55+ let
56+ p =
57+ Ciphers.page {} CiphersMsg
58+ in
59+ ( p.title model, p.view model )
60+
49 LoadingPage ->
61 LoadingPage ->
50 ( "", [ loader ] )
62 ( "", [ loader ] )
51
63
@@ -61,20 +73,14 @@ main =
61 getBridge ShowError
73 getBridge ShowError
62 (\msg ->
74 (\msg ->
63 case msg of
75 case msg of
64− Bridge.Hello { first, second } ->
65− RecieveMessage ("Got it! " ++ first ++ second)
66−
67− Bridge.GotEverything everything ->
68− RecieveMessage everything
69−
70− Bridge.Empty ->
71− RecieveMessage "Empty"
72−
73 Bridge.NeedsLogin ->
76 Bridge.NeedsLogin ->
74 ShowLoginPage
77 ShowLoginPage
75
78
76 Bridge.Error err ->
79 Bridge.Error err ->
77 ShowError err
80 ShowError err
81+
82+ Bridge.LoadCiphers ciphers ->
83+ LoadCiphers ciphers
78 )
84 )
79 }
85 }
80
86
@@ -98,6 +104,25 @@ view model =
98 [ Navigation.navigation { back = False, title = title }
104 [ Navigation.navigation { back = False, title = title }
99 , main_ []
105 , main_ []
100 (maybeList (List.head model.notifications) (Notification.notification CloseNotification)
106 (maybeList (List.head model.notifications) (Notification.notification CloseNotification)
107+ ++ [ div [ Attr.class "p-side-navigation--icons is-drawer-expanded" ]
108+ [ div [ Attr.class "p-side-navigation__overlay" ] []
109+ , nav [ Attr.class "p-side-navigation__drawer" ]
110+ [ div [ Attr.class "p-side-navigation__drawer-header" ]
111+ [ a [ Attr.class "p-side-navigation__toggle--in-drawer" ] [ text "mail@iko.soy" ]
112+ ]
113+ , ul [ Attr.class "p-side-navigation__list" ]
114+ (List.repeat 6
115+ (li [ Attr.class "p-side-navigation__item" ]
116+ [ a [ Attr.class "p-side-navigation__link" ]
117+ [ i [ Attr.class "p-icon--help p-side-navigation__icon" ] []
118+ , text "Users"
119+ ]
120+ ]
121+ )
122+ )
123+ ]
124+ ]
125+ ]
101 ++ body
126 ++ body
102 )
127 )
103 ]
128 ]
@@ -161,11 +186,28 @@ update msg model =
161 _ ->
186 _ ->
162 ( model, Cmd.none )
187 ( model, Cmd.none )
163
188
189+ CiphersMsg imsg ->
190+ case model.page of
191+ CiphersModel page ->
192+ (Ciphers.page ciphersCallbacks CiphersMsg).update imsg page |> processPage CiphersModel
193+
194+ _ ->
195+ ( model, Cmd.none )
196+
164 ShowLoginPage ->
197 ShowLoginPage ->
165 (Login.page loginCallbacks LoginMsg).init ()
198 (Login.page loginCallbacks LoginMsg).init ()
166 |> Tuple.mapFirst (\pageModel -> { model | page = LoginModel pageModel })
199 |> Tuple.mapFirst (\pageModel -> { model | page = LoginModel pageModel })
167
200
201+ LoadCiphers ciphers ->
202+ (Ciphers.page ciphersCallbacks CiphersMsg).init ciphers
203+ |> Tuple.mapFirst (\pageModel -> { model | page = CiphersModel pageModel })
204+
168
205
169loginCallbacks : Login.Callbacks Msg
206loginCallbacks : Login.Callbacks Msg
170loginCallbacks =
207loginCallbacks =
171 { submit = SubmitLogin }
208 { submit = SubmitLogin }
209+
210+
211+ciphersCallbacks : Ciphers.Callbacks
212+ciphersCallbacks =
213+ {}
frontend/Pages/Ciphers.elmnew file+67
@@ -0,0 +1,67 @@
1+module Pages.Ciphers exposing (Callbacks, Model, Msg, page)
2+
3+import Bridge
4+import Html exposing (..)
5+import Html.Attributes as Attr
6+import Html.Events as Ev
7+import Page exposing (..)
8+import Utils exposing (..)
9+
10+
11+type alias Model =
12+ { ciphers : Bridge.Sub_LoadCiphers_List
13+ }
14+
15+
16+type Msg
17+ = Noop
18+
19+
20+type alias Callbacks =
21+ {}
22+
23+
24+page : Callbacks -> Page Bridge.Sub_LoadCiphers_List Model Msg emsg
25+page callbacks liftMsg =
26+ { init = \ciphers -> Tuple.mapSecond (Cmd.map liftMsg) (init ciphers)
27+ , view = \model -> view model |> List.map (Html.map liftMsg)
28+ , update = \msg model -> update callbacks liftMsg msg model
29+ , subscriptions = \model -> subscriptions model |> Sub.map liftMsg
30+ , title = always "Passwords"
31+ }
32+
33+
34+init : Bridge.Sub_LoadCiphers_List -> ( Model, Cmd Msg )
35+init ciphers =
36+ ( { ciphers = ciphers }
37+ , Cmd.none
38+ )
39+
40+
41+update : Callbacks -> (Msg -> emsg) -> Msg -> Model -> ( Result String Model, Cmd emsg )
42+update {} liftMsg msg model =
43+ case msg of
44+ Noop ->
45+ ( Ok model, Cmd.none )
46+
47+
48+subscriptions : Model -> Sub Msg
49+subscriptions model =
50+ Sub.none
51+
52+
53+view : Model -> List (Html Msg)
54+view { ciphers } =
55+ [ ul [ Attr.class "p-list--divided" ]
56+ (List.map
57+ (\{ name, date } ->
58+ li [ Attr.class "p-list__item cipher-row" ]
59+ [ div [ Attr.class "cipher-row-container" ]
60+ [ p [] [ text name ]
61+ , p [ Attr.class "p-text--small u-align-text--right u-text--muted" ] [ text date ]
62+ ]
63+ ]
64+ )
65+ ciphers
66+ )
67+ ]
frontend/Pages/Navigation.elm+31
@@ -22,3 +22,34 @@ navigation { back, title } =
22 , text title
22 , text title
23 ]
23 ]
24 ]
24 ]
25+
26+
27+type alias MenuConfig msg =
28+ { title : String
29+ , items : List { icon : String, name : String, trigger : msg }
30+ , close : msg
31+ }
32+
33+
34+menu : MenuConfig msg -> Html msg
35+menu { title, items, close } =
36+ div [ Attr.class "p-side-navigation--icons is-drawer-expanded" ]
37+ [ div [ Attr.class "p-side-navigation__overlay" ] []
38+ , nav [ Attr.class "p-side-navigation__drawer" ]
39+ [ div [ Attr.class "p-side-navigation__drawer-header" ]
40+ [ a [ Attr.class "p-side-navigation__toggle--in-drawer" ] [ text title ]
41+ ]
42+ , ul [ Attr.class "p-side-navigation__list" ]
43+ (items
44+ |> List.map
45+ (\{ icon, name, trigger } ->
46+ li [ Attr.class "p-side-navigation__item" ]
47+ [ a [ Attr.class "p-side-navigation__link" ]
48+ [ i [ Attr.class "p-side-navigation__icon", Attr.class ("p-icon--" ++ icon) ] []
49+ , text name
50+ ]
51+ ]
52+ )
53+ )
54+ ]
55+ ]
spago.dhall+3
@@ -17,11 +17,14 @@ to generate this file without the comments in this block.
17 , "argonaut"
17 , "argonaut"
18 , "argonaut-aeson-generic"
18 , "argonaut-aeson-generic"
19 , "argonaut-codecs"
19 , "argonaut-codecs"
20+ , "arrays"
20 , "bw"
21 , "bw"
21 , "console"
22 , "console"
22 , "datetime"
23 , "datetime"
24+ , "datetime-parsing"
23 , "effect"
25 , "effect"
24 , "either"
26 , "either"
27+ , "el"
25 , "exceptions"
28 , "exceptions"
26 , "foldable-traversable"
29 , "foldable-traversable"
27 , "foreign"
30 , "foreign"
style.css+18
@@ -14,6 +14,7 @@ nav.p-tabs__list {
14 background-color: white;
14 background-color: white;
15 padding-bottom: 1rem;
15 padding-bottom: 1rem;
16 padding-bottom: 0;
16 padding-bottom: 0;
17+ z-index: 10;
17}
18}
18
19
19.ninety-counter {
20.ninety-counter {
@@ -40,3 +41,20 @@ nav.p-tabs__list {
40.spinner{
41.spinner{
41 padding: 1rem;
42 padding: 1rem;
42}
43}
44+
45+.cipher-row-container {
46+ display: flex;
47+ width: 100%;
48+ align-items: baseline;
49+ justify-content: space-between;
50+ flex-direction: row;
51+ flex-wrap: wrap;
52+}
53+
54+.cipher-row p {
55+ margin-bottom: 0.5rem;
56+}
57+
58+.cipher-row .p-text--small {
59+ margin-bottom: 0.5rem;
60+}