A music player that connects to your cloud/distributed storage.
5
fork

Configure Feed

Select the types of activity you want to include in your feed.

Use PKCE code flow to authenticate

+303 -41
+4 -4
src/Applications/Brain/User/State.elm
··· 420 420 in 421 421 case model.authMethod of 422 422 -- 🚀 423 - Just (Dropbox { token }) -> 423 + Just (Dropbox { accessToken, refreshToken }) -> 424 424 [ ( "file", file ) 425 - , ( "token", Json.string token ) 425 + , ( "token", Json.string accessToken ) 426 426 ] 427 427 |> Json.object 428 428 |> Alien.broadcast Alien.AuthDropbox ··· 516 516 in 517 517 case model.authMethod of 518 518 -- 🚀 519 - Just (Dropbox { token }) -> 519 + Just (Dropbox { accessToken, refreshToken }) -> 520 520 [ ( "data", json ) 521 521 , ( "file", file ) 522 - , ( "token", Json.string token ) 522 + , ( "token", Json.string accessToken ) 523 523 ] 524 524 |> Json.object 525 525 |> Alien.broadcast Alien.AuthDropbox
+9 -1
src/Applications/UI.elm
··· 14 14 import LastFm 15 15 import Maybe.Extra as Maybe 16 16 import Notifications 17 + import Random 17 18 import Return 18 19 import Task 19 20 import Time ··· 47 48 import UI.User.State.Import as User 48 49 import UI.View exposing (view) 49 50 import Url exposing (Url) 51 + import Url.Ext as Url 50 52 51 53 52 54 ··· 98 100 , page = page 99 101 , pressedKeys = [] 100 102 , processAutomatically = True 103 + , uuidSeed = Random.initialSeed flags.initialTime 101 104 , url = url 102 105 , viewport = flags.viewport 103 106 ··· 216 219 Routing.resetUrl key url page 217 220 218 221 else 219 - Cmd.none 222 + case Url.action url of 223 + [ "authenticate", "dropbox" ] -> 224 + Routing.resetUrl key url page 225 + 226 + _ -> 227 + Cmd.none 220 228 ) 221 229 |> Return.command 222 230 (Task.perform SetCurrentTime Time.now)
+3
src/Applications/UI/Authentication/Common.elm
··· 24 24 Authenticated method -> 25 25 Just method 26 26 27 + Authenticating -> 28 + Nothing 29 + 27 30 InputScreen method _ -> 28 31 Just method 29 32
+82 -25
src/Applications/UI/Authentication/State.elm
··· 5 5 import Binary 6 6 import Browser.Navigation as Nav 7 7 import Common exposing (Switch(..)) 8 + import Dict 8 9 import Html exposing (a) 9 10 import Html.Attributes exposing (value) 10 11 import Html.Events.Extra.Mouse as Mouse 11 12 import Http 13 + import Http.Ext as Http 12 14 import Json.Decode as Json 13 15 import Json.Encode 14 16 import Lens.Ext as Lens ··· 25 27 import UI.Backdrop as Backdrop 26 28 import UI.Common.State as Common exposing (showNotification, showNotificationWithModel) 27 29 import UI.Ports as Ports 30 + import UI.Routing.State as Routing 28 31 import UI.Sources.Query 29 32 import UI.Sources.State as Sources 30 33 import UI.Types as UI exposing (..) ··· 32 35 import Url exposing (Protocol(..), Url) 33 36 import Url.Ext as Url 34 37 import User.Layer exposing (..) 38 + import User.Layer.Methods.Dropbox as Dropbox 35 39 import User.Layer.Methods.RemoteStorage as RemoteStorage 36 40 import Webnative 37 41 import Webnative.Constants as Webnative ··· 57 61 initialModel url = 58 62 case Url.action url of 59 63 [ "authenticate", "dropbox" ] -> 60 - url.fragment 61 - |> Maybe.map (String.split "&") 62 - |> Maybe.map (List.filter <| String.startsWith "access_token=") 63 - |> Maybe.andThen List.head 64 - |> Maybe.withDefault "" 65 - |> String.replace "access_token=" "" 66 - |> (\t -> 67 - NewEncryptionKeyScreen 68 - (Dropbox { token = t }) 69 - Nothing 70 - ) 64 + case Dict.get "code" (Url.queryDictionary url) of 65 + Just _ -> 66 + Authenticating 67 + 68 + _ -> 69 + Unauthenticated 71 70 72 71 [ "authenticate", "remotestorage", encodedUserAddress ] -> 73 72 let 73 + dict = 74 + Url.queryDictionary url 75 + 74 76 userAddress = 75 77 encodedUserAddress 76 78 |> Url.percentDecode 77 79 |> Maybe.andThen (Base64.decode >> Result.toMaybe) 78 80 |> Maybe.withDefault encodedUserAddress 79 81 in 80 - url.fragment 81 - |> Maybe.map (String.split "&") 82 - |> Maybe.map (List.filter <| String.startsWith "access_token=") 83 - |> Maybe.andThen List.head 84 - |> Maybe.withDefault "" 85 - |> String.replace "access_token=" "" 86 - |> (\t -> 87 - NewEncryptionKeyScreen 88 - (RemoteStorage { userAddress = userAddress, token = t }) 89 - Nothing 90 - ) 82 + case Dict.get "access_token" dict of 83 + Just t -> 84 + NewEncryptionKeyScreen 85 + (RemoteStorage 86 + { userAddress = userAddress 87 + , token = t 88 + } 89 + ) 90 + Nothing 91 + 92 + Nothing -> 93 + Unauthenticated 91 94 92 95 _ -> 93 96 Welcome ··· 96 99 initialCommand : Url -> Cmd Authentication.Msg 97 100 initialCommand url = 98 101 case Url.action url of 102 + [ "authenticate", "dropbox" ] -> 103 + case Dict.get "code" (Url.queryDictionary url) of 104 + Just code -> 105 + Dropbox.exchangeAuthCode 106 + (GotDropboxTokens Dropbox.Code) 107 + url 108 + code 109 + 110 + _ -> 111 + Cmd.none 112 + 99 113 [ "authenticate", "fission" ] -> 100 114 Webnative.permissions 101 115 |> Webnative.initWithOptions ··· 133 147 134 148 GetStarted -> 135 149 startFlow 150 + 151 + GotDropboxTokens a b -> 152 + gotDropboxTokens a b 136 153 137 154 NotAuthenticated -> 138 155 notAuthenticated ··· 235 252 Authenticated method -> 236 253 Authenticated method 237 254 255 + Authenticating -> 256 + Unauthenticated 257 + 238 258 InputScreen _ _ -> 239 259 Unauthenticated 240 260 ··· 259 279 externalAuth method string model = 260 280 case method of 261 281 Dropbox _ -> 262 - [ ( "response_type", "token" ) 263 - , ( "client_id", "te0c9pbeii8f8bw" ) 264 - , ( "redirect_uri", Common.urlOrigin model.url ++ "?action=authenticate/dropbox" ) 282 + [ ( "client_id", Dropbox.clientId ) 283 + , ( "redirect_uri", Dropbox.redirectUri model.url ) 284 + , ( "response_type", "code" ) 285 + , ( "token_access_type", "offline" ) 265 286 ] 266 287 |> Common.queryString 267 288 |> String.append "https://www.dropbox.com/oauth2/authorize" ··· 295 316 296 317 _ -> 297 318 Return.singleton model 319 + 320 + 321 + gotDropboxTokens : Dropbox.TokenFlow -> Result Http.Error Dropbox.Tokens -> Manager 322 + gotDropboxTokens flow result model = 323 + case ( flow, result ) of 324 + ( Dropbox.Code, Ok tokens ) -> 325 + case tokens.refreshToken of 326 + Just refreshToken -> 327 + Nothing 328 + |> NewEncryptionKeyScreen 329 + (Dropbox 330 + { accessToken = tokens.accessToken 331 + , expiresIn = tokens.expiresIn 332 + , refreshToken = refreshToken 333 + } 334 + ) 335 + |> Lens.replace lens model 336 + |> Return.singleton 337 + 338 + Nothing -> 339 + "Missing refresh token in Dropbox code exchange flow." 340 + |> Notifications.stickyError 341 + |> showNotificationWithModel 342 + (Lens.replace lens model Unauthenticated) 343 + 344 + ( Dropbox.Refresh, Ok tokens ) -> 345 + -- TODO 346 + Return.singleton model 347 + 348 + ( _, Err err ) -> 349 + [] 350 + |> Notifications.errorWithCode 351 + "Failed to authenticate with Dropbox" 352 + (Http.errorToString err) 353 + |> showNotificationWithModel 354 + (Lens.replace lens model Unauthenticated) 298 355 299 356 300 357 missingSecretKey : Json.Value -> Manager
+3
src/Applications/UI/Authentication/Types.elm
··· 4 4 import Http 5 5 import Json.Decode as Json 6 6 import User.Layer exposing (Method) 7 + import User.Layer.Methods.Dropbox as Dropbox 7 8 import User.Layer.Methods.RemoteStorage as RemoteStorage 8 9 9 10 ··· 13 14 14 15 type State 15 16 = Authenticated Method 17 + | Authenticating 16 18 | InputScreen Method Question 17 19 | NewEncryptionKeyScreen Method (Maybe String) 18 20 | UpdateEncryptionKeyScreen Method (Maybe String) ··· 37 39 | BootFailure String 38 40 | CancelFlow 39 41 | GetStarted 42 + | GotDropboxTokens Dropbox.TokenFlow (Result Http.Error Dropbox.Tokens) 40 43 | NotAuthenticated 41 44 | RemoteStorageWebfinger RemoteStorage.Attributes (Result Http.Error String) 42 45 | ShowMoreOptions Mouse.Event
+32 -1
src/Applications/UI/Authentication/View.elm
··· 7 7 import Html.Events exposing (onClick, onSubmit) 8 8 import Html.Events.Extra exposing (onClickStopPropagation) 9 9 import Html.Events.Extra.Mouse as Mouse 10 + import Html.Extra as Html 10 11 import Html.Lazy as Lazy 11 12 import Markdown 12 13 import Material.Icons as Icons ··· 75 76 -- Speech bubble 76 77 ---------------- 77 78 , case state of 79 + Authenticating -> 80 + speechBubble negotiating 81 + 78 82 InputScreen _ { question } -> 79 83 question 80 84 |> String.lines ··· 163 167 Authenticated _ -> 164 168 choicesScreen 165 169 170 + Authenticating -> 171 + Html.nothing 172 + 166 173 Welcome -> 167 174 welcomeScreen 168 175 ··· 224 231 225 232 226 233 234 + -- LOADING 235 + 236 + 237 + negotiating : Html Authentication.Msg 238 + negotiating = 239 + chunk 240 + [ "flex" 241 + , "items-center" 242 + ] 243 + [ chunk 244 + [ "transform", "-translate-y-px" ] 245 + [ Html.map never (UI.Svg.Elements.loadingWithSize 14) ] 246 + , chunk 247 + [ "italic" 248 + , "ml-2" 249 + , "text-opacity-80" 250 + , "text-sm" 251 + , "text-white" 252 + ] 253 + [ Html.text "Negotiating with service" ] 254 + ] 255 + 256 + 257 + 227 258 -- CHOICES 228 259 229 260 ··· 256 287 , outOfOrder = False 257 288 } 258 289 , choiceButton 259 - { action = TriggerExternalAuth (Dropbox { token = "" }) "" 290 + { action = TriggerExternalAuth (Dropbox { accessToken = "", expiresIn = 0, refreshToken = "" }) "" 260 291 , icon = \_ _ -> Svg.map never UI.Svg.Elements.dropboxLogo 261 292 , infoLink = Just "https://dropbox.com/" 262 293 , label = "Dropbox"
+6 -1
src/Applications/UI/Page.elm
··· 45 45 path = 46 46 Maybe.withDefault "" maybePath 47 47 in 48 - if Maybe.unwrap False (String.contains "token=") url.fragment then 48 + if 49 + Maybe.unwrap 50 + False 51 + (\f -> String.contains "token=" f || String.contains "code=" f) 52 + url.fragment 53 + then 49 54 -- For some oauth stuff, replace the query with the fragment 50 55 { url | path = path, query = url.fragment } 51 56
+8 -3
src/Applications/UI/Svg/Elements.elm
··· 1 - module UI.Svg.Elements exposing (dropboxLogo, fissionLogo, ipfsLogo, loading, remoteStorageLogo) 1 + module UI.Svg.Elements exposing (dropboxLogo, fissionLogo, ipfsLogo, loading, loadingWithSize, remoteStorageLogo) 2 2 3 3 import Svg exposing (..) 4 4 import Svg.Attributes exposing (..) ··· 112 112 113 113 loading : Svg Never 114 114 loading = 115 + loadingWithSize 29 116 + 117 + 118 + loadingWithSize : Int -> Svg Never 119 + loadingWithSize size = 115 120 svg 116 121 [ class "loading-animation" 117 - , height "29" 122 + , height (String.fromInt size) 118 123 , viewBox "0 0 30 30" 119 - , width "29" 124 + , width (String.fromInt size) 120 125 ] 121 126 [ circle 122 127 [ class "loading-animation__circle"
+2
src/Applications/UI/Types.elm
··· 22 22 import Notifications exposing (Notification) 23 23 import Playlists exposing (Playlist, PlaylistTrack) 24 24 import Queue 25 + import Random 25 26 import Sources exposing (Source) 26 27 import Time 27 28 import Tracks exposing (..) ··· 70 71 , page : Page 71 72 , pressedKeys : List Keyboard.Key 72 73 , processAutomatically : Bool 74 + , uuidSeed : Random.Seed 73 75 , url : Url 74 76 , viewport : Viewport 75 77
+37
src/Library/Http/Ext.elm
··· 1 + module Http.Ext exposing (errorToString) 2 + 3 + import Http exposing (Error(..)) 4 + import Json.Decode as Json 5 + 6 + 7 + 8 + -- 🛠 9 + 10 + 11 + errorToString : Http.Error -> String 12 + errorToString err = 13 + -- Thanks to: https://github.com/hercules-ci/elm-hercules-extras/blob/1.0.0/src/Http/Extras.elm 14 + case err of 15 + Timeout -> 16 + "Timeout exceeded" 17 + 18 + NetworkError -> 19 + "Network error" 20 + 21 + BadStatus code -> 22 + "Something went wrong, got status code: " ++ String.fromInt code 23 + 24 + BadBody text -> 25 + "Unexpected response: " ++ text 26 + 27 + BadUrl url -> 28 + "Malformed url: " ++ url 29 + 30 + 31 + 32 + -- ㊙️ 33 + 34 + 35 + parseError : String -> Maybe String 36 + parseError = 37 + Json.decodeString (Json.field "error" Json.string) >> Result.toMaybe
+19 -1
src/Library/Url/Ext.elm
··· 1 - module Url.Ext exposing (action, extractQueryParam) 1 + module Url.Ext exposing (action, extractQueryParam, queryDictionary) 2 2 3 + import Dict exposing (Dict) 3 4 import Maybe.Extra as Maybe 4 5 import Url exposing (Url) 5 6 import Url.Parser as Url ··· 23 24 { url | path = "" } 24 25 |> Url.parse (Url.query (Query.string key)) 25 26 |> Maybe.join 27 + 28 + 29 + queryDictionary : Url -> Dict String String 30 + queryDictionary url = 31 + url.query 32 + |> Maybe.map (String.split "&") 33 + |> Maybe.withDefault [] 34 + |> List.filterMap 35 + (\s -> 36 + case String.split "=" s of 37 + [ k, v ] -> 38 + Just ( k, v ) 39 + 40 + _ -> 41 + Nothing 42 + ) 43 + |> Dict.fromList
+13 -5
src/Library/User/Layer.elm
··· 37 37 38 38 39 39 type Method 40 - = Dropbox { token : String } 40 + = Dropbox { accessToken : String, expiresIn : Int, refreshToken : String } 41 41 | Fission { initialised : Bool } 42 42 | Ipfs { apiOrigin : String } 43 43 | Local ··· 139 139 methodFromString : String -> Maybe Method 140 140 methodFromString string = 141 141 case String.split methodSeparator string of 142 - [ "DROPBOX", t ] -> 143 - Just (Dropbox { token = t }) 142 + [ "DROPBOX", a, e, r ] -> 143 + Just 144 + (Dropbox 145 + { accessToken = a 146 + , expiresIn = Maybe.withDefault 0 (String.toInt e) 147 + , refreshToken = r 148 + } 149 + ) 144 150 145 151 [ "FISSION" ] -> 146 152 Just (Fission { initialised = False }) ··· 161 167 methodToString : Method -> String 162 168 methodToString method = 163 169 case method of 164 - Dropbox { token } -> 170 + Dropbox { accessToken, expiresIn, refreshToken } -> 165 171 String.join 166 172 methodSeparator 167 173 [ "DROPBOX" 168 - , token 174 + , accessToken 175 + , String.fromInt expiresIn 176 + , refreshToken 169 177 ] 170 178 171 179 Fission _ ->
+85
src/Library/User/Layer/Methods/Dropbox.elm
··· 1 + module User.Layer.Methods.Dropbox exposing (..) 2 + 3 + import Common 4 + import Http 5 + import Json.Decode as Json 6 + import Url exposing (Url) 7 + 8 + 9 + 10 + -- 🌳 11 + 12 + 13 + type TokenFlow 14 + = Code 15 + | Refresh 16 + 17 + 18 + type alias Tokens = 19 + { accessToken : String 20 + , expiresIn : Int -- Time in seconds the access token expires in 21 + , refreshToken : Maybe String 22 + } 23 + 24 + 25 + 26 + -- 🏔 27 + 28 + 29 + clientId : String 30 + clientId = 31 + "te0c9pbeii8f8bw" 32 + 33 + 34 + clientSecret : String 35 + clientSecret = 36 + "kxmlfdsw8k9e0ot" 37 + 38 + 39 + redirectUri : Url -> String 40 + redirectUri url = 41 + Common.urlOrigin url ++ "?action=authenticate/dropbox" 42 + 43 + 44 + 45 + -- ENCODING 46 + 47 + 48 + tokensDecoder : Json.Decoder Tokens 49 + tokensDecoder = 50 + Json.map3 51 + (\a e r -> 52 + { accessToken = a 53 + , expiresIn = e 54 + , refreshToken = r 55 + } 56 + ) 57 + (Json.field "access_token" Json.string) 58 + (Json.field "expires_in" Json.int) 59 + (Json.string 60 + |> Json.field "refresh_token" 61 + |> Json.maybe 62 + ) 63 + 64 + 65 + 66 + -- 🛠 67 + 68 + 69 + exchangeAuthCode : (Result Http.Error Tokens -> msg) -> Url -> String -> Cmd msg 70 + exchangeAuthCode msg url code = 71 + [ ( "client_id", clientId ) 72 + , ( "client_secret", clientSecret ) 73 + , ( "code", code ) 74 + , ( "grant_type", "authorization_code" ) 75 + , ( "redirect_uri", redirectUri url ) 76 + ] 77 + |> Common.queryString 78 + |> String.append "https://api.dropboxapi.com/oauth2/token" 79 + |> (\u -> 80 + { url = u 81 + , body = Http.emptyBody 82 + , expect = Http.expectJson msg tokensDecoder 83 + } 84 + ) 85 + |> Http.post