├── .gitignore
├── src
├── styles.css
├── index.html
└── Main.elm
├── elm-package.json
├── package.json
├── LICENSE
├── gulpfile.js
└── README.md
/.gitignore:
--------------------------------------------------------------------------------
1 | /elm-stuff
2 | /node_modules
3 | /dist
4 |
--------------------------------------------------------------------------------
/src/styles.css:
--------------------------------------------------------------------------------
1 | .container {
2 | margin: 1em auto;
3 | max-width: 600px;
4 | }
5 | blockquote {
6 | margin: 1em 0;
7 | }
8 | .jumbotron {
9 | margin: 2em auto;
10 | max-width: 400px;
11 | }
12 | .jumbotron h2 {
13 | margin-top: 0;
14 | }
15 | .jumbotron .help-block {
16 | font-size: 14px;
17 | }
--------------------------------------------------------------------------------
/elm-package.json:
--------------------------------------------------------------------------------
1 | {
2 | "version": "1.0.0",
3 | "summary": "Build an App in Elm with JWT Authentication and an API",
4 | "repository": "https://github.com/auth0-blog/elm-with-jwt-api.git",
5 | "license": "MIT",
6 | "source-directories": [
7 | "src",
8 | "dist"
9 | ],
10 | "exposed-modules": [],
11 | "dependencies": {
12 | "elm-lang/core": "5.1.1 <= v < 6.0.0",
13 | "elm-lang/html": "2.0.0 <= v < 3.0.0",
14 | "elm-lang/http": "1.0.0 <= v < 2.0.0",
15 | "rgrempel/elm-http-decorators": "2.0.0 <= v < 3.0.0"
16 | },
17 | "elm-version": "0.18.0 <= v < 0.19.0"
18 | }
19 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "elm-app-jwt-api",
3 | "version": "1.0.0",
4 | "author": "Auth0",
5 | "description": "Authenticating an Elm App with JWT",
6 | "main": "",
7 | "repository": {
8 | "type": "git",
9 | "url": "https://github.com/auth0-blog/elm-app-jwt-api.git"
10 | },
11 | "keywords": [
12 | "elm"
13 | ],
14 | "license": "MIT",
15 | "bugs": {
16 | "url": "https://github.com/auth0-blog/elm-app-jwt-api/issues"
17 | },
18 | "scripts": {
19 | "dev": "gulp",
20 | "build": "gulp build"
21 | },
22 | "homepage": "https://github.com/auth0-blog/elm-app-jwt-api",
23 | "dependencies": {},
24 | "devDependencies": {
25 | "gulp": "^3.9.0",
26 | "gulp-connect": "^4.0.0",
27 | "gulp-elm": "^0.6.1",
28 | "gulp-plumber": "^1.1.0",
29 | "gulp-util": "^3.0.7"
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/src/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 | Chuck Norris Quoter
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
27 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2017 Auth0
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/gulpfile.js:
--------------------------------------------------------------------------------
1 | var gulp = require('gulp');
2 | var elm = require('gulp-elm');
3 | var gutil = require('gulp-util');
4 | var plumber = require('gulp-plumber');
5 | var connect = require('gulp-connect');
6 |
7 | // File paths
8 | var paths = {
9 | dest: 'dist',
10 | elm: 'src/*.elm',
11 | static: 'src/*.{html,css}'
12 | };
13 |
14 | // Init Elm
15 | gulp.task('elm-init', elm.init);
16 |
17 | // Compile Elm
18 | gulp.task('elm', ['elm-init'], function(){
19 | return gulp.src(paths.elm)
20 | .pipe(plumber())
21 | .pipe(elm())
22 | .pipe(gulp.dest(paths.dest));
23 | });
24 |
25 | // Move static assets to dist
26 | gulp.task('static', function() {
27 | return gulp.src(paths.static)
28 | .pipe(plumber())
29 | .pipe(gulp.dest(paths.dest));
30 | });
31 |
32 | // Watch for changes and compile
33 | gulp.task('watch', function() {
34 | gulp.watch(paths.elm, ['elm']);
35 | gulp.watch(paths.static, ['static']);
36 | });
37 |
38 | // Local server
39 | gulp.task('connect', function() {
40 | connect.server({
41 | root: 'dist',
42 | port: 3000
43 | });
44 | });
45 |
46 | // Main gulp tasks
47 | gulp.task('build', ['elm', 'static']);
48 | gulp.task('default', ['connect', 'build', 'watch']);
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Creating Your First Elm App: From Authentication to Calling an API
2 |
3 | This is the source code repository for:
4 |
5 | [Creating Your First Elm App: From Authentication to Calling an API (Part 1)](https://auth0.com/blog/creating-your-first-elm-app-part-1)
6 |
7 | [Creating Your First Elm App: From Authentication to Calling an API (Part 2)](https://auth0.com/blog/creating-your-first-elm-app-part-2)
8 |
9 | Tutorial steps for `Main.elm` are available as [branches](https://github.com/auth0-blog/elm-with-jwt-api/branches).
10 |
11 | ## What is Auth0?
12 |
13 | Auth0 helps you to:
14 |
15 | * Add authentication with [multiple authentication sources](https://docs.auth0.com/identityproviders), either social like **Google, Facebook, Microsoft Account, LinkedIn, GitHub, Twitter, Box, Salesforce, amont others**, or enterprise identity systems like **Windows Azure AD, Google Apps, Active Directory, ADFS or any SAML Identity Provider**.
16 | * Add authentication through more traditional **[username/password databases](https://docs.auth0.com/mysql-connection-tutorial)**.
17 | * Add support for **[linking different user accounts](https://docs.auth0.com/link-accounts)** with the same user.
18 | * Support for generating signed [Json Web Tokens](https://docs.auth0.com/jwt) to call your APIs and **flow the user identity** securely.
19 | * Analytics of how, when and where users are logging in.
20 | * Pull data from other sources and add it to the user profile, through [JavaScript rules](https://docs.auth0.com/rules).
21 |
22 | ## Create a Free Auth0 Account
23 |
24 | 1. Go to [Auth0](https://auth0.com) and click Sign Up.
25 | 2. Use Google, GitHub, or Microsoft Account to log in.
26 |
27 | ## Issue Reporting
28 |
29 | If you have found a bug or if you have a feature request, please report them at this repository issues section. Please do not report security vulnerabilities on the public GitHub issue tracker. The [Responsible Disclosure Program](https://auth0.com/whitehat) details the procedure for disclosing security issues.
30 |
31 | ## Author
32 |
33 | [Auth0](auth0.com)
34 |
35 | ## License
36 |
37 | This project is licensed under the MIT license. See the [LICENSE](LICENSE) file for more info.
38 |
--------------------------------------------------------------------------------
/src/Main.elm:
--------------------------------------------------------------------------------
1 | port module Main exposing (..)
2 |
3 | import Html exposing (..)
4 | import Html.Events exposing (..)
5 | import Html.Attributes exposing (..)
6 | import Http
7 | import Json.Decode as Decode exposing (..)
8 | import Json.Encode as Encode exposing (..)
9 |
10 |
11 | main : Program (Maybe Model) Model Msg
12 | main =
13 | Html.programWithFlags
14 | { init = init
15 | , update = update
16 | , subscriptions = \_ -> Sub.none
17 | , view = view
18 | }
19 |
20 |
21 |
22 | {-
23 | MODEL
24 | * Model type
25 | * Initialize model with empty values
26 | * Initialize with a random quote
27 | -}
28 |
29 |
30 | type alias Model =
31 | { username : String
32 | , password : String
33 | , token : String
34 | , quote : String
35 | , protectedQuote : String
36 | , errorMsg : String
37 | }
38 |
39 |
40 | init : Maybe Model -> ( Model, Cmd Msg )
41 | init model =
42 | case model of
43 | Just model ->
44 | ( model, fetchRandomQuoteCmd )
45 |
46 | Nothing ->
47 | ( Model "" "" "" "" "" "", fetchRandomQuoteCmd )
48 |
49 |
50 |
51 | {-
52 | UPDATE
53 | * API routes
54 | * GET and POST
55 | * Encode request body
56 | * Decode responses
57 | * Messages
58 | * Ports
59 | * Update case
60 | -}
61 | -- API request URLs
62 |
63 |
64 | api : String
65 | api =
66 | "http://localhost:3001/"
67 |
68 |
69 | randomQuoteUrl : String
70 | randomQuoteUrl =
71 | api ++ "api/random-quote"
72 |
73 |
74 | registerUrl : String
75 | registerUrl =
76 | api ++ "users"
77 |
78 |
79 | loginUrl : String
80 | loginUrl =
81 | api ++ "sessions/create"
82 |
83 |
84 | protectedQuoteUrl : String
85 | protectedQuoteUrl =
86 | api ++ "api/protected/random-quote"
87 |
88 |
89 |
90 | -- GET a random quote (unauthenticated)
91 |
92 |
93 | fetchRandomQuote : Http.Request String
94 | fetchRandomQuote =
95 | Http.getString randomQuoteUrl
96 |
97 |
98 | fetchRandomQuoteCmd : Cmd Msg
99 | fetchRandomQuoteCmd =
100 | Http.send FetchRandomQuoteCompleted fetchRandomQuote
101 |
102 |
103 | fetchRandomQuoteCompleted : Model -> Result Http.Error String -> ( Model, Cmd Msg )
104 | fetchRandomQuoteCompleted model result =
105 | case result of
106 | Ok newQuote ->
107 | setStorageHelper { model | quote = newQuote }
108 |
109 | Err _ ->
110 | ( model, Cmd.none )
111 |
112 |
113 | -- Encode user to construct POST request body (for Register and Log In)
114 |
115 |
116 | userEncoder : Model -> Encode.Value
117 | userEncoder model =
118 | Encode.object
119 | [ ( "username", Encode.string model.username )
120 | , ( "password", Encode.string model.password )
121 | ]
122 |
123 |
124 |
125 | -- POST register / login request
126 |
127 |
128 | authUser : Model -> String -> Http.Request String
129 | authUser model apiUrl =
130 | let
131 | body =
132 | model
133 | |> userEncoder
134 | |> Http.jsonBody
135 | in
136 | Http.post apiUrl body tokenDecoder
137 |
138 |
139 | authUserCmd : Model -> String -> Cmd Msg
140 | authUserCmd model apiUrl =
141 | Http.send GetTokenCompleted (authUser model apiUrl)
142 |
143 |
144 |
145 | getTokenCompleted : Model -> Result Http.Error String -> ( Model, Cmd Msg )
146 | getTokenCompleted model result =
147 | case result of
148 | Ok newToken ->
149 | setStorageHelper { model | token = newToken, password = "", errorMsg = "" }
150 |
151 | Err error ->
152 | ( { model | errorMsg = (toString error) }, Cmd.none )
153 |
154 |
155 |
156 | -- Decode POST response to get access token
157 |
158 |
159 | tokenDecoder : Decoder String
160 | tokenDecoder =
161 | Decode.field "access_token" Decode.string
162 |
163 |
164 |
165 | -- GET request for random protected quote (authenticated)
166 |
167 |
168 | fetchProtectedQuote : Model -> Http.Request String
169 | fetchProtectedQuote model =
170 | { method = "GET"
171 | , headers = [ Http.header "Authorization" ("Bearer " ++ model.token) ]
172 | , url = protectedQuoteUrl
173 | , body = Http.emptyBody
174 | , expect = Http.expectString
175 | , timeout = Nothing
176 | , withCredentials = False
177 | }
178 | |> Http.request
179 |
180 |
181 | fetchProtectedQuoteCmd : Model -> Cmd Msg
182 | fetchProtectedQuoteCmd model =
183 | Http.send FetchProtectedQuoteCompleted (fetchProtectedQuote model)
184 |
185 |
186 | fetchProtectedQuoteCompleted : Model -> Result Http.Error String -> ( Model, Cmd Msg )
187 | fetchProtectedQuoteCompleted model result =
188 | case result of
189 | Ok newPQuote ->
190 | setStorageHelper { model | protectedQuote = newPQuote }
191 |
192 | Err _ ->
193 | ( model, Cmd.none )
194 |
195 |
196 |
197 | -- Helper to update model and set localStorage with the updated model
198 |
199 |
200 | setStorageHelper : Model -> ( Model, Cmd Msg )
201 | setStorageHelper model =
202 | ( model, setStorage model )
203 |
204 |
205 |
206 | -- Messages
207 |
208 |
209 | type Msg
210 | = GetQuote
211 | | FetchRandomQuoteCompleted (Result Http.Error String)
212 | | SetUsername String
213 | | SetPassword String
214 | | ClickRegisterUser
215 | | ClickLogIn
216 | | GetTokenCompleted (Result Http.Error String)
217 | | GetProtectedQuote
218 | | FetchProtectedQuoteCompleted (Result Http.Error String)
219 | | LogOut
220 |
221 |
222 |
223 | -- Ports
224 |
225 |
226 | port setStorage : Model -> Cmd msg
227 |
228 |
229 | port removeStorage : Model -> Cmd msg
230 |
231 |
232 |
233 | -- Update
234 |
235 |
236 | update : Msg -> Model -> ( Model, Cmd Msg )
237 | update msg model =
238 | case msg of
239 | GetQuote ->
240 | ( model, fetchRandomQuoteCmd )
241 |
242 | FetchRandomQuoteCompleted result ->
243 | fetchRandomQuoteCompleted model result
244 |
245 | SetUsername username ->
246 | ( { model | username = username }, Cmd.none )
247 |
248 | SetPassword password ->
249 | ( { model | password = password }, Cmd.none )
250 |
251 | ClickRegisterUser ->
252 | ( model, authUserCmd model registerUrl )
253 |
254 | ClickLogIn ->
255 | ( model, authUserCmd model loginUrl )
256 |
257 | GetTokenCompleted result ->
258 | getTokenCompleted model result
259 |
260 | GetProtectedQuote ->
261 | ( model, fetchProtectedQuoteCmd model )
262 |
263 | FetchProtectedQuoteCompleted result ->
264 | fetchProtectedQuoteCompleted model result
265 |
266 | LogOut ->
267 | ( { model | username = "", protectedQuote = "", token = "" }, removeStorage model )
268 |
269 |
270 |
271 | {-
272 | VIEW
273 | * Hide sections of view depending on authenticaton state of model
274 | * Get a quote
275 | * Log In or Register
276 | * Get a protected quote
277 | -}
278 |
279 |
280 | view : Model -> Html Msg
281 | view model =
282 | let
283 | -- Is the user logged in?
284 | loggedIn : Bool
285 | loggedIn =
286 | if String.length model.token > 0 then
287 | True
288 | else
289 | False
290 |
291 | -- If the user is logged in, show a greeting; if logged out, show the login/register form
292 | authBoxView =
293 | let
294 | -- If there is an error on authentication, show the error alert
295 | showError : String
296 | showError =
297 | if String.isEmpty model.errorMsg then
298 | "hidden"
299 | else
300 | ""
301 |
302 | -- Greet a logged in user by username
303 | greeting : String
304 | greeting =
305 | "Hello, " ++ model.username ++ "!"
306 | in
307 | if loggedIn then
308 | div [ id "greeting" ]
309 | [ h3 [ class "text-center" ] [ text greeting ]
310 | , p [ class "text-center" ] [ text "You have super-secret access to protected quotes." ]
311 | , p [ class "text-center" ]
312 | [ button [ class "btn btn-danger", onClick LogOut ] [ text "Log Out" ]
313 | ]
314 | ]
315 | else
316 | div [ id "form" ]
317 | [ h2 [ class "text-center" ] [ text "Log In or Register" ]
318 | , p [ class "help-block" ] [ text "If you already have an account, please Log In. Otherwise, enter your desired username and password and Register." ]
319 | , div [ class showError ]
320 | [ div [ class "alert alert-danger" ] [ text model.errorMsg ]
321 | ]
322 | , div [ class "form-group row" ]
323 | [ div [ class "col-md-offset-2 col-md-8" ]
324 | [ label [ for "username" ] [ text "Username:" ]
325 | , input [ id "username", type_ "text", class "form-control", Html.Attributes.value model.username, onInput SetUsername ] []
326 | ]
327 | ]
328 | , div [ class "form-group row" ]
329 | [ div [ class "col-md-offset-2 col-md-8" ]
330 | [ label [ for "password" ] [ text "Password:" ]
331 | , input [ id "password", type_ "password", class "form-control", Html.Attributes.value model.password, onInput SetPassword ] []
332 | ]
333 | ]
334 | , div [ class "text-center" ]
335 | [ button [ class "btn btn-primary", onClick ClickLogIn ] [ text "Log In" ]
336 | , button [ class "btn btn-link", onClick ClickRegisterUser ] [ text "Register" ]
337 | ]
338 | ]
339 |
340 | -- If user is logged in, show button and quote; if logged out, show a message instructing them to log in
341 | protectedQuoteView =
342 | let
343 | -- If no protected quote, apply a class of "hidden"
344 | hideIfNoProtectedQuote : String
345 | hideIfNoProtectedQuote =
346 | if String.isEmpty model.protectedQuote then
347 | "hidden"
348 | else
349 | ""
350 | in
351 | if loggedIn then
352 | div []
353 | [ p [ class "text-center" ]
354 | [ button [ class "btn btn-info", onClick GetProtectedQuote ] [ text "Grab a protected quote!" ]
355 | ]
356 | -- Blockquote with protected quote: only show if a protectedQuote is present in model
357 | , blockquote [ class hideIfNoProtectedQuote ]
358 | [ p [] [ text model.protectedQuote ]
359 | ]
360 | ]
361 | else
362 | p [ class "text-center" ] [ text "Please log in or register to see protected quotes." ]
363 | in
364 | div [ class "container" ]
365 | [ h2 [ class "text-center" ] [ text "Chuck Norris Quotes" ]
366 | , p [ class "text-center" ]
367 | [ button [ class "btn btn-success", onClick GetQuote ] [ text "Grab a quote!" ]
368 | ]
369 | -- Blockquote with quote
370 | , blockquote []
371 | [ p [] [ text model.quote ]
372 | ]
373 | , div [ class "jumbotron text-left" ]
374 | [ -- Login/Register form or user greeting
375 | authBoxView
376 | ]
377 | , div []
378 | [ h2 [ class "text-center" ] [ text "Protected Chuck Norris Quotes" ]
379 | -- Protected quotes
380 | , protectedQuoteView
381 | ]
382 | ]
383 |
--------------------------------------------------------------------------------