diff --git a/src/ApiRoute.elm b/src/ApiRoute.elm index e16d4ba22..c75885784 100644 --- a/src/ApiRoute.elm +++ b/src/ApiRoute.elm @@ -65,10 +65,11 @@ You define your ApiRoute's in `app/Api.elm`. Here's a simple example: import ApiRoute import BackendTask exposing (BackendTask) + import FatalError exposing (FatalError) import Server.Request routes : - BackendTask (List Route) + BackendTask FatalError (List Route) -> (Maybe { indent : Int, newLines : Bool } -> Html Never -> String) -> List (ApiRoute.ApiRoute ApiRoute.Response) routes getStaticRoutes htmlToString = diff --git a/src/BackendTask.elm b/src/BackendTask.elm index 7a1d9aa79..a35b46aff 100644 --- a/src/BackendTask.elm +++ b/src/BackendTask.elm @@ -140,6 +140,7 @@ resolve = {-| Turn a list of `BackendTask`s into a single one. import BackendTask + import FatalError exposing (FatalError) import Json.Decode as Decode exposing (Decoder) type alias Pokemon = @@ -147,7 +148,7 @@ resolve = , sprite : String } - pokemonDetailRequest : BackendTask (List Pokemon) + pokemonDetailRequest : BackendTask FatalError (List Pokemon) pokemonDetailRequest = BackendTask.Http.getJson "https://pokeapi.co/api/v2/pokemon/?limit=3" @@ -169,6 +170,7 @@ resolve = ) ) |> BackendTask.andThen BackendTask.combine + |> BackendTask.allowFatal -} combine : List (BackendTask error value) -> BackendTask error (List value) @@ -190,11 +192,11 @@ combine items = } ) (get - (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages") + "https://api.github.com/repos/dillonkearns/elm-pages" (Decode.field "stargazers_count" Decode.int) ) (get - (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-markdown") + "https://api.github.com/repos/dillonkearns/elm-markdown" (Decode.field "stargazers_count" Decode.int) ) @@ -239,17 +241,19 @@ map2 fn request1 request2 = from the previous response to build up the URL, headers, etc. that you send to the subsequent request. import BackendTask + import FatalError exposing (FatalError) import Json.Decode as Decode exposing (Decoder) - licenseData : BackendTask String + licenseData : BackendTask FatalError String licenseData = - BackendTask.Http.get - (Secrets.succeed "https://api.github.com/repos/dillonkearns/elm-pages") + BackendTask.Http.getJson + "https://api.github.com/repos/dillonkearns/elm-pages" (Decode.at [ "license", "url" ] Decode.string) |> BackendTask.andThen (\licenseUrl -> - BackendTask.Http.get (Secrets.succeed licenseUrl) (Decode.field "description" Decode.string) + BackendTask.Http.getJson licenseUrl (Decode.field "description" Decode.string) ) + |> BackendTask.allowFatal -} andThen : (a -> BackendTask error b) -> BackendTask error a -> BackendTask error b diff --git a/src/BackendTask/Glob.elm b/src/BackendTask/Glob.elm index 049727afb..f47d0be8f 100644 --- a/src/BackendTask/Glob.elm +++ b/src/BackendTask/Glob.elm @@ -28,7 +28,7 @@ With the `BackendTask.Glob` API, you could get all of those files like so: import BackendTask exposing (BackendTask) - blogPostsGlob : BackendTask (List String) + blogPostsGlob : BackendTask error (List String) blogPostsGlob = Glob.succeed (\slug -> slug) |> Glob.match (Glob.literal "content/blog/") @@ -69,7 +69,7 @@ There will be one argument for every `capture` in your pipeline, whereas `match` import BackendTask exposing (BackendTask) import BackendTask.Glob as Glob - blogPostsGlob : BackendTask (List String) + blogPostsGlob : BackendTask error (List String) blogPostsGlob = Glob.succeed (\slug -> slug) -- no argument from this, but we will only @@ -97,7 +97,7 @@ Let's try our blogPostsGlob from before, but change every `match` to `capture`. import BackendTask exposing (BackendTask) blogPostsGlob : - BackendTask + BackendTask error (List { filePath : String , slug : String @@ -155,20 +155,22 @@ This is my first post! Then we could read that title for our blog post list page using our `blogPosts` `BackendTask` that we defined above. import BackendTask.File + import FatalError exposing (FatalError) import Json.Decode as Decode exposing (Decoder) - titles : BackendTask (List BlogPost) + titles : BackendTask FatalError (List BlogPost) titles = blogPosts |> BackendTask.map (List.map (\blogPost -> - BackendTask.File.request + BackendTask.File.onlyFrontmatter + blogFrontmatterDecoder blogPost.filePath - (BackendTask.File.frontmatter blogFrontmatterDecoder) ) ) |> BackendTask.resolve + |> BackendTask.allowFatal type alias BlogPost = { title : String } @@ -249,7 +251,7 @@ could use import BackendTask exposing (BackendTask) import BackendTask.Glob as Glob - blogPostsGlob : BackendTask (List String) + blogPostsGlob : BackendTask error (List String) blogPostsGlob = Glob.succeed (\slug -> slug) |> Glob.match (Glob.literal "content/blog/") @@ -258,14 +260,14 @@ could use |> Glob.toBackendTask If you want to validate file formats, you can combine that with some `BackendTask` helpers to turn a `Glob (Result String value)` into -a `BackendTask (List value)`. +a `BackendTask FatalError (List value)`. For example, you could take a date and parse it. import BackendTask exposing (BackendTask) import BackendTask.Glob as Glob - example : BackendTask (List ( String, String )) + example : BackendTask FatalError (List ( String, String )) example = Glob.succeed (\dateResult slug -> @@ -281,14 +283,14 @@ For example, you could take a date and parse it. |> BackendTask.map (List.map BackendTask.fromResult) |> BackendTask.resolve - expectDateFormat : List String -> Result String String + expectDateFormat : List String -> Result FatalError String expectDateFormat dateParts = case dateParts of [ year, month, date ] -> Ok (String.join "-" [ year, month, date ]) _ -> - Err "Unexpected date format, expected yyyy/mm/dd folder structure." + Err <| FatalError.fromString "Unexpected date format, expected yyyy/mm/dd folder structure." -} map : (a -> b) -> Glob a -> Glob b @@ -322,7 +324,7 @@ fullFilePath = import BackendTask.Glob as Glob blogPosts : - BackendTask + BackendTask error (List { filePath : String , slug : String @@ -368,11 +370,11 @@ match 0 or more path parts like, see `recursiveWildcard`. , slug : String } - example : BackendTask (List BlogPost) + example : BackendTask error (List BlogPost) example = Glob.succeed BlogPost |> Glob.match (Glob.literal "blog/") - |> Glob.match Glob.wildcard + |> Glob.capture Glob.wildcard |> Glob.match (Glob.literal "-") |> Glob.capture Glob.wildcard |> Glob.match (Glob.literal "-") @@ -391,7 +393,7 @@ match 0 or more path parts like, see `recursiveWildcard`. That will match to: - results : BackendTask (List BlogPost) + results : BackendTask error (List BlogPost) results = BackendTask.succeed [ { year = "2021" @@ -443,7 +445,7 @@ Leading 0's are ignored. import BackendTask exposing (BackendTask) import BackendTask.Glob as Glob - slides : BackendTask (List Int) + slides : BackendTask error (List Int) slides = Glob.succeed identity |> Glob.match (Glob.literal "slide-") @@ -472,7 +474,7 @@ With files Yields - matches : BackendTask (List Int) + matches : BackendTask error (List Int) matches = BackendTask.succeed [ 1 @@ -513,7 +515,7 @@ This is the elm-pages equivalent of `**/*.txt` in standard shell syntax: import BackendTask exposing (BackendTask) import BackendTask.Glob as Glob - example : BackendTask (List ( List String, String )) + example : BackendTask error (List ( List String, String )) example = Glob.succeed Tuple.pair |> Glob.match (Glob.literal "articles/") @@ -537,7 +539,7 @@ With these files: We would get the following matches: - matches : BackendTask (List ( List String, String )) + matches : BackendTask error (List ( List String, String )) matches = BackendTask.succeed [ ( [ "archive", "1977", "06", "10" ], "apple-2-announced" ) @@ -552,14 +554,14 @@ And also note that it matches 0 path parts into an empty list. If we didn't include the `wildcard` after the `recursiveWildcard`, then we would only get a single level of matches because it is followed by a file extension. - example : BackendTask (List String) + example : BackendTask error (List String) example = Glob.succeed identity |> Glob.match (Glob.literal "articles/") |> Glob.capture Glob.recursiveWildcard |> Glob.match (Glob.literal ".txt") - matches : BackendTask (List String) + matches : BackendTask error (List String) matches = BackendTask.succeed [ "google-io-2021-recap" @@ -677,7 +679,7 @@ Exactly the same as `match` except it also captures the matched sub-pattern. , slug : String } - archives : BackendTask ArchivesArticle + archives : BackendTask error ArchivesArticle archives = Glob.succeed ArchivesArticle |> Glob.match (Glob.literal "archive/") @@ -693,7 +695,7 @@ Exactly the same as `match` except it also captures the matched sub-pattern. The file `archive/1977/06/10/apple-2-released.md` will give us this match: - matches : List ArchivesArticle + matches : List error ArchivesArticle matches = BackendTask.succeed [ { year = 1977 @@ -744,7 +746,7 @@ capture (Glob matcherPattern apply1) (Glob pattern apply2) = , extension : String } - dataFiles : BackendTask (List DataFile) + dataFiles : BackendTask error (List DataFile) dataFiles = Glob.succeed DataFile |> Glob.match (Glob.literal "my-data/") @@ -768,7 +770,7 @@ If we have the following files That gives us - results : BackendTask (List DataFile) + results : BackendTask error (List DataFile) results = BackendTask.succeed [ { name = "authors" @@ -781,7 +783,7 @@ That gives us You could also match an optional file path segment using `oneOf`. - rootFilesMd : BackendTask (List String) + rootFilesMd : BackendTask error (List String) rootFilesMd = Glob.succeed (\slug -> slug) |> Glob.match (Glob.literal "blog/") @@ -806,7 +808,7 @@ With these files: This would give us: - results : BackendTask (List String) + results : BackendTask error (List String) results = BackendTask.succeed [ "first-post" @@ -965,7 +967,7 @@ encodeOptions options = import BackendTask.Glob as Glob exposing (OnlyFolders, defaultOptions) - matchingFiles : Glob a -> BackendTask (List a) + matchingFiles : Glob a -> BackendTask error (List a) matchingFiles glob = glob |> Glob.toBackendTaskWithOptions { defaultOptions | include = OnlyFolders } @@ -1010,12 +1012,12 @@ For example, maybe you can have import BackendTask exposing (BackendTask) import BackendTask.Glob as Glob - findBlogBySlug : String -> BackendTask String + findBlogBySlug : String -> BackendTask FatalError String findBlogBySlug slug = Glob.succeed identity |> Glob.captureFilePath |> Glob.match (Glob.literal "blog/") - |> Glob.capture (Glob.literal slug) + |> Glob.match (Glob.literal slug) |> Glob.match (Glob.oneOf ( ( "", () ) @@ -1024,6 +1026,7 @@ For example, maybe you can have ) |> Glob.match (Glob.literal ".md") |> Glob.expectUniqueMatch + |> BackendTask.allowFatal If we used `findBlogBySlug "first-post"` with these files: @@ -1035,7 +1038,7 @@ If we used `findBlogBySlug "first-post"` with these files: This would give us: - results : BackendTask String + results : BackendTask FatalError String results = BackendTask.succeed "blog/first-post/index.md" diff --git a/src/Server/Session.elm b/src/Server/Session.elm index c5b71b5fc..b0f6ca856 100644 --- a/src/Server/Session.elm +++ b/src/Server/Session.elm @@ -27,7 +27,7 @@ Using these functions, you can store and read session data in cookies to maintai type alias Data = { darkMode : Bool } - data : RouteParams -> Request -> BackendTask (Response Data ErrorPage) + data : RouteParams -> Request -> BackendTask FatalError (Response Data ErrorPage) data routeParams request = request |> Session.withSession @@ -42,12 +42,15 @@ Using these functions, you can store and read session data in cookies to maintai (session |> Session.get "mode" |> Maybe.withDefault "light") == "dark" in - ( session - , { darkMode = darkMode } - ) + BackendTask.succeed + ( session + , Response.render + { darkMode = darkMode + } + ) ) -The elm-pages framework will manage signing these cookies using the `secrets : BackendTask (List String)` you pass in. +The elm-pages framework will manage signing these cookies using the `secrets : BackendTask FatalError (List String)` you pass in. That means that the values you set in your session will be directly visible to anyone who has access to the cookie (so don't directly store sensitive data in your session). Since the session cookie is signed using the secret you provide, the cookie will be invalidated if it is tampered with because it won't match when elm-pages verifies that it has been @@ -56,7 +59,7 @@ signed with your secrets. Of course you need to provide secure secrets and treat ### Rotating Secrets -The first String in `secrets : BackendTask (List String)` will be used to sign sessions, while the remaining String's will +The first String in `secrets : BackendTask FatalError (List String)` will be used to sign sessions, while the remaining String's will still be used to attempt to "unsign" the cookies. So if you have a single secret: Session.withSession