elm-pages 3.0.0-beta.9 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/adapter/netlify.js +207 -0
- package/codegen/{elm-pages-codegen.js → elm-pages-codegen.cjs} +2731 -2939
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Pages-Review-DeadCodeEliminateData.elmi +0 -0
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Pages-Review-DeadCodeEliminateData.elmo +0 -0
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Pages-Review-DeadCodeEliminateDataTest.elmo +0 -0
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/d.dat +0 -0
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/i.dat +0 -0
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/o.dat +0 -0
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/elm.json +1 -1
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/js/Reporter.elm.js +1527 -422
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/js/Runner.elm.js +16840 -13653
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/js/node_runner.js +1 -1
- package/generator/dead-code-review/elm-stuff/tests-0.19.1/js/node_supervisor.js +4 -4
- package/generator/dead-code-review/elm.json +9 -7
- package/generator/dead-code-review/src/Pages/Review/DeadCodeEliminateData.elm +59 -10
- package/generator/dead-code-review/tests/Pages/Review/DeadCodeEliminateDataTest.elm +52 -36
- package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Pages-Internal-RoutePattern.elmi +0 -0
- package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Pages-Internal-RoutePattern.elmo +0 -0
- package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Pages-Review-NoContractViolations.elmi +0 -0
- package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/Pages-Review-NoContractViolations.elmo +0 -0
- package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/d.dat +0 -0
- package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/i.dat +0 -0
- package/generator/review/elm-stuff/tests-0.19.1/elm-stuff/0.19.1/o.dat +0 -0
- package/generator/review/elm-stuff/tests-0.19.1/elm.json +1 -1
- package/generator/review/elm-stuff/tests-0.19.1/js/Reporter.elm.js +1527 -422
- package/generator/review/elm-stuff/tests-0.19.1/js/Runner.elm.js +25118 -21832
- package/generator/review/elm-stuff/tests-0.19.1/js/node_runner.js +1 -1
- package/generator/review/elm-stuff/tests-0.19.1/js/node_supervisor.js +4 -4
- package/generator/review/elm.json +10 -10
- package/generator/src/RouteBuilder.elm +93 -128
- package/generator/src/SharedTemplate.elm +8 -7
- package/generator/src/SiteConfig.elm +3 -2
- package/generator/src/basepath-middleware.js +3 -3
- package/generator/src/build.js +143 -59
- package/generator/src/cli.js +292 -88
- package/generator/src/codegen.js +29 -27
- package/generator/src/compatibility-key.js +3 -0
- package/generator/src/compile-elm.js +43 -26
- package/generator/src/config.js +2 -4
- package/generator/src/copy-dir.js +2 -2
- package/generator/src/dev-server.js +160 -102
- package/generator/src/dir-helpers.js +9 -26
- package/generator/src/elm-codegen.js +5 -4
- package/generator/src/elm-file-constants.js +2 -3
- package/generator/src/error-formatter.js +12 -11
- package/generator/src/file-helpers.js +3 -4
- package/generator/src/generate-template-module-connector.js +23 -23
- package/generator/src/init.js +9 -8
- package/generator/src/pre-render-html.js +10 -13
- package/generator/src/render-test.js +109 -0
- package/generator/src/render-worker.js +25 -28
- package/generator/src/render.js +320 -143
- package/generator/src/request-cache.js +265 -162
- package/generator/src/resolve-elm-module.js +64 -0
- package/generator/src/rewrite-client-elm-json.js +6 -5
- package/generator/src/rewrite-elm-json-help.js +56 -0
- package/generator/src/rewrite-elm-json.js +17 -7
- package/generator/src/route-codegen-helpers.js +16 -31
- package/generator/src/seo-renderer.js +1 -3
- package/generator/src/vite-utils.js +1 -2
- package/generator/static-code/elm-pages.js +10 -0
- package/generator/static-code/hmr.js +79 -13
- package/generator/template/app/Api.elm +3 -2
- package/generator/template/app/Effect.elm +155 -0
- package/generator/template/app/ErrorPage.elm +49 -6
- package/generator/template/app/Route/Blog/Slug_.elm +86 -0
- package/generator/template/app/Route/Greet.elm +107 -0
- package/generator/template/app/Route/Hello.elm +119 -0
- package/generator/template/app/Route/Index.elm +26 -25
- package/generator/template/app/Shared.elm +38 -39
- package/generator/template/app/Site.elm +4 -7
- package/generator/template/app/View.elm +9 -8
- package/generator/template/codegen/elm.codegen.json +18 -0
- package/generator/template/custom-backend-task.ts +3 -0
- package/generator/template/elm-pages.config.mjs +13 -0
- package/generator/template/elm-tooling.json +0 -3
- package/generator/template/elm.json +25 -20
- package/generator/template/index.ts +1 -2
- package/generator/template/netlify.toml +4 -1
- package/generator/template/package.json +10 -4
- package/generator/template/script/.elm-pages/compiled-ports/custom-backend-task.mjs +7 -0
- package/generator/template/script/custom-backend-task.ts +3 -0
- package/generator/template/script/elm.json +61 -0
- package/generator/template/script/src/AddRoute.elm +312 -0
- package/generator/template/script/src/Stars.elm +42 -0
- package/package.json +30 -27
- package/src/ApiRoute.elm +249 -82
- package/src/BackendTask/Custom.elm +325 -0
- package/src/BackendTask/Env.elm +90 -0
- package/src/{DataSource → BackendTask}/File.elm +171 -56
- package/src/{DataSource → BackendTask}/Glob.elm +136 -125
- package/src/BackendTask/Http.elm +679 -0
- package/src/{DataSource → BackendTask}/Internal/Glob.elm +1 -1
- package/src/BackendTask/Internal/Request.elm +69 -0
- package/src/BackendTask/Random.elm +79 -0
- package/src/BackendTask/Time.elm +47 -0
- package/src/BackendTask.elm +531 -0
- package/src/FatalError.elm +90 -0
- package/src/FormData.elm +21 -18
- package/src/Head/Seo.elm +4 -4
- package/src/Head.elm +112 -8
- package/src/Internal/ApiRoute.elm +7 -5
- package/src/Internal/Request.elm +84 -4
- package/src/PageServerResponse.elm +6 -1
- package/src/Pages/ConcurrentSubmission.elm +127 -0
- package/src/Pages/Form.elm +340 -0
- package/src/Pages/FormData.elm +19 -0
- package/src/Pages/GeneratorProgramConfig.elm +15 -0
- package/src/Pages/Internal/FatalError.elm +5 -0
- package/src/Pages/Internal/Msg.elm +93 -0
- package/src/Pages/Internal/NotFoundReason.elm +4 -4
- package/src/Pages/Internal/Platform/Cli.elm +586 -768
- package/src/Pages/Internal/Platform/CompatibilityKey.elm +1 -1
- package/src/Pages/Internal/Platform/Effect.elm +1 -2
- package/src/Pages/Internal/Platform/GeneratorApplication.elm +379 -0
- package/src/Pages/Internal/Platform/StaticResponses.elm +65 -276
- package/src/Pages/Internal/Platform/ToJsPayload.elm +6 -9
- package/src/Pages/Internal/Platform.elm +330 -203
- package/src/Pages/Internal/ResponseSketch.elm +2 -2
- package/src/Pages/Internal/Script.elm +17 -0
- package/src/Pages/Internal/StaticHttpBody.elm +35 -1
- package/src/Pages/Manifest.elm +52 -11
- package/src/Pages/Navigation.elm +85 -0
- package/src/Pages/PageUrl.elm +26 -12
- package/src/Pages/ProgramConfig.elm +32 -22
- package/src/Pages/Script.elm +166 -0
- package/src/Pages/SiteConfig.elm +3 -2
- package/src/Pages/StaticHttp/Request.elm +2 -2
- package/src/Pages/StaticHttpRequest.elm +23 -99
- package/src/Pages/Url.elm +3 -3
- package/src/PagesMsg.elm +88 -0
- package/src/QueryParams.elm +21 -172
- package/src/RenderRequest.elm +7 -7
- package/src/RequestsAndPending.elm +37 -20
- package/src/Result/Extra.elm +26 -0
- package/src/Scaffold/Form.elm +569 -0
- package/src/Scaffold/Route.elm +1431 -0
- package/src/Server/Request.elm +476 -1001
- package/src/Server/Response.elm +130 -36
- package/src/Server/Session.elm +181 -111
- package/src/Server/SetCookie.elm +80 -32
- package/src/Stub.elm +53 -0
- package/src/Test/Html/Internal/ElmHtml/ToString.elm +8 -9
- package/src/{Path.elm → UrlPath.elm} +33 -36
- package/generator/template/public/images/icon-png.png +0 -0
- package/src/DataSource/Env.elm +0 -38
- package/src/DataSource/Http.elm +0 -446
- package/src/DataSource/Internal/Request.elm +0 -20
- package/src/DataSource/Port.elm +0 -90
- package/src/DataSource.elm +0 -546
- package/src/Form/Field.elm +0 -717
- package/src/Form/FieldStatus.elm +0 -36
- package/src/Form/FieldView.elm +0 -417
- package/src/Form/FormData.elm +0 -22
- package/src/Form/Validation.elm +0 -391
- package/src/Form/Value.elm +0 -118
- package/src/Form.elm +0 -1683
- package/src/FormDecoder.elm +0 -102
- package/src/Pages/FormState.elm +0 -256
- package/src/Pages/Generate.elm +0 -1242
- package/src/Pages/Internal/Form.elm +0 -17
- package/src/Pages/Internal/Platform/Cli.elm.bak +0 -1276
- package/src/Pages/Msg.elm +0 -79
- package/src/Pages/Transition.elm +0 -70
package/src/Server/Session.elm
CHANGED
|
@@ -1,48 +1,53 @@
|
|
|
1
1
|
module Server.Session exposing
|
|
2
|
-
( withSession
|
|
2
|
+
( withSession, withSessionResult
|
|
3
3
|
, NotLoadedReason(..)
|
|
4
4
|
, Session, empty, get, insert, remove, update, withFlash
|
|
5
5
|
)
|
|
6
6
|
|
|
7
|
-
{-| You can manage server state with HTTP cookies using this Server.Session API. Server-rendered
|
|
8
|
-
|
|
7
|
+
{-| You can manage server state with HTTP cookies using this Server.Session API. Server-rendered routes have a `Server.Request.Request`
|
|
8
|
+
argument that lets you inspect the incoming HTTP request, and return a response using the `Server.Response.Response` type.
|
|
9
9
|
|
|
10
|
+
This API provides a higher-level abstraction for extracting data from the HTTP request, and setting data in the HTTP response.
|
|
11
|
+
It manages the session through key-value data stored in cookies, and lets you [`insert`](#insert), [`update`](#update), and [`remove`](#remove)
|
|
12
|
+
values from the Session. It also provides an abstraction for flash session values through [`withFlash`](#withFlash).
|
|
10
13
|
|
|
11
|
-
|
|
14
|
+
|
|
15
|
+
## Using Sessions
|
|
12
16
|
|
|
13
17
|
Using these functions, you can store and read session data in cookies to maintain state between requests.
|
|
14
|
-
For example, TODO:
|
|
15
|
-
|
|
16
|
-
action : RouteParams -> Request.Parser (DataSource (Response ActionData ErrorPage))
|
|
17
|
-
action routeParams =
|
|
18
|
-
MySession.withSession
|
|
19
|
-
(Request.formDataWithServerValidation (form |> Form.initCombinedServer identity))
|
|
20
|
-
(\nameResultData session ->
|
|
21
|
-
nameResultData
|
|
22
|
-
|> DataSource.map
|
|
23
|
-
(\nameResult ->
|
|
24
|
-
case nameResult of
|
|
25
|
-
Err errors ->
|
|
26
|
-
( session
|
|
27
|
-
|> Result.withDefault Nothing
|
|
28
|
-
|> Maybe.withDefault Session.empty
|
|
29
|
-
, Response.render
|
|
30
|
-
{ errors = errors
|
|
31
|
-
}
|
|
32
|
-
)
|
|
33
18
|
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
19
|
+
import Server.Session as Session
|
|
20
|
+
|
|
21
|
+
secrets : BackendTask FatalError (List String)
|
|
22
|
+
secrets =
|
|
23
|
+
Env.expect "SESSION_SECRET"
|
|
24
|
+
|> BackendTask.allowFatal
|
|
25
|
+
|> BackendTask.map List.singleton
|
|
26
|
+
|
|
27
|
+
type alias Data =
|
|
28
|
+
{ darkMode : Bool }
|
|
29
|
+
|
|
30
|
+
data : RouteParams -> Request -> BackendTask (Response Data ErrorPage)
|
|
31
|
+
data routeParams request =
|
|
32
|
+
request
|
|
33
|
+
|> Session.withSession
|
|
34
|
+
{ name = "mysession"
|
|
35
|
+
, secrets = secrets
|
|
36
|
+
, options = Nothing
|
|
37
|
+
}
|
|
38
|
+
(\session ->
|
|
39
|
+
let
|
|
40
|
+
darkMode : Bool
|
|
41
|
+
darkMode =
|
|
42
|
+
(session |> Session.get "mode" |> Maybe.withDefault "light")
|
|
43
|
+
== "dark"
|
|
44
|
+
in
|
|
45
|
+
( session
|
|
46
|
+
, { darkMode = darkMode }
|
|
47
|
+
)
|
|
48
|
+
)
|
|
44
49
|
|
|
45
|
-
The elm-pages framework will manage signing these cookies using the `secrets :
|
|
50
|
+
The elm-pages framework will manage signing these cookies using the `secrets : BackendTask (List String)` you pass in.
|
|
46
51
|
That means that the values you set in your session will be directly visible to anyone who has access to the cookie
|
|
47
52
|
(so don't directly store sensitive data in your session). Since the session cookie is signed using the secret you provide,
|
|
48
53
|
the cookie will be invalidated if it is tampered with because it won't match when elm-pages verifies that it has been
|
|
@@ -51,15 +56,15 @@ signed with your secrets. Of course you need to provide secure secrets and treat
|
|
|
51
56
|
|
|
52
57
|
### Rotating Secrets
|
|
53
58
|
|
|
54
|
-
The first String in `secrets :
|
|
59
|
+
The first String in `secrets : BackendTask (List String)` will be used to sign sessions, while the remaining String's will
|
|
55
60
|
still be used to attempt to "unsign" the cookies. So if you have a single secret:
|
|
56
61
|
|
|
57
62
|
Session.withSession
|
|
58
63
|
{ name = "mysession"
|
|
59
64
|
, secrets =
|
|
60
|
-
|
|
65
|
+
BackendTask.map List.singleton
|
|
61
66
|
(Env.expect "SESSION_SECRET2022-09-01")
|
|
62
|
-
, options =
|
|
67
|
+
, options = Nothing
|
|
63
68
|
}
|
|
64
69
|
|
|
65
70
|
Then you add a second secret
|
|
@@ -67,11 +72,11 @@ Then you add a second secret
|
|
|
67
72
|
Session.withSession
|
|
68
73
|
{ name = "mysession"
|
|
69
74
|
, secrets =
|
|
70
|
-
|
|
75
|
+
BackendTask.map2
|
|
71
76
|
(\newSecret oldSecret -> [ newSecret, oldSecret ])
|
|
72
77
|
(Env.expect "SESSION_SECRET2022-12-01")
|
|
73
78
|
(Env.expect "SESSION_SECRET2022-09-01")
|
|
74
|
-
, options =
|
|
79
|
+
, options = Nothing
|
|
75
80
|
}
|
|
76
81
|
|
|
77
82
|
The new secret (`2022-12-01`) will be used to sign all requests. This API always re-signs using the newest secret in the list
|
|
@@ -87,16 +92,16 @@ it will invalidate all cookies signed with that. For example, if we remove our o
|
|
|
87
92
|
Session.withSession
|
|
88
93
|
{ name = "mysession"
|
|
89
94
|
, secrets =
|
|
90
|
-
|
|
95
|
+
BackendTask.map List.singleton
|
|
91
96
|
(Env.expect "SESSION_SECRET2022-12-01")
|
|
92
|
-
, options =
|
|
97
|
+
, options = Nothing
|
|
93
98
|
}
|
|
94
99
|
|
|
95
100
|
And then a user makes a request but had a session signed with our old secret (`2022-09-01`), the session will be invalid
|
|
96
101
|
(so `withSession` would parse the session for that request as `Nothing`). It's standard for cookies to have an expiration date,
|
|
97
102
|
so there's nothing wrong with an old session expiring (and the browser will eventually delete old cookies), just be aware of that when rotating secrets.
|
|
98
103
|
|
|
99
|
-
@docs withSession
|
|
104
|
+
@docs withSession, withSessionResult
|
|
100
105
|
|
|
101
106
|
@docs NotLoadedReason
|
|
102
107
|
|
|
@@ -107,18 +112,23 @@ so there's nothing wrong with an old session expiring (and the browser will even
|
|
|
107
112
|
|
|
108
113
|
-}
|
|
109
114
|
|
|
110
|
-
import
|
|
111
|
-
import
|
|
112
|
-
import
|
|
115
|
+
import BackendTask exposing (BackendTask)
|
|
116
|
+
import BackendTask.Http
|
|
117
|
+
import BackendTask.Internal.Request
|
|
113
118
|
import Dict exposing (Dict)
|
|
114
119
|
import Json.Decode
|
|
115
120
|
import Json.Encode
|
|
116
|
-
import Server.Request
|
|
121
|
+
import Server.Request exposing (Request)
|
|
117
122
|
import Server.Response exposing (Response)
|
|
118
123
|
import Server.SetCookie as SetCookie
|
|
119
124
|
|
|
120
125
|
|
|
121
|
-
{-| -
|
|
126
|
+
{-| Represents a Session with key-value Strings.
|
|
127
|
+
|
|
128
|
+
Use with `withSession` to read in the `Session`, and encode any changes you make to the `Session` back through cookie storage
|
|
129
|
+
via the outgoing HTTP response.
|
|
130
|
+
|
|
131
|
+
-}
|
|
122
132
|
type Session
|
|
123
133
|
= Session (Dict String Value)
|
|
124
134
|
|
|
@@ -130,7 +140,12 @@ type Value
|
|
|
130
140
|
| NewFlash String
|
|
131
141
|
|
|
132
142
|
|
|
133
|
-
{-|
|
|
143
|
+
{-| Flash session values are values that are only available for the next request.
|
|
144
|
+
|
|
145
|
+
session
|
|
146
|
+
|> Session.withFlash "message" "Your payment was successful!"
|
|
147
|
+
|
|
148
|
+
-}
|
|
134
149
|
withFlash : String -> String -> Session -> Session
|
|
135
150
|
withFlash key value (Session session) =
|
|
136
151
|
session
|
|
@@ -138,7 +153,12 @@ withFlash key value (Session session) =
|
|
|
138
153
|
|> Session
|
|
139
154
|
|
|
140
155
|
|
|
141
|
-
{-|
|
|
156
|
+
{-| Insert a value under the given key in the `Session`.
|
|
157
|
+
|
|
158
|
+
session
|
|
159
|
+
|> Session.insert "mode" "dark"
|
|
160
|
+
|
|
161
|
+
-}
|
|
142
162
|
insert : String -> String -> Session -> Session
|
|
143
163
|
insert key value (Session session) =
|
|
144
164
|
session
|
|
@@ -146,7 +166,15 @@ insert key value (Session session) =
|
|
|
146
166
|
|> Session
|
|
147
167
|
|
|
148
168
|
|
|
149
|
-
{-|
|
|
169
|
+
{-| Retrieve a String value from the session for the given key (or `Nothing` if the key is not present).
|
|
170
|
+
|
|
171
|
+
(session
|
|
172
|
+
|> Session.get "mode"
|
|
173
|
+
|> Maybe.withDefault "light"
|
|
174
|
+
)
|
|
175
|
+
== "dark"
|
|
176
|
+
|
|
177
|
+
-}
|
|
150
178
|
get : String -> Session -> Maybe String
|
|
151
179
|
get key (Session session) =
|
|
152
180
|
session
|
|
@@ -168,7 +196,25 @@ unwrap value =
|
|
|
168
196
|
string
|
|
169
197
|
|
|
170
198
|
|
|
171
|
-
{-|
|
|
199
|
+
{-| Update the `Session`, given a `Maybe String` of the current value for the given key, and returning a `Maybe String`.
|
|
200
|
+
|
|
201
|
+
If you return `Nothing`, the key-value pair will be removed from the `Session` (or left out if it didn't exist in the first place).
|
|
202
|
+
|
|
203
|
+
session
|
|
204
|
+
|> Session.update "mode"
|
|
205
|
+
(\mode ->
|
|
206
|
+
case mode of
|
|
207
|
+
Just "dark" ->
|
|
208
|
+
Just "light"
|
|
209
|
+
|
|
210
|
+
Just "light" ->
|
|
211
|
+
Just "dark"
|
|
212
|
+
|
|
213
|
+
Nothing ->
|
|
214
|
+
Just "dark"
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
-}
|
|
172
218
|
update : String -> (Maybe String -> Maybe String) -> Session -> Session
|
|
173
219
|
update key updateFn (Session session) =
|
|
174
220
|
session
|
|
@@ -192,7 +238,8 @@ update key updateFn (Session session) =
|
|
|
192
238
|
|> Session
|
|
193
239
|
|
|
194
240
|
|
|
195
|
-
{-|
|
|
241
|
+
{-| Remove a key from the `Session`.
|
|
242
|
+
-}
|
|
196
243
|
remove : String -> Session -> Session
|
|
197
244
|
remove key (Session session) =
|
|
198
245
|
session
|
|
@@ -200,13 +247,15 @@ remove key (Session session) =
|
|
|
200
247
|
|> Session
|
|
201
248
|
|
|
202
249
|
|
|
203
|
-
{-| -
|
|
250
|
+
{-| An empty `Session` with no key-value pairs.
|
|
251
|
+
-}
|
|
204
252
|
empty : Session
|
|
205
253
|
empty =
|
|
206
254
|
Session Dict.empty
|
|
207
255
|
|
|
208
256
|
|
|
209
|
-
{-|
|
|
257
|
+
{-| [`withSessionResult`](#withSessionResult) will return a `Result` with this type if it can't load a session.
|
|
258
|
+
-}
|
|
210
259
|
type NotLoadedReason
|
|
211
260
|
= NoSessionCookie
|
|
212
261
|
| InvalidSessionCookie
|
|
@@ -239,66 +288,87 @@ flashPrefix =
|
|
|
239
288
|
"__flash__"
|
|
240
289
|
|
|
241
290
|
|
|
242
|
-
{-| -
|
|
291
|
+
{-| The main function for using sessions. If you need more fine-grained control over cases where a session can't be loaded, see
|
|
292
|
+
[`withSessionResult`](#withSessionResult).
|
|
293
|
+
-}
|
|
243
294
|
withSession :
|
|
244
295
|
{ name : String
|
|
245
|
-
, secrets :
|
|
246
|
-
, options : SetCookie.Options
|
|
296
|
+
, secrets : BackendTask error (List String)
|
|
297
|
+
, options : Maybe SetCookie.Options
|
|
247
298
|
}
|
|
248
|
-
-> (
|
|
249
|
-
->
|
|
250
|
-
->
|
|
251
|
-
withSession config toRequest
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
299
|
+
-> (Session -> BackendTask error ( Session, Response data errorPage ))
|
|
300
|
+
-> Request
|
|
301
|
+
-> BackendTask error (Response data errorPage)
|
|
302
|
+
withSession config toRequest request_ =
|
|
303
|
+
request_
|
|
304
|
+
|> withSessionResult config
|
|
305
|
+
(\session ->
|
|
306
|
+
session
|
|
307
|
+
|> Result.withDefault empty
|
|
308
|
+
|> toRequest
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
{-| Same as `withSession`, but gives you an `Err` with the reason why the Session couldn't be loaded instead of
|
|
313
|
+
using `Session.empty` as a default in the cases where there is an error loading the session.
|
|
314
|
+
|
|
315
|
+
A session won't load if there is no session, or if it cannot be unsigned with your secrets. This could be because the cookie was tampered with
|
|
316
|
+
or otherwise corrupted, or because the cookie was signed with a secret that is no longer in the rotation.
|
|
317
|
+
|
|
318
|
+
-}
|
|
319
|
+
withSessionResult :
|
|
320
|
+
{ name : String
|
|
321
|
+
, secrets : BackendTask error (List String)
|
|
322
|
+
, options : Maybe SetCookie.Options
|
|
323
|
+
}
|
|
324
|
+
-> (Result NotLoadedReason Session -> BackendTask error ( Session, Response data errorPage ))
|
|
325
|
+
-> Request
|
|
326
|
+
-> BackendTask error (Response data errorPage)
|
|
327
|
+
withSessionResult config toTask request =
|
|
328
|
+
let
|
|
329
|
+
unsigned : BackendTask error (Result NotLoadedReason Session)
|
|
330
|
+
unsigned =
|
|
331
|
+
case Server.Request.cookie config.name request of
|
|
332
|
+
Just sessionCookie ->
|
|
333
|
+
sessionCookie
|
|
334
|
+
|> unsignCookie config
|
|
335
|
+
|> BackendTask.map
|
|
336
|
+
(\unsignResult ->
|
|
337
|
+
case unsignResult of
|
|
338
|
+
Ok decoded ->
|
|
339
|
+
Ok decoded
|
|
340
|
+
|
|
341
|
+
Err () ->
|
|
342
|
+
Err InvalidSessionCookie
|
|
343
|
+
)
|
|
270
344
|
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
)
|
|
279
|
-
(Server.Request.cookie config.name)
|
|
280
|
-
userRequest
|
|
345
|
+
Nothing ->
|
|
346
|
+
Err NoSessionCookie
|
|
347
|
+
|> BackendTask.succeed
|
|
348
|
+
in
|
|
349
|
+
unsigned
|
|
350
|
+
|> BackendTask.andThen
|
|
351
|
+
(encodeSessionUpdate config toTask)
|
|
281
352
|
|
|
282
353
|
|
|
283
354
|
encodeSessionUpdate :
|
|
284
355
|
{ name : String
|
|
285
|
-
, secrets :
|
|
286
|
-
, options : SetCookie.Options
|
|
356
|
+
, secrets : BackendTask error (List String)
|
|
357
|
+
, options : Maybe SetCookie.Options
|
|
287
358
|
}
|
|
288
|
-
-> (
|
|
289
|
-
-> c
|
|
359
|
+
-> (d -> BackendTask error ( Session, Response data errorPage ))
|
|
290
360
|
-> d
|
|
291
|
-
->
|
|
292
|
-
encodeSessionUpdate config toRequest
|
|
361
|
+
-> BackendTask error (Response data errorPage)
|
|
362
|
+
encodeSessionUpdate config toRequest sessionResult =
|
|
293
363
|
sessionResult
|
|
294
|
-
|> toRequest
|
|
295
|
-
|>
|
|
364
|
+
|> toRequest
|
|
365
|
+
|> BackendTask.andThen
|
|
296
366
|
(\( sessionUpdate, response ) ->
|
|
297
|
-
|
|
367
|
+
BackendTask.map
|
|
298
368
|
(\encoded ->
|
|
299
369
|
response
|
|
300
370
|
|> Server.Response.withSetCookieHeader
|
|
301
|
-
(SetCookie.setCookie config.name encoded config.options)
|
|
371
|
+
(SetCookie.setCookie config.name encoded (config.options |> Maybe.withDefault SetCookie.options))
|
|
302
372
|
)
|
|
303
373
|
(sign config.secrets
|
|
304
374
|
(encodeNonExpiringPairs sessionUpdate)
|
|
@@ -306,11 +376,11 @@ encodeSessionUpdate config toRequest userRequestData sessionResult =
|
|
|
306
376
|
)
|
|
307
377
|
|
|
308
378
|
|
|
309
|
-
unsignCookie : { a | secrets :
|
|
379
|
+
unsignCookie : { a | secrets : BackendTask error (List String) } -> String -> BackendTask error (Result () Session)
|
|
310
380
|
unsignCookie config sessionCookie =
|
|
311
381
|
sessionCookie
|
|
312
382
|
|> unsign config.secrets (Json.Decode.dict Json.Decode.string)
|
|
313
|
-
|>
|
|
383
|
+
|> BackendTask.map
|
|
314
384
|
(Result.map
|
|
315
385
|
(\dict ->
|
|
316
386
|
dict
|
|
@@ -331,15 +401,15 @@ unsignCookie config sessionCookie =
|
|
|
331
401
|
)
|
|
332
402
|
|
|
333
403
|
|
|
334
|
-
sign :
|
|
404
|
+
sign : BackendTask error (List String) -> Json.Encode.Value -> BackendTask error String
|
|
335
405
|
sign getSecrets input =
|
|
336
406
|
getSecrets
|
|
337
|
-
|>
|
|
407
|
+
|> BackendTask.andThen
|
|
338
408
|
(\secrets ->
|
|
339
|
-
|
|
409
|
+
BackendTask.Internal.Request.request
|
|
340
410
|
{ name = "encrypt"
|
|
341
411
|
, body =
|
|
342
|
-
|
|
412
|
+
BackendTask.Http.jsonBody
|
|
343
413
|
(Json.Encode.object
|
|
344
414
|
[ ( "values", input )
|
|
345
415
|
, ( "secret"
|
|
@@ -353,21 +423,21 @@ sign getSecrets input =
|
|
|
353
423
|
]
|
|
354
424
|
)
|
|
355
425
|
, expect =
|
|
356
|
-
|
|
426
|
+
BackendTask.Http.expectJson
|
|
357
427
|
Json.Decode.string
|
|
358
428
|
}
|
|
359
429
|
)
|
|
360
430
|
|
|
361
431
|
|
|
362
|
-
unsign :
|
|
432
|
+
unsign : BackendTask error (List String) -> Json.Decode.Decoder a -> String -> BackendTask error (Result () a)
|
|
363
433
|
unsign getSecrets decoder input =
|
|
364
434
|
getSecrets
|
|
365
|
-
|>
|
|
435
|
+
|> BackendTask.andThen
|
|
366
436
|
(\secrets ->
|
|
367
|
-
|
|
437
|
+
BackendTask.Internal.Request.request
|
|
368
438
|
{ name = "decrypt"
|
|
369
439
|
, body =
|
|
370
|
-
|
|
440
|
+
BackendTask.Http.jsonBody
|
|
371
441
|
(Json.Encode.object
|
|
372
442
|
[ ( "input", Json.Encode.string input )
|
|
373
443
|
, ( "secrets", Json.Encode.list Json.Encode.string secrets )
|
|
@@ -377,6 +447,6 @@ unsign getSecrets decoder input =
|
|
|
377
447
|
decoder
|
|
378
448
|
|> Json.Decode.nullable
|
|
379
449
|
|> Json.Decode.map (Result.fromMaybe ())
|
|
380
|
-
|>
|
|
450
|
+
|> BackendTask.Http.expectJson
|
|
381
451
|
}
|
|
382
452
|
)
|
package/src/Server/SetCookie.elm
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
module Server.SetCookie exposing
|
|
2
|
-
( SetCookie
|
|
3
|
-
,
|
|
4
|
-
,
|
|
5
|
-
, withImmediateExpiration, makeVisibleToJavaScript, nonSecure,
|
|
2
|
+
( SetCookie, setCookie
|
|
3
|
+
, Options, options
|
|
4
|
+
, SameSite(..), withSameSite
|
|
5
|
+
, withImmediateExpiration, makeVisibleToJavaScript, nonSecure, withDomain, withExpiration, withMaxAge, withPath, withoutPath
|
|
6
6
|
, toString
|
|
7
7
|
)
|
|
8
8
|
|
|
@@ -20,16 +20,29 @@ You can learn more about the basics of cookies in the Web Platform in these help
|
|
|
20
20
|
- <https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie>
|
|
21
21
|
- <https://developer.mozilla.org/en-US/docs/Web/HTTP/Cookies>
|
|
22
22
|
|
|
23
|
-
@docs SetCookie
|
|
23
|
+
@docs SetCookie, setCookie
|
|
24
24
|
|
|
25
|
-
@docs SameSite
|
|
26
25
|
|
|
26
|
+
## Building Options
|
|
27
27
|
|
|
28
|
-
|
|
28
|
+
Usually you'll want to start by creating default `Options` with `options` and then overriding defaults using the `with...` helpers.
|
|
29
29
|
|
|
30
|
-
|
|
30
|
+
import Server.SetCookie as SetCookie
|
|
31
31
|
|
|
32
|
-
|
|
32
|
+
options : SetCookie.Options
|
|
33
|
+
options =
|
|
34
|
+
SetCookie.options
|
|
35
|
+
|> SetCookie.nonSecure
|
|
36
|
+
|> SetCookie.withMaxAge 123
|
|
37
|
+
|> SetCookie.makeVisibleToJavaScript
|
|
38
|
+
|> SetCookie.withoutPath
|
|
39
|
+
|> SetCookie.setCookie "id" "a3fWa"
|
|
40
|
+
|
|
41
|
+
@docs Options, options
|
|
42
|
+
|
|
43
|
+
@docs SameSite, withSameSite
|
|
44
|
+
|
|
45
|
+
@docs withImmediateExpiration, makeVisibleToJavaScript, nonSecure, withDomain, withExpiration, withMaxAge, withPath, withoutPath
|
|
33
46
|
|
|
34
47
|
|
|
35
48
|
## Internal
|
|
@@ -51,7 +64,8 @@ type alias SetCookie =
|
|
|
51
64
|
}
|
|
52
65
|
|
|
53
66
|
|
|
54
|
-
{-|
|
|
67
|
+
{-| The set of possible configuration options. You can configure this record directly, or use the `with...` helpers.
|
|
68
|
+
-}
|
|
55
69
|
type alias Options =
|
|
56
70
|
{ expiration : Maybe Time.Posix
|
|
57
71
|
, visibleToJavaScript : Bool
|
|
@@ -63,7 +77,15 @@ type alias Options =
|
|
|
63
77
|
}
|
|
64
78
|
|
|
65
79
|
|
|
66
|
-
{-| -
|
|
80
|
+
{-| Possible values for [the cookie's same-site value](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value).
|
|
81
|
+
|
|
82
|
+
The default option is [`Lax`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#lax) (Lax does not send
|
|
83
|
+
cookies in cross-origin requests so it is a good default for most cases, but [`Strict`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#strict)
|
|
84
|
+
is even more restrictive).
|
|
85
|
+
|
|
86
|
+
Override the default option using [`withSameSite`](#withSameSite).
|
|
87
|
+
|
|
88
|
+
-}
|
|
67
89
|
type SameSite
|
|
68
90
|
= Strict
|
|
69
91
|
| Lax
|
|
@@ -95,24 +117,24 @@ toString builder =
|
|
|
95
117
|
else
|
|
96
118
|
""
|
|
97
119
|
|
|
98
|
-
|
|
99
|
-
|
|
120
|
+
options_ : Options
|
|
121
|
+
options_ =
|
|
100
122
|
builder.options
|
|
101
123
|
|
|
102
124
|
httpOnly : Bool
|
|
103
125
|
httpOnly =
|
|
104
|
-
not
|
|
126
|
+
not options_.visibleToJavaScript
|
|
105
127
|
in
|
|
106
128
|
builder.name
|
|
107
129
|
++ "="
|
|
108
130
|
++ Url.percentEncode builder.value
|
|
109
|
-
++ option "Expires" (
|
|
110
|
-
++ option "Max-Age" (
|
|
111
|
-
++ option "Path"
|
|
112
|
-
++ option "Domain"
|
|
113
|
-
++ option "SameSite" (
|
|
131
|
+
++ option "Expires" (options_.expiration |> Maybe.map Utc.fromTime)
|
|
132
|
+
++ option "Max-Age" (options_.maxAge |> Maybe.map String.fromInt)
|
|
133
|
+
++ option "Path" options_.path
|
|
134
|
+
++ option "Domain" options_.domain
|
|
135
|
+
++ option "SameSite" (options_.sameSite |> Maybe.map sameSiteToString)
|
|
114
136
|
++ boolOption "HttpOnly" httpOnly
|
|
115
|
-
++ boolOption "Secure"
|
|
137
|
+
++ boolOption "Secure" options_.secure
|
|
116
138
|
|
|
117
139
|
|
|
118
140
|
sameSiteToString : SameSite -> String
|
|
@@ -128,22 +150,26 @@ sameSiteToString sameSite =
|
|
|
128
150
|
"None"
|
|
129
151
|
|
|
130
152
|
|
|
131
|
-
{-| -
|
|
153
|
+
{-| Create a `SetCookie` record with the given name, value, and [`Options`](Options]. To add a `Set-Cookie` header, you can
|
|
154
|
+
pass this value with [`Server.Response.withSetCookieHeader`](Server-Response#withSetCookieHeader). Or for more low-level
|
|
155
|
+
uses you can stringify the value manually with [`toString`](#toString).
|
|
156
|
+
-}
|
|
132
157
|
setCookie : String -> String -> Options -> SetCookie
|
|
133
|
-
setCookie name value
|
|
158
|
+
setCookie name value options_ =
|
|
134
159
|
{ name = name
|
|
135
160
|
, value = value
|
|
136
|
-
, options =
|
|
161
|
+
, options = options_
|
|
137
162
|
}
|
|
138
163
|
|
|
139
164
|
|
|
140
|
-
{-|
|
|
141
|
-
|
|
142
|
-
|
|
165
|
+
{-| Initialize the default `SetCookie` `Options`. Can be configured directly through a record update, or with `withExpiration`, etc.
|
|
166
|
+
-}
|
|
167
|
+
options : Options
|
|
168
|
+
options =
|
|
143
169
|
{ expiration = Nothing
|
|
144
170
|
, visibleToJavaScript = False
|
|
145
171
|
, maxAge = Nothing
|
|
146
|
-
, path =
|
|
172
|
+
, path = Just "/"
|
|
147
173
|
, domain = Nothing
|
|
148
174
|
, secure = True
|
|
149
175
|
, sameSite = Nothing
|
|
@@ -158,7 +184,9 @@ withExpiration time builder =
|
|
|
158
184
|
}
|
|
159
185
|
|
|
160
186
|
|
|
161
|
-
{-| -
|
|
187
|
+
{-| Sets [`Expires`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#expiresdate) to `Time.millisToPosix 0`,
|
|
188
|
+
which effectively tells the browser to delete the cookie immediately (by giving it an expiration date in the past).
|
|
189
|
+
-}
|
|
162
190
|
withImmediateExpiration : Options -> Options
|
|
163
191
|
withImmediateExpiration builder =
|
|
164
192
|
{ builder
|
|
@@ -174,7 +202,7 @@ dynamically). In this API you opt into exposing a cookie you set to JavaScript t
|
|
|
174
202
|
|
|
175
203
|
In general if you can accomplish your goal using HttpOnly cookies (i.e. not using `makeVisibleToJavaScript`) then
|
|
176
204
|
it's a good practice. With server-rendered `elm-pages` applications you can often manage your session state by pulling
|
|
177
|
-
in session data from cookies in a `
|
|
205
|
+
in session data from cookies in a `BackendTask` (which is resolved server-side before it ever reaches the browser).
|
|
178
206
|
|
|
179
207
|
-}
|
|
180
208
|
makeVisibleToJavaScript : Options -> Options
|
|
@@ -184,7 +212,8 @@ makeVisibleToJavaScript builder =
|
|
|
184
212
|
}
|
|
185
213
|
|
|
186
214
|
|
|
187
|
-
{-| -
|
|
215
|
+
{-| Sets the `Set-Cookie`'s [`Max-Age`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#max-agenumber).
|
|
216
|
+
-}
|
|
188
217
|
withMaxAge : Int -> Options -> Options
|
|
189
218
|
withMaxAge maxAge builder =
|
|
190
219
|
{ builder
|
|
@@ -192,7 +221,11 @@ withMaxAge maxAge builder =
|
|
|
192
221
|
}
|
|
193
222
|
|
|
194
223
|
|
|
195
|
-
{-| -
|
|
224
|
+
{-| Sets the `Set-Cookie`'s [`Path`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#pathpath-value).
|
|
225
|
+
|
|
226
|
+
The default value is `/`, which will match any sub-directories or the root directory. See also [\`withoutPath](#withoutPath)
|
|
227
|
+
|
|
228
|
+
-}
|
|
196
229
|
withPath : String -> Options -> Options
|
|
197
230
|
withPath path builder =
|
|
198
231
|
{ builder
|
|
@@ -200,7 +233,22 @@ withPath path builder =
|
|
|
200
233
|
}
|
|
201
234
|
|
|
202
235
|
|
|
203
|
-
{-|
|
|
236
|
+
{-|
|
|
237
|
+
|
|
238
|
+
> If the server omits the Path attribute, the user agent will use the "directory" of the request-uri's path component as the default value.
|
|
239
|
+
|
|
240
|
+
Source: <https://www.rfc-editor.org/rfc/rfc6265>. See <https://stackoverflow.com/a/43336097>.
|
|
241
|
+
|
|
242
|
+
-}
|
|
243
|
+
withoutPath : Options -> Options
|
|
244
|
+
withoutPath builder =
|
|
245
|
+
{ builder
|
|
246
|
+
| path = Nothing
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
{-| Sets the `Set-Cookie`'s [`Domain`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#domaindomain-value).
|
|
251
|
+
-}
|
|
204
252
|
withDomain : String -> Options -> Options
|
|
205
253
|
withDomain domain builder =
|
|
206
254
|
{ builder
|