summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorJan Tuomi <jan.tuomi@valuemotive.com>2021-10-30 19:24:00 +0300
committerJan Tuomi <jan.tuomi@valuemotive.com>2021-10-30 19:29:03 +0300
commitc6d55561bf8422301ea7a2d08b9a7c3e30a1440e (patch)
tree33fd6b0c1150b67e2f8ba38806a984c3cecccc75
parente24ba80820bd49af72a9e1702ed635e9b63452d1 (diff)
Implement authentication
-rw-r--r--db.json109
-rw-r--r--main.js46
-rw-r--r--src/Config.elm6
-rw-r--r--src/Header.elm34
-rw-r--r--src/Main.elm80
-rw-r--r--src/Pages/Feed.elm40
-rw-r--r--src/RemoteData.elm52
-rw-r--r--src/Types.elm16
-rw-r--r--src/Utils.elm42
9 files changed, 263 insertions, 162 deletions
diff --git a/db.json b/db.json
index 7a81353..0f6afd0 100644
--- a/db.json
+++ b/db.json
@@ -1,104 +1,59 @@
{
"posts": [
{
- "id": "5CEDAFAA-4CB9-4BF9-A0B2-EB4DF373B4A3",
- "content": "Hello world",
- "createdAt": 1635505671,
+ "content": "hello",
+ "createdAt": 1635607271,
+ "likes": 3,
+ "userPictureUrl": "https://lh3.googleusercontent.com/a-/AOh14Ggi0fsy3DrKGf2CswW96pft9BO3tDskxr0241FmxQ=s96-c",
"author": {
- "id": "EA8E23FC-8F12-404A-9A68-576FBDF72934",
- "name": "George Technoman"
+ "id": "jans.tuomi@gmail.com",
+ "name": "Jan Tuomi"
},
- "likes": 38
+ "id": "1"
},
{
"content": "asd",
- "createdAt": 1635513526,
- "likes": 2,
- "author": {
- "id": "logged-in-user-id",
- "name": "Logged in user"
- },
- "id": "-_5iRYd"
- },
- {
- "content": "new post :)))",
- "createdAt": 1635513654,
- "likes": 1,
- "author": {
- "id": "logged-in-user-id",
- "name": "Logged in user"
- },
- "id": "sgiJnlL"
- },
- {
- "content": "I'll have two number nines",
- "createdAt": 1635514095,
- "likes": 1,
- "author": {
- "id": "logged-in-user-id",
- "name": "Logged in user"
- },
- "id": "g5Vggns"
- },
- {
- "content": "asdasdasd",
- "createdAt": 1635514147,
+ "createdAt": 1635607657,
"likes": 0,
+ "userPictureUrl": "https://lh3.googleusercontent.com/a-/AOh14Ggi0fsy3DrKGf2CswW96pft9BO3tDskxr0241FmxQ=s96-c",
"author": {
- "id": "logged-in-user-id",
- "name": "Logged in user"
+ "id": "jans.tuomi@gmail.com",
+ "name": "Jan Tuomi"
},
- "id": "bIZavcS"
+ "id": "3B9scEm"
},
{
- "content": "asdasdas",
- "createdAt": 1635514155,
+ "content": "throwaway account here :---))",
+ "createdAt": 1635610312,
"likes": 0,
+ "userPictureUrl": "https://s.gravatar.com/avatar/ef587429f8bb85b585c901c528cacca9?s=480&r=pg&d=https%3A%2F%2Fcdn.auth0.com%2Favatars%2Fnj.png",
"author": {
- "id": "logged-in-user-id",
- "name": "Logged in user"
+ "id": "njezhmeteiexjqoviz@pp7rvv.com",
+ "name": "njezhmeteiexjqoviz@pp7rvv.com"
},
- "id": "bsb19RF"
+ "id": "sKnOARV"
},
{
- "content": "asdasd",
- "createdAt": 1635514207,
- "likes": 2,
- "author": {
- "id": "logged-in-user-id",
- "name": "Logged in user"
- },
- "id": "miEWafM"
- },
- {
- "content": "plö lö",
- "createdAt": 1635516837,
- "likes": 5,
- "author": {
- "id": "logged-in-user-id",
- "name": "Logged in user"
- },
- "id": "A44uzWi"
- },
- {
- "content": "asdasd",
- "createdAt": 1635526426,
- "likes": 3,
+ "content": "asd",
+ "createdAt": 1635610687,
+ "likes": 0,
+ "userPictureUrl": "https://s.gravatar.com/avatar/ef587429f8bb85b585c901c528cacca9?s=480&r=pg&d=https%3A%2F%2Fcdn.auth0.com%2Favatars%2Fnj.png",
"author": {
- "id": "logged-in-user-id",
- "name": "Logged in user"
+ "id": "njezhmeteiexjqoviz@pp7rvv.com",
+ "name": "njezhmeteiexjqoviz@pp7rvv.com"
},
- "id": "_wepGww"
+ "id": "0nOVGJM"
},
{
- "content": "pippeli",
- "createdAt": 1635526491,
- "likes": 1,
+ "content": "foobar",
+ "createdAt": 1635610870,
+ "likes": 0,
+ "userPictureUrl": "https://lh3.googleusercontent.com/a-/AOh14Ggi0fsy3DrKGf2CswW96pft9BO3tDskxr0241FmxQ=s96-c",
"author": {
- "id": "logged-in-user-id",
- "name": "Logged in user"
+ "id": "jans.tuomi@gmail.com",
+ "name": "Jan Tuomi"
},
- "id": "E3th4Nq"
+ "id": "JvXaf8k"
}
]
} \ No newline at end of file
diff --git a/main.js b/main.js
index bc89f99..a7aae91 100644
--- a/main.js
+++ b/main.js
@@ -51,53 +51,61 @@ const configureClient = async() => {
window.auth0 = auth0;
};
-// Will run when page finishes loading
-window.onload = async() => {
+const authenticateIfNeeded = async() => {
await configureClient();
- // If unable to parse the history hash, default to the root URL
- // if (!showContentFromUrl(window.location.pathname)) {
- // showContentFromUrl("/");
- // window.history.replaceState({ url: "/" }, {}, "/");
- // }
-
- const isAuthenticated = await auth0.isAuthenticated();
+ const userDataExists = await auth0.isAuthenticated();
- if (isAuthenticated) {
+ if (userDataExists) {
+ // If user is in browser state, use that user data
console.log("> User is authenticated");
window.history.replaceState({}, document.title, window.location.pathname);
- // runElmApp();
return;
}
console.log("> User not authenticated");
+ // No user data in browser, try checking the query params for callback info
const query = window.location.search;
const shouldParseResult = query.includes("code=") && query.includes("state=");
if (shouldParseResult) {
+ // If URL contains callback query params, parse them and authenticate
console.log("> Parsing redirect");
try {
- const result = await auth0.handleRedirectCallback();
-
- if (result.appState && result.appState.targetUrl) {
- showContentFromUrl(result.appState.targetUrl);
- }
+ await auth0.handleRedirectCallback();
console.log("Logged in!");
- return runElmApp();
+
+ window.history.replaceState({}, document.title, window.location.pathname);
+ return;
} catch (err) {
console.log("Error parsing redirect:", err);
}
}
+ // No data to authenticate with, start login flow and redirect
login();
+}
+
+// Will run when page finishes loading
+window.onload = async() => {
+ await authenticateIfNeeded();
+ await runElmApp();
};
-const runElmApp = () => {
+const runElmApp = async() => {
try {
+ const user = await auth0.getUser();
+ const flags = {
+ email: user.email,
+ name: user.name,
+ pictureUrl: user.picture,
+ };
+
const app = Elm.Main.init({
- node: document.getElementById("app")
+ node: document.getElementById("app"),
+ flags,
});
app.ports.showAlert.subscribe((message) => window.alert(message));
diff --git a/src/Config.elm b/src/Config.elm
index ecd263b..397bc38 100644
--- a/src/Config.elm
+++ b/src/Config.elm
@@ -1,5 +1,7 @@
module Config exposing (ApiResource(..), makeApiUrl)
+import Utils exposing (templ)
+
baseUrl : String
baseUrl =
@@ -20,8 +22,8 @@ makeApiUrl res =
"{baseUrl}/posts?_sort=createdAt&_order=desc"
ApiLikePost postId ->
- "{baseUrl}/posts/{postId}"
- |> String.replace "{postId}" postId
+ "{baseUrl}/posts/{0}"
+ |> templ [ postId ]
ApiCompose ->
"{baseUrl}/posts"
diff --git a/src/Header.elm b/src/Header.elm
new file mode 100644
index 0000000..257b308
--- /dev/null
+++ b/src/Header.elm
@@ -0,0 +1,34 @@
+module Header exposing (..)
+
+import Css exposing (..)
+import Html.Styled exposing (Html, button, div, text)
+import Html.Styled.Attributes exposing (css)
+import Html.Styled.Events exposing (onClick)
+import Types exposing (Model, Msg(..))
+import Utils exposing (templ)
+
+
+styles =
+ { container =
+ css
+ [ displayFlex
+ , flexFlow2 row wrap
+ , justifyContent flexEnd
+ , width (pct 100)
+ , marginBottom (px 10)
+ ]
+ , logoutButton =
+ css
+ [ marginLeft (px 10)
+ , cursor pointer
+ ]
+ }
+
+
+headerView : Model -> List (Html Msg)
+headerView model =
+ [ div [ styles.container ]
+ [ div [] [ "Logged in as {0}" |> templ [ model.userData.name ] |> text ]
+ , button [ styles.logoutButton, onClick RequestLogout ] [ text "Logout" ]
+ ]
+ ]
diff --git a/src/Main.elm b/src/Main.elm
index 9d55d9c..9f5b699 100644
--- a/src/Main.elm
+++ b/src/Main.elm
@@ -3,19 +3,21 @@ port module Main exposing (..)
import Browser exposing (Document)
import Browser.Navigation exposing (Key)
import Config exposing (makeApiUrl)
+import Header exposing (headerView)
import Html.Styled exposing (..)
-import Html.Styled.Events exposing (onClick)
import Http
-import Json.Decode exposing (Decoder, field, int, list, map2, map5, string)
+import Json.Decode exposing (Decoder, field, int, list, map2, map6, string)
import Json.Encode
import Pages.Feed exposing (feedView)
+import RemoteData
import Task
import Time
-import Types exposing (Author, Model, Msg(..), Post)
+import Types exposing (Author, Model, Msg(..), Post, UserData)
import Url exposing (Url)
+import Utils exposing (httpErrorToString, listFlat)
-main : Program () Model Msg
+main : Program UserData Model Msg
main =
Browser.application
{ init = init
@@ -41,13 +43,18 @@ port requestLogout : () -> Cmd msg
-- INIT
-init : () -> Url -> Key -> ( Model, Cmd Msg )
-init _ _ _ =
- ( { posts = Nothing
- , now = Time.millisToPosix 0
+init : UserData -> Url -> Key -> ( Model, Cmd Msg )
+init userData _ key =
+ ( { now = Time.millisToPosix 0
+ , key = key
+ , userData = userData
+ , posts = RemoteData.Loading
, composeInputValue = ""
}
- , Cmd.batch [ getPosts, Task.perform SetNowPosix Time.now ]
+ , Cmd.batch
+ [ getPosts
+ , Task.perform SetNowPosix Time.now
+ ]
)
@@ -71,10 +78,10 @@ update msg model =
( model, getPosts )
GotPosts (Result.Ok posts) ->
- ( { model | posts = Just posts }, Cmd.none )
+ ( { model | posts = RemoteData.Success posts }, Cmd.none )
GotPosts (Result.Err httpErr) ->
- ( model
+ ( { model | posts = RemoteData.Failure httpErr }
, showAlert <| httpErrorToString httpErr
)
@@ -82,7 +89,7 @@ update msg model =
( model, likePost post )
LikedPost (Result.Ok post) ->
- ( { model | posts = Just <| replaceMatchingPost post (Maybe.withDefault [] model.posts) }
+ ( { model | posts = RemoteData.map (replaceMatchingPost post) model.posts }
, Cmd.none
)
@@ -105,7 +112,7 @@ update msg model =
ComposedPost (Result.Ok post) ->
( { model
- | posts = Maybe.map (\posts -> post :: posts) model.posts
+ | posts = RemoteData.map (\posts -> post :: posts) model.posts
, composeInputValue = ""
}
, Cmd.none
@@ -166,10 +173,11 @@ composePost model =
[ ( "content", Json.Encode.string model.composeInputValue )
, ( "createdAt", Json.Encode.int <| Time.posixToMillis model.now // 1000 )
, ( "likes", Json.Encode.int 0 )
+ , ( "userPictureUrl", Json.Encode.string model.userData.pictureUrl )
, ( "author"
, Json.Encode.object
- [ ( "id", Json.Encode.string "logged-in-user-id" )
- , ( "name", Json.Encode.string "Logged in user" )
+ [ ( "id", Json.Encode.string model.userData.email )
+ , ( "name", Json.Encode.string model.userData.name )
]
)
]
@@ -202,7 +210,7 @@ postsDecoder =
postDecoder : Decoder Post
postDecoder =
- map5 Post
+ map6 Post
(field "id" string)
(field "content" string)
(field "author"
@@ -213,6 +221,7 @@ postDecoder =
)
(field "createdAt" decodeTime)
(field "likes" int)
+ (field "userPictureUrl" string)
@@ -222,39 +231,8 @@ postDecoder =
view : Model -> Document Msg
view model =
{ title = "Shoob book"
- , body = button [ onClick RequestLogout ] [ text "Logout" ] :: feedView model |> List.map toUnstyled
+ , body =
+ [ headerView model, feedView model ]
+ |> listFlat
+ |> List.map toUnstyled
}
-
-
-
--- UTILS
-
-
-httpErrorToString : Http.Error -> String
-httpErrorToString err =
- case err of
- Http.BadUrl url ->
- "URL {0} is invalid" |> templ [ url ]
-
- Http.Timeout ->
- "Request has timed out"
-
- Http.NetworkError ->
- "Unable to reach the server, check your network connection"
-
- Http.BadStatus status ->
- "Server responded with status {0}" |> templ [ String.fromInt status ]
-
- Http.BadBody msg ->
- msg
-
-
-templ : List String -> String -> String
-templ rs original =
- let
- templElement_ : ( Int, String ) -> String -> String
- templElement_ ( index, r ) orig_ =
- String.replace ("{" ++ String.fromInt index ++ "}") r orig_
- in
- List.indexedMap Tuple.pair rs
- |> List.foldl templElement_ original
diff --git a/src/Pages/Feed.elm b/src/Pages/Feed.elm
index 824552a..7fbe98a 100644
--- a/src/Pages/Feed.elm
+++ b/src/Pages/Feed.elm
@@ -3,18 +3,16 @@ module Pages.Feed exposing (..)
import Css exposing (..)
import DateFormat.Relative exposing (relativeTime)
import Html.Styled exposing (..)
-import Html.Styled.Attributes exposing (css, placeholder, type_, value)
+import Html.Styled.Attributes exposing (css, placeholder, src, type_, value)
import Html.Styled.Events exposing (onClick, onInput, onSubmit)
+import RemoteData exposing (RemoteData(..))
import Time exposing (now)
import Types exposing (Model, Msg(..), Post)
styles =
{ page =
- css
- [ maxWidth (px 600)
- , margin2 (px 10) auto
- ]
+ css [ margin2 (px 10) auto ]
, postList =
css
[ overflow scroll
@@ -27,6 +25,16 @@ styles =
, backgroundColor (hex "#eee")
, marginBottom (px 10)
]
+ , postProfilePicture =
+ css
+ [ width (px 40)
+ , height (px 40)
+ ]
+ , postHeader =
+ css
+ [ display inlineBlock
+ , marginLeft (px 10)
+ ]
, postTitle =
css
[ margin (px 0) ]
@@ -50,9 +58,13 @@ styles =
postDiv : Time.Posix -> Post -> Html Msg
postDiv now post =
div [ styles.post ]
- [ h3 [ styles.postTitle ] [ text post.author.name ]
- , i []
- [ text <| relativeTime now post.createdAt
+ [ span []
+ [ img [ styles.postProfilePicture, src post.userPictureUrl ] [] ]
+ , span [ styles.postHeader ]
+ [ h3 [ styles.postTitle ] [ text post.author.name ]
+ , i []
+ [ text <| relativeTime now post.createdAt
+ ]
]
, p [] [ text post.content ]
, span []
@@ -65,12 +77,18 @@ postDiv now post =
postsDiv : Model -> Html Msg
postsDiv model =
case model.posts of
- Just posts ->
- div [ styles.postList ] <| List.map (postDiv model.now) posts
+ Initial ->
+ div [] []
- Nothing ->
+ Loading ->
div [] [ text "Loading posts..." ]
+ Failure _ ->
+ div [] []
+
+ Success posts ->
+ div [ styles.postList ] <| List.map (postDiv model.now) posts
+
composeDiv : Model -> Html Msg
composeDiv model =
diff --git a/src/RemoteData.elm b/src/RemoteData.elm
new file mode 100644
index 0000000..7173851
--- /dev/null
+++ b/src/RemoteData.elm
@@ -0,0 +1,52 @@
+module RemoteData exposing (..)
+
+import Http
+
+
+type RemoteData a
+ = Initial
+ | Loading
+ | Failure Http.Error
+ | Success a
+
+
+withDefault : a -> RemoteData a -> a
+withDefault default rd =
+ case rd of
+ Success value ->
+ value
+
+ _ ->
+ default
+
+
+map : (a -> b) -> RemoteData a -> RemoteData b
+map fn rd =
+ case rd of
+ Initial ->
+ Initial
+
+ Loading ->
+ Loading
+
+ Failure err ->
+ Failure err
+
+ Success value ->
+ Success (fn value)
+
+
+andThen : (a -> RemoteData b) -> RemoteData a -> RemoteData b
+andThen callback rd =
+ case rd of
+ Initial ->
+ Initial
+
+ Loading ->
+ Loading
+
+ Failure err ->
+ Failure err
+
+ Success value ->
+ callback value
diff --git a/src/Types.elm b/src/Types.elm
index ce487b6..d5068d6 100644
--- a/src/Types.elm
+++ b/src/Types.elm
@@ -1,13 +1,24 @@
module Types exposing (..)
+import Browser.Navigation exposing (Key)
import Http
+import RemoteData exposing (RemoteData)
import Time
type alias Model =
- { posts : Maybe (List Post)
+ { now : Time.Posix
+ , key : Key
+ , userData : UserData
+ , posts : RemoteData (List Post)
, composeInputValue : String
- , now : Time.Posix
+ }
+
+
+type alias UserData =
+ { name : String
+ , email : String
+ , pictureUrl : String
}
@@ -39,4 +50,5 @@ type alias Post =
, author : Author
, createdAt : Time.Posix
, likes : Int
+ , userPictureUrl : String
}
diff --git a/src/Utils.elm b/src/Utils.elm
new file mode 100644
index 0000000..3da2c74
--- /dev/null
+++ b/src/Utils.elm
@@ -0,0 +1,42 @@
+module Utils exposing (..)
+
+import Http
+
+
+
+-- UTILS
+
+
+httpErrorToString : Http.Error -> String
+httpErrorToString err =
+ case err of
+ Http.BadUrl url ->
+ "URL {0} is invalid" |> templ [ url ]
+
+ Http.Timeout ->
+ "Request has timed out"
+
+ Http.NetworkError ->
+ "Unable to reach the server, check your network connection"
+
+ Http.BadStatus status ->
+ "Server responded with status {0}" |> templ [ String.fromInt status ]
+
+ Http.BadBody msg ->
+ msg
+
+
+templ : List String -> String -> String
+templ rs original =
+ let
+ templElement_ : ( Int, String ) -> String -> String
+ templElement_ ( index, r ) orig_ =
+ String.replace (String.replace "n" (String.fromInt index) "{n}") r orig_
+ in
+ List.indexedMap Tuple.pair rs
+ |> List.foldl templElement_ original
+
+
+listFlat : List (List a) -> List a
+listFlat ll =
+ List.foldl (\acc l -> l ++ acc) [] ll