Compare commits

...

No commits in common. "70a315165f90a6350a11bddd53b81233d4114ca3" and "40ee0c16d7afdabbfe8bd430d645203aab28d1d4" have entirely different histories.

34 changed files with 107 additions and 15778 deletions

5
.gitignore vendored
View file

@ -116,8 +116,3 @@ dist
.yarn/install-state.gz .yarn/install-state.gz
.pnp.* .pnp.*
static/
.idea
elm-stuff/

View file

@ -3,7 +3,5 @@
"https": false, "https": false,
"alter_db": true, "alter_db": true,
"port": 8080, "port": 8080,
"db_url": "postgres://postgres:@127.0.0.1/todo", "db_url": "postgres://postgres:@127.0.0.1/todo"
"cert": "",
"cert_key": ""
} }

View file

@ -1,29 +0,0 @@
{
"type": "application",
"source-directories": [
"src"
],
"elm-version": "0.19.1",
"dependencies": {
"direct": {
"elm/browser": "1.0.2",
"elm/core": "1.0.5",
"elm/html": "1.0.0",
"elm/http": "2.0.0",
"elm/json": "1.1.3",
"elm/time": "1.0.0",
"elm/url": "1.0.0",
"f0i/iso8601": "1.1.2",
"mdgriffith/elm-ui": "1.1.8"
},
"indirect": {
"elm/bytes": "1.0.8",
"elm/file": "1.0.5",
"elm/virtual-dom": "1.0.2"
}
},
"test-dependencies": {
"direct": {},
"indirect": {}
}
}

File diff suppressed because it is too large Load diff

View file

@ -1,9 +0,0 @@
module About exposing (..)
import Element exposing (..)
import Model exposing (..)
getPage : Model -> Element Msg
getPage _ =
text "About page."

View file

@ -1,9 +0,0 @@
module Account exposing (..)
import Element exposing (..)
import Model exposing (..)
getPage : Model -> Element Msg
getPage _ =
text "Account page."

View file

@ -1,290 +0,0 @@
port module Api exposing (Cred, addServerError, application, decodeErrors, delete, get, login, logout, post, put, register, settings, storeCredWith, username, viewerChanges)
{-| This module is responsible for communicating to the Conduit API.
It exposes an opaque Endpoint type which is guaranteed to point to the correct URL.
-}
import Api.Endpoint as Endpoint exposing (Endpoint)
import Avatar exposing (Avatar)
import Browser
import Browser.Navigation as Nav
import Http exposing (Body, Expect)
import Json.Decode as Decode exposing (Decoder, Value, decodeString, field, string)
import Json.Encode as Encode
import Url exposing (Url)
import Username exposing (Username)
-- CRED
{-| The authentication credentials for the Viewer (that is, the currently logged-in user.)
This includes:
- The cred's Username
- The cred's authentication token
By design, there is no way to access the token directly as a String.
It can be encoded for persistence, and it can be added to a header
to a HttpBuilder for a request, but that's it.
This token should never be rendered to the end user, and with this API, it
can't be!
-}
type Cred
= Cred Username String
username : Cred -> Username
username (Cred val _) =
val
credHeader : Cred -> Http.Header
credHeader (Cred _ str) =
Http.header "authorization" ("Token " ++ str)
{-| It's important that this is never exposed!
We expose `login` and `application` instead, so we can be certain that if anyone
ever has access to a `Cred` value, it came from either the login API endpoint
or was passed in via flags.
-}
credDecoder : Decoder Cred
credDecoder =
Decode.succeed Cred
|> field "username" Username.decoder
|> field "token" Decode.string
-- PERSISTENCE
decode : Decoder (Cred -> viewer) -> Value -> Result Decode.Error viewer
decode decoder value =
-- It's stored in localStorage as a JSON String;
-- first decode the Value as a String, then
-- decode that String as JSON.
Decode.decodeValue Decode.string value
|> Result.andThen (\str -> Decode.decodeString (Decode.field "user" (decoderFromCred decoder)) str)
port onStoreChange : (Value -> msg) -> Sub msg
viewerChanges : (Maybe viewer -> msg) -> Decoder (Cred -> viewer) -> Sub msg
viewerChanges toMsg decoder =
onStoreChange (\value -> toMsg (decodeFromChange decoder value))
decodeFromChange : Decoder (Cred -> viewer) -> Value -> Maybe viewer
decodeFromChange viewerDecoder val =
-- It's stored in localStorage as a JSON String;
-- first decode the Value as a String, then
-- decode that String as JSON.
Decode.decodeValue (storageDecoder viewerDecoder) val
|> Result.toMaybe
storeCredWith : Cred -> Avatar -> Cmd msg
storeCredWith (Cred uname token) avatar =
let
json =
Encode.object
[ ( "user"
, Encode.object
[ ( "username", Username.encode uname )
, ( "token", Encode.string token )
, ( "image", Avatar.encode avatar )
]
)
]
in
storeCache (Just json)
logout : Cmd msg
logout =
storeCache Nothing
port storeCache : Maybe Value -> Cmd msg
-- SERIALIZATION
-- APPLICATION
application :
Decoder (Cred -> viewer)
->
{ init : Maybe viewer -> Url -> Nav.Key -> ( model, Cmd msg )
, onUrlChange : Url -> msg
, onUrlRequest : Browser.UrlRequest -> msg
, subscriptions : model -> Sub msg
, update : msg -> model -> ( model, Cmd msg )
, view : model -> Browser.Document msg
}
-> Program Value model msg
application viewerDecoder config =
let
init flags url navKey =
let
maybeViewer =
Decode.decodeValue Decode.string flags
|> Result.andThen (Decode.decodeString (storageDecoder viewerDecoder))
|> Result.toMaybe
in
config.init maybeViewer url navKey
in
Browser.application
{ init = init
, onUrlChange = config.onUrlChange
, onUrlRequest = config.onUrlRequest
, subscriptions = config.subscriptions
, update = config.update
, view = config.view
}
storageDecoder : Decoder (Cred -> viewer) -> Decoder viewer
storageDecoder viewerDecoder =
Decode.field "user" (decoderFromCred viewerDecoder)
-- HTTP
get : Endpoint -> Maybe Cred -> Decoder a -> Cmd a
get url maybeCred decoder =
Endpoint.request
{ method = "GET"
, url = url
, expect = Http.expectJson decoder
, headers =
case maybeCred of
Just cred ->
[ credHeader cred ]
Nothing ->
[]
, body = Http.emptyBody
, timeout = Nothing
, withCredentials = False
}
put : Endpoint -> Cred -> Body -> Decoder a -> Cmd a
put url cred body decoder =
Endpoint.request
{ method = "PUT"
, url = url
, expect = Http.expectJson decoder
, headers = [ credHeader cred ]
, body = body
, timeout = Nothing
, withCredentials = False
}
post : Endpoint -> Maybe Cred -> Body -> Decoder a -> Cmd a
post url maybeCred body decoder =
Endpoint.request
{ method = "POST"
, url = url
, expect = Http.expectJson decoder
, headers =
case maybeCred of
Just cred ->
[ credHeader cred ]
Nothing ->
[]
, body = body
, timeout = Nothing
, withCredentials = False
}
delete : Endpoint -> Cred -> Body -> Decoder a -> Cmd a
delete url cred body decoder =
Endpoint.request
{ method = "DELETE"
, url = url
, expect = Http.expectJson decoder
, headers = [ credHeader cred ]
, body = body
, timeout = Nothing
, withCredentials = False
}
login : Http.Body -> Decoder (Cred -> a) -> Cmd a
login body decoder =
post Endpoint.login Nothing body (Decode.field "user" (decoderFromCred decoder))
register : Http.Body -> Decoder (Cred -> a) -> Cmd a
register body decoder =
post Endpoint.users Nothing body (Decode.field "user" (decoderFromCred decoder))
settings : Cred -> Http.Body -> Decoder (Cred -> a) -> Cmd a
settings cred body decoder =
put Endpoint.user cred body (Decode.field "user" (decoderFromCred decoder))
decoderFromCred : Decoder (Cred -> a) -> Decoder a
decoderFromCred decoder =
Decode.map2 (\fromCred cred -> fromCred cred)
decoder
credDecoder
-- ERRORS
addServerError : List String -> List String
addServerError list =
"Server error" :: list
{-| Many API endpoints include an "errors" field in their BadStatus responses.
-}
decodeErrors : Http.Error -> List String
decodeErrors error =
case error of
Http.BadStatus errid ->
[ Int.toString errid ]
err ->
[ "Server error" ]
errorsDecoder : Decoder (List String)
errorsDecoder =
Decode.keyValuePairs (Decode.list Decode.string)
|> Decode.map (List.concatMap fromPair)
fromPair : ( String, List String ) -> List String
fromPair ( field, errors ) =
List.map (\error -> field ++ " " ++ error) errors
-- LOCALSTORAGE KEYS
cacheStorageKey : String
cacheStorageKey =
"cache"
credStorageKey : String
credStorageKey =
"cred"

View file

@ -1,99 +0,0 @@
module Api.Endpoint exposing (Endpoint, login, request, tags, todo, todoList, user, users)
import Http
import Todo.UUID as UUID exposing (UUID)
import Url.Builder exposing (QueryParameter)
import Username exposing (Username)
{-| Http.request, except it takes an Endpoint instead of a Url.
-}
request :
{ body : Http.Body
, expect : Http.Expect a
, headers : List Http.Header
, method : String
, timeout : Maybe Float
, url : Endpoint
, tracker : Maybe String
}
-> Cmd a
request config =
Http.request
{ body = config.body
, expect = config.expect
, headers = config.headers
, method = config.method
, timeout = config.timeout
, url = unwrap config.url
, tracker = config.tracker
}
-- TYPES
{-| Get a URL to the Conduit API.
This is not publicly exposed, because we want to make sure the only way to get one of these URLs is from this module.
-}
type Endpoint
= Endpoint String
unwrap : Endpoint -> String
unwrap (Endpoint str) =
str
url : List String -> List QueryParameter -> Endpoint
url paths queryParams =
-- NOTE: Url.Builder takes care of percent-encoding special URL characters.
-- See https://package.elm-lang.org/packages/elm/url/latest/Url#percentEncode
Url.Builder.crossOrigin "https://conduit.productionready.io"
("api" :: paths)
queryParams
|> Endpoint
-- ENDPOINTS
login : Endpoint
login =
url [ "users", "login" ] []
user : Endpoint
user =
url [ "user" ] []
users : Endpoint
users =
url [ "users" ] []
follow : Username -> Endpoint
follow uname =
url [ "profiles", Username.toString uname, "follow" ] []
-- ARTICLE ENDPOINTS
todo : UUID -> Endpoint
todo uuid =
url [ "articles", UUID.toString uuid ] []
todoList : List QueryParameter -> Endpoint
todoList params =
url [ "articles" ] params
tags : Endpoint
tags =
url [ "tags" ] []

View file

@ -1,46 +0,0 @@
module Asset exposing (Image, defaultAvatar, error, loading, src)
{-| Assets, such as images, videos, and audio. (We only have images for now.)
We should never expose asset URLs directly; this module should be in charge of
all of them. One source of truth!
-}
import Html exposing (Attribute, Html)
import Html.Attributes as Attr
type Image
= Image String
-- IMAGES
error : Image
error =
image "error.jpg"
loading : Image
loading =
image "loading.svg"
defaultAvatar : Image
defaultAvatar =
image "smiley-cyrus.jpg"
image : String -> Image
image filename =
Image ("/assets/images/" ++ filename)
-- USING IMAGES
src : Image -> Attribute msg
src (Image url) =
Attr.src url

View file

@ -1,56 +0,0 @@
module Avatar exposing (Avatar, decoder, encode, src, toMaybeString)
import Asset
import Html exposing (Attribute)
import Html.Attributes
import Json.Decode as Decode exposing (Decoder)
import Json.Encode as Encode exposing (Value)
-- TYPES
type Avatar
= Avatar (Maybe String)
-- CREATE
decoder : Decoder Avatar
decoder =
Decode.map Avatar (Decode.nullable Decode.string)
-- TRANSFORM
encode : Avatar -> Value
encode (Avatar maybeUrl) =
case maybeUrl of
Just url ->
Encode.string url
Nothing ->
Encode.null
src : Avatar -> Attribute msg
src (Avatar maybeUrl) =
case maybeUrl of
Nothing ->
Asset.src Asset.defaultAvatar
Just "" ->
Asset.src Asset.defaultAvatar
Just url ->
Html.Attributes.src url
toMaybeString : Avatar -> Maybe String
toMaybeString (Avatar maybeUrl) =
maybeUrl

View file

@ -1,31 +0,0 @@
-- this is a list of common styled elements
module CommonElements exposing (..)
import Element exposing (..)
import Element.Font as Font
bolded : String -> Element msg
bolded content =
el
[ Font.bold ]
(text content)
namedLink : String -> String -> Element msg
namedLink path name =
link []
{ url = "/#" ++ path
, label = text name
}
debugLog : a -> a
debugLog value =
let
_ =
Debug.log "value: " value
in
value

View file

@ -1,38 +0,0 @@
module Email exposing (Email, decoder, encode, toString)
import Json.Decode as Decode exposing (Decoder)
import Json.Encode as Encode exposing (Value)
{-| An email address.
Having this as a custom type that's separate from String makes certain
mistakes impossible. Consider this function:
updateEmailAddress : Email -> String -> Http.Request
updateEmailAddress email password = ...
(The server needs your password to confirm that you should be allowed
to update the email address.)
Because Email is not a type alias for String, but is instead a separate
custom type, it is now impossible to mix up the argument order of the
email and the password. If we do, it won't compile!
If Email were instead defined as `type alias Email = String`, we could
call updateEmailAddress password email and it would compile (and never
work properly).
This way, we make it impossible for a bug like that to compile!
-}
type Email
= Email String
toString : Email -> String
toString (Email str) =
str
encode : Email -> Value
encode (Email str) =
Encode.string str
decoder : Decoder Email
decoder =
Decode.map Email Decode.string

View file

@ -1,9 +0,0 @@
module Home exposing (..)
import Element exposing (..)
import Model exposing (..)
getPage : Model -> Element Msg
getPage _ =
text "Home page."

View file

@ -1,40 +0,0 @@
module Login exposing (..)
import CommonElements exposing (..)
import Element exposing (..)
import Element.Input as Input
import Model exposing (..)
getPage : Model -> Element Msg
getPage model =
column []
[ loginBox model
, namedLink "/signup" "Sign Up"
]
loginBox : Model -> Element Msg
loginBox model =
column []
[ text "Log in"
, Input.email
[ Input.focusedOnLoad
]
{ onChange = CredentialsChange "loginUsername"
, text = Maybe.withDefault "" model.storage.loginUsername
, placeholder = Just (Input.placeholder [] (text ""))
, label = Input.labelHidden "email"
}
, Input.currentPassword []
{ onChange = CredentialsChange "loginPassword"
, text = Maybe.withDefault "" model.storage.loginPassword
, placeholder = Just (Input.placeholder [] (text ""))
, label = Input.labelHidden "password"
, show = False
}
, Input.button []
{ onPress = Just SubmitLogin
, label = text "Log In"
}
]

View file

@ -1,178 +0,0 @@
module Main exposing (..)
import Browser
import Browser.Navigation as Nav
import CommonElements exposing (..)
import Element exposing (..)
import NavRow exposing (..)
import PageState exposing (..)
import Session exposing (Session)
import Url
import Viewer exposing (..)
type Model
= Redirect Session
| NotFound Session
| About Session
| Home Home.Model
| Account Account.Model
| Login Login.Model
| Signup Signup.Model
| Todo Todo.Model
| Editor (Maybe UUID) Editor.Model
type Msg
= UrlChange Url.Url
| Request Browser.UrlRequest
| GotHomeMsg Home.Msg
| GotAccountMsg Account.Msg
| GotLoginMsg Login.Msg
| GotSignupMsg Signup.Msg
| GotEditorMsg Editor.Msg
| GotTodoMsg Todo.Msg
| GotSession Session
init : Maybe Viewer -> Url.Url -> Nav.Key -> ( Model, Cmd Msg )
init maybeViewer url key =
changeRouteTo (Route.fromUrl url)
(Redirect (Session.fromViewer navKey maybeViewer))
toSession : Model -> Session
toSession page =
case page of
Redirect session ->
session
NotFound session ->
session
About session ->
session
Home home ->
Home.toSession home
Account settings ->
Settings.toSession settings
Login login ->
Login.toSession login
Signup signup ->
Signup.toSession signup
Todo todo ->
Todo.toSession todo
Editor _ editor ->
Editor.toSession editor
changeRouteTo : Maybe Route -> Model -> ( Model, Cmd Msg )
changeRouteTo maybeRoute model =
let
session =
toSession model
in
case maybeRoute of
Nothing ->
( NotFound session, Cmd.none )
Just Route.Root ->
( model, Route.replaceUrl (Session.navKey session) Route.Home )
Just Route.Logout ->
( model, Api.logout )
Just Route.NewTodo ->
Editor.initNew session
|> updateWith (Editor Nothing) GotEditorMsg model
Just (Route.EditTodo slug) ->
Editor.initEdit session slug
|> updateWith (Editor (Just slug)) GotEditorMsg model
Just Route.Account ->
Account.init session
|> updateWith Account GotAccountMsg model
Just Route.Home ->
Home.init session
|> updateWith Home GotHomeMsg model
Just Route.Login ->
Login.init session
|> updateWith Login GotLoginMsg model
Just Route.Signup ->
Signup.init session
|> updateWith Signup GotSignupMsg model
Just (Route.Profile username) ->
Profile.init session username
|> updateWith (Profile username) GotProfileMsg model
Just (Route.Todo slug) ->
Todo.init session slug
|> updateWith Todo GotArticleMsg model
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case ( msg, model ) of
( _, _ ) ->
( model, Cmd.none )
updateWith : (subModel -> Model) -> (subMsg -> Msg) -> Model -> ( subModel, Cmd subMsg ) -> ( Model, Cmd Msg )
updateWith toModel toMsg model ( subModel, subCmd ) =
( toModel subModel
, Cmd.map toMsg subCmd
)
-- view takes current application state and is responsible
-- for rendering the document based on it
view : Model -> Browser.Document Msg
view model =
{ title = "Elm App Test"
, body =
[ layout [ height fill ]
(column [ width fill ]
[ getNavRow model
, getMainContent model
]
)
]
}
-- subscriptions takes the current state and
-- maybe returns the subscriptions based on it (?)
-- might be used to subscribe to different events based on
-- the url, etc.
subscriptions : Model -> Sub Msg
subscriptions _ =
Sub.none
main : Program Value Model Msg
main =
Api.application Viewer.decoder
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
, onUrlChange = UrlChange -- these are actual types from Msg
, onUrlRequest = Request
}

View file

@ -1,57 +0,0 @@
module NavRow exposing (..)
import CommonElements exposing (..)
import Element exposing (..)
import Element.Region as Region
import Model exposing (..)
import Request exposing (..)
import Url
getNavRow : Model -> Element Msg
getNavRow model =
row
[ Region.navigation
--, explain Debug.todo
, paddingXY 10 5
, spacing 10
, width fill
]
[ namedLink "/" "TODOAPP"
, getDebugInfo model
, getCurrentUser model
]
-- temp function to get current page url
-- and links in case shit breaks
getDebugInfo : Model -> Element Msg
getDebugInfo model =
row
[ centerX ]
[ text "Current URL: "
, bolded (Url.toString model.url)
, column []
[ namedLink "/" "root"
, namedLink "/login" "login"
, namedLink "/signup" "signup"
, namedLink "/account" "account"
, namedLink "/about" "about"
]
]
getCurrentUser : Model -> Element Msg
getCurrentUser model =
el []
(case model.user of
Just user ->
namedLink "/account" user.email
_ ->
namedLink "/login" "Log In"
)

View file

@ -1,66 +0,0 @@
module PageState exposing (getMainContent)
import About as AboutPage
import Account as AccountPage
import Element exposing (..)
import Home as HomePage
import Login as LoginPage
import Model exposing (..)
import Signup as SignupPage
import Url
import Url.Parser as Parser exposing (..)
type Route
= About
| Account
| Home
| Login
| Signup
| NotFound
getMainContent : Model -> Element Msg
getMainContent model =
el []
(case consume (toRoute model.url) of
About ->
AboutPage.getPage model
Account ->
AccountPage.getPage model
Home ->
HomePage.getPage model
Login ->
LoginPage.getPage model
Signup ->
SignupPage.getPage model
_ ->
text "Page not found."
)
routeParser : Parser (Route -> a) a
routeParser =
oneOf
[ Parser.map Home top
, Parser.map Account (s "account")
, Parser.map About (s "about")
, Parser.map Login (s "login")
, Parser.map Signup (s "signup")
]
toRoute : Url.Url -> Maybe Route
toRoute url =
{ url | path = Maybe.withDefault "" url.fragment, fragment = Nothing }
|> Parser.parse routeParser
consume : Maybe Route -> Route
consume route =
Maybe.withDefault NotFound route

View file

@ -1,98 +0,0 @@
module Route exposing (Route(..), fromUrl, href, replaceUrl)
import Browser.Navigation as Nav
import Html exposing (Attribute)
import Html.Attributes as Attr
import Todo.UUID as UUID
import Url exposing (Url)
import Url.Parser as Parser exposing ((</>), Parser, oneOf, s, string)
import Username exposing (Username)
-- ROUTING
type Route
= Home
| Login
| Logout
| Signup
| Account
| Todo UUID.UUID
| NewTodo
| EditTodo UUID.UUID
parser : Parser (Route -> a) a
parser =
oneOf
[ Parser.map Home Parser.top
, Parser.map Login (s "login")
, Parser.map Logout (s "logout")
, Parser.map Account (s "account")
, Parser.map Signup (s "signup")
, Parser.map Todo (s "article" </> UUID.urlParser)
, Parser.map NewTodo (s "editor")
, Parser.map EditTodo (s "editor" </> UUID.urlParser)
]
-- PUBLIC HELPERS
href : Route -> Attribute msg
href targetRoute =
Attr.href (routeToString targetRoute)
replaceUrl : Nav.Key -> Route -> Cmd msg
replaceUrl key route =
Nav.replaceUrl key (routeToString route)
fromUrl : Url -> Maybe Route
fromUrl url =
-- The RealWorld spec treats the fragment like a path.
-- This makes it *literally* the path, so we can proceed
-- with parsing as if it had been a normal path all along.
{ url | path = Maybe.withDefault "" url.fragment, fragment = Nothing }
|> Parser.parse parser
-- INTERNAL
routeToString : Route -> String
routeToString page =
"#/" ++ String.join "/" (routeToPieces page)
routeToPieces : Route -> List String
routeToPieces page =
case page of
Home ->
[]
Login ->
[ "login" ]
Logout ->
[ "logout" ]
Signup ->
[ "Signup" ]
Account ->
[ "account" ]
Todo uuid ->
[ "article", UUID.toString uuid ]
NewTodo ->
[ "editor" ]
EditTodo uuid ->
[ "editor", UUID.toString uuid ]

View file

@ -1,75 +0,0 @@
module Session exposing (Session, changes, cred, fromViewer, navKey, viewer)
import Api exposing (Cred)
import Avatar exposing (Avatar)
import Browser.Navigation as Nav
import Json.Decode as Decode exposing (Decoder)
import Json.Encode as Encode exposing (Value)
import Profile exposing (Profile)
import Time
import Viewer exposing (Viewer)
-- TYPES
type Session
= LoggedIn Nav.Key Viewer
| Guest Nav.Key
-- INFO
viewer : Session -> Maybe Viewer
viewer session =
case session of
LoggedIn _ val ->
Just val
Guest _ ->
Nothing
cred : Session -> Maybe Cred
cred session =
case session of
LoggedIn _ val ->
Just (Viewer.cred val)
Guest _ ->
Nothing
navKey : Session -> Nav.Key
navKey session =
case session of
LoggedIn key _ ->
key
Guest key ->
key
-- CHANGES
changes : (Session -> msg) -> Nav.Key -> Sub msg
changes toMsg key =
Api.viewerChanges (\maybeViewer -> toMsg (fromViewer key maybeViewer)) Viewer.decoder
fromViewer : Nav.Key -> Maybe Viewer -> Session
fromViewer key maybeViewer =
-- It's stored in localStorage as a JSON String;
-- first decode the Value as a String, then
-- decode that String as JSON.
case maybeViewer of
Just viewerVal ->
LoggedIn key viewerVal
Nothing ->
Guest key

View file

@ -1,53 +0,0 @@
module Signup exposing (..)
import CommonElements exposing (..)
import Element exposing (..)
import Element.Input as Input
import Model exposing (..)
getPage : Model -> Element Msg
getPage model =
el [] (signupBox model)
signupBox : Model -> Element Msg
signupBox model =
column []
[ text "Sign up"
, Input.email
[ Input.focusedOnLoad
]
{ onChange = CredentialsChange "loginUsername"
, text = Maybe.withDefault "" model.storage.loginUsername
, placeholder = Just (Input.placeholder [] (text "email"))
, label = Input.labelHidden "email"
}
, Input.newPassword []
{ onChange = CredentialsChange "loginPassword"
, text = Maybe.withDefault "" model.storage.loginPassword
, placeholder = Just (Input.placeholder [] (text "password"))
, label = Input.labelHidden "password"
, show = False
}
, Input.newPassword []
{ onChange = CredentialsChange "confirmPassword"
, text = Maybe.withDefault "" model.storage.signupConfirmPassword
, placeholder = Just (Input.placeholder [] (text "confirm password"))
, label = Input.labelHidden "confirm password"
, show = False
}
, Input.button []
{ onPress = ensurePasswordMatch model
, label = text "Sign Up"
}
]
ensurePasswordMatch : Model -> Maybe Msg
ensurePasswordMatch model =
if model.storage.signupConfirmPassword == model.storage.loginPassword then
Just SubmitSignup
else
Just SignupPasswordMismatch

View file

@ -1,253 +0,0 @@
module Todo exposing (Full, Preview, Todo, author, body, favorite, favoriteButton, fetch, fromPreview, fullDecoder, mapAuthor, metadata, previewDecoder, unfavorite, unfavoriteButton, uuid)
{-| The interface to the Todo data structure.
This includes:
- The Todo type itself
- Ways to make HTTP requests to retrieve and modify Todos
- Ways to access information about an Todo
- Converting between various types
-}
import Api exposing (Cred)
import Api.Endpoint as Endpoint
import Author exposing (Author)
import Element exposing (..)
import Http
import Iso8601
import Json.Decode as Decode exposing (Decoder)
import Json.Encode as Encode
import Markdown
import Time
import Todo.Body as Body exposing (Body)
import Todo.Tag as Tag exposing (Tag)
import Todo.UUID exposing (UUID)
import Username as Username exposing (Username)
import Viewer exposing (Viewer)
-- TYPES
{-| An Todo, optionally with an Todo body.
To see the difference between { extraInfo : a } and { extraInfo : Maybe Body },
consider the difference between the "view individual Todo" page (which
renders one Todo, including its body) and the "Todo feed" -
which displays multiple Todos, but without bodies.
This definition for `Todo` means we can write:
viewTodo : Todo Full -> Html msg
viewFeed : List (Todo Preview) -> Html msg
This indicates that `viewTodo` requires an Todo _with a `body` present_,
wereas `viewFeed` accepts Todos with no bodies. (We could also have written
it as `List (Todo a)` to specify that feeds can accept either Todos that
have `body` present or not. Either work, given that feeds do not attempt to
read the `body` field from Todos.)
This is an important distinction, because in Request.Todo, the `feed`
function produces `List (Todo Preview)` because the API does not return bodies.
Those Todos are useful to the feed, but not to the individual Todo view.
-}
type Todo a
= Todo Internals a
{-| Metadata about the Todo - its title, description, and so on.
Importantly, this module's public API exposes a way to read this metadata, but
not to alter it. This is read-only information!
If we find ourselves using any particular piece of metadata often,
for example `title`, we could expose a convenience function like this:
Todo.title : Todo a -> String
If you like, it's totally reasonable to expose a function like that for every one
of these fields!
(Okay, to be completely honest, exposing one function per field is how I prefer
to do it, and that's how I originally wrote this module. However, I'm aware that
this code base has become a common reference point for beginners, and I think it
is _extremely important_ that slapping some "getters and setters" on a record
does not become a habit for anyone who is getting started with Elm. The whole
point of making the Todo type opaque is to create guarantees through
_selectively choosing boundaries_ around it. If you aren't selective about
where those boundaries are, and instead expose a "getter and setter" for every
field in the record, the result is an API with no more guarantees than if you'd
exposed the entire record directly! It is so important to me that beginners not
fall into the terrible "getters and setters" trap that I've exposed this
Metadata record instead of exposing a single function for each of its fields,
as I did originally. This record is not a bad way to do it, by any means,
but if this seems at odds with <https://youtu.be/x1FU3e0sT1I> - now you know why!
)
-}
type alias Metadata =
{ description : String
, title : String
, tags : List String
, createdAt : Time.Posix
, favorited : Bool
, favoritesCount : Int
}
type alias Internals =
{ uuid : UUID
, author : Author
, metadata : Metadata
}
type Preview
= Preview
type Full
= Full Body
-- INFO
author : Todo a -> Author
author (Todo internals _) =
internals.author
metadata : Todo a -> Metadata
metadata (Todo internals _) =
internals.metadata
uuid : Todo a -> UUID
uuid (Todo internals _) =
internals.uuid
body : Todo Full -> Body
body (Todo _ (Full extraInfo)) =
extraInfo
-- TRANSFORM
{-| This is the only way you can transform an existing Todo:
you can change its author (e.g. to follow or unfollow them).
All other Todo data necessarily comes from the server!
We can tell this for sure by looking at the types of the exposed functions
in this module.
-}
mapAuthor : (Author -> Author) -> Todo a -> Todo a
mapAuthor transform (Todo info extras) =
Todo { info | author = transform info.author } extras
fromPreview : Body -> Todo Preview -> Todo Full
fromPreview newBody (Todo info Preview) =
Todo info (Full newBody)
-- SERIALIZATION
previewDecoder : Maybe Cred -> Decoder (Todo Preview)
previewDecoder maybeCred =
Decode.succeed Todo
|> custom (internalsDecoder maybeCred)
|> hardcoded Preview
fullDecoder : Maybe Cred -> Decoder (Todo Full)
fullDecoder maybeCred =
Decode.succeed Todo
|> custom (internalsDecoder maybeCred)
|> required "body" (Decode.map Full Body.decoder)
internalsDecoder : Maybe Cred -> Decoder Internals
internalsDecoder maybeCred =
Decode.succeed Internals
|> required "uuid" UUID.decoder
|> required "author" (Author.decoder maybeCred)
|> custom metadataDecoder
metadataDecoder : Decoder Metadata
metadataDecoder =
Decode.succeed Metadata
|> required "description" (Decode.map (Maybe.withDefault "") (Decode.nullable Decode.string))
|> required "title" Decode.string
|> required "tagList" (Decode.list Decode.string)
|> required "createdAt" Iso8601.decoder
|> required "favorited" Decode.bool
|> required "favoritesCount" Decode.int
-- SINGLE
fetch : Maybe Cred -> UUID -> Http.Request (Todo Full)
fetch maybeCred uuid =
Decode.field "Todo" (fullDecoder maybeCred)
|> Api.get (Endpoint.Todo uuid) maybeCred
-- FAVORITE
favorite : UUID -> Cred -> Http.Request (Todo Preview)
favorite uuid cred =
Api.post (Endpoint.favorite uuid) (Just cred) Http.emptyBody (faveDecoder cred)
unfavorite : UUID -> Cred -> Http.Request (Todo Preview)
unfavorite uuid cred =
Api.delete (Endpoint.favorite uuid) cred Http.emptyBody (faveDecoder cred)
faveDecoder : Cred -> Decoder (Todo Preview)
faveDecoder cred =
Decode.field "Todo" (previewDecoder (Just cred))
{-| This is a "build your own element" API.
You pass it some configuration, followed by a `List (Attribute msg)` and a
`List (Html msg)`, just like any standard Html element.
-}
favoriteButton :
Cred
-> msg
-> List (Attribute msg)
-> List (Element msg)
-> Element msg
favoriteButton _ msg attrs kids =
toggleFavoriteButton "btn btn-sm btn-outline-primary" msg attrs kids
unfavoriteButton :
Cred
-> msg
-> List (Attribute msg)
-> List (Element msg)
-> Element msg
unfavoriteButton _ msg attrs kids =
toggleFavoriteButton "btn btn-sm btn-primary" msg attrs kids
toggleFavoriteButton :
String
-> msg
-> List (Attribute msg)
-> List (Element msg)
-> Element msg
toggleFavoriteButton classStr msg attrs kids =
Html.button
(class classStr :: onClickStopPropagation msg :: attrs)
(i [ class "ion-heart" ] [] :: kids)
onClickStopPropagation : msg -> Attribute msg
onClickStopPropagation msg =
stopPropagationOn "click"
(Decode.succeed ( msg, True ))

View file

@ -1,35 +0,0 @@
module Todo.UUID exposing (UUID, decoder, toString, urlParser)
import Json.Decode as Decode exposing (Decoder)
import Url.Parser exposing (Parser)
-- TYPES
type UUID
= UUID String
-- CREATE
urlParser : Parser (UUID -> a) a
urlParser =
Url.Parser.custom "UUID" (\str -> Just (UUID str))
decoder : Decoder UUID
decoder =
Decode.map UUID Decode.string
-- TRANSFORM
toString : UUID -> String
toString (UUID str) =
str

View file

@ -1,47 +0,0 @@
module Username exposing (Username, decoder, encode, toHtml, toString, urlParser)
import Element exposing (..)
import Json.Decode as Decode exposing (Decoder)
import Json.Encode as Encode exposing (Value)
import Url.Parser
-- TYPES
type Username
= Username String
-- CREATE
decoder : Decoder Username
decoder =
Decode.map Username Decode.string
-- TRANSFORM
encode : Username -> Value
encode (Username username) =
Encode.string username
toString : Username -> String
toString (Username username) =
username
urlParser : Url.Parser.Parser (Username -> a) a
urlParser =
Url.Parser.custom "USERNAME" (\str -> Just (Username str))
toHtml : Username -> Element msg
toHtml (Username username) =
text username

View file

@ -1,66 +0,0 @@
module Viewer exposing (Viewer, avatar, cred, decoder, minPasswordChars, store, username)
{-| The logged-in user currently viewing this page. It stores enough data to
be able to render the menu bar (username and avatar), along with Cred so it's
impossible to have a Viewer if you aren't logged in.
-}
import Api exposing (Cred)
import Avatar exposing (Avatar)
import Email exposing (Email)
import Json.Decode as Decode exposing (Decoder)
import Json.Decode.Pipeline exposing (custom, required)
import Json.Encode as Encode exposing (Value)
import Profile exposing (Profile)
import Username exposing (Username)
-- TYPES
type Viewer
= Viewer Avatar Cred
-- INFO
cred : Viewer -> Cred
cred (Viewer _ val) =
val
username : Viewer -> Username
username (Viewer _ val) =
Api.username val
avatar : Viewer -> Avatar
avatar (Viewer val _) =
val
{-| Passwords must be at least this many characters long!
-}
minPasswordChars : Int
minPasswordChars =
6
-- SERIALIZATION
decoder : Decoder (Cred -> Viewer)
decoder =
Decode.succeed Viewer
|> custom (Decode.field "image" Avatar.decoder)
store : Viewer -> Cmd msg
store (Viewer avatarVal credVal) =
Api.storeCredWith
credVal
avatarVal

View file

@ -4,10 +4,6 @@
"description": "todo list app (because it hasnt been done before)", "description": "todo list app (because it hasnt been done before)",
"main": "src/index.js", "main": "src/index.js",
"scripts": { "scripts": {
"who": "pwd",
"build": "cd frontend; elm make src/Main.elm --output=../static/elm.js",
"debug": "cd frontend; elm make src/Main.elm --debug --output=../static/elm.js",
"prod": "cd frontend; elm make src/Main.elm --optimize --output=../static/elm.js",
"start": "node src/index.js" "start": "node src/index.js"
}, },
"repository": { "repository": {
@ -18,7 +14,6 @@
"license": "SEE LICENSE IN LICENSE", "license": "SEE LICENSE IN LICENSE",
"dependencies": { "dependencies": {
"cookie-parser": "^1.4.5", "cookie-parser": "^1.4.5",
"cors": "^2.8.5",
"express": "^4.17.1", "express": "^4.17.1",
"pg": "^8.6.0", "pg": "^8.6.0",
"sequelize": "^6.6.2" "sequelize": "^6.6.2"

View file

@ -1,6 +1,5 @@
dependencies: dependencies:
cookie-parser: 1.4.5 cookie-parser: 1.4.5
cors: 2.8.5
express: 4.17.1 express: 4.17.1
pg: 8.6.0 pg: 8.6.0
sequelize: 6.6.2_pg@8.6.0 sequelize: 6.6.2_pg@8.6.0
@ -89,15 +88,6 @@ packages:
node: '>= 0.6' node: '>= 0.6'
resolution: resolution:
integrity: sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg== integrity: sha512-+Hp8fLp57wnUSt0tY0tHEXh4voZRDnoIrZPqlo3DPiI4y9lwg/jqx+1Om94/W6ZaPDOUbnjOt/99w66zk+l1Xg==
/cors/2.8.5:
dependencies:
object-assign: 4.1.1
vary: 1.1.2
dev: false
engines:
node: '>= 0.10'
resolution:
integrity: sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==
/debug/2.6.9: /debug/2.6.9:
dependencies: dependencies:
ms: 2.0.0 ms: 2.0.0
@ -343,12 +333,6 @@ packages:
node: '>= 0.6' node: '>= 0.6'
resolution: resolution:
integrity: sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw== integrity: sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==
/object-assign/4.1.1:
dev: false
engines:
node: '>=0.10.0'
resolution:
integrity: sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=
/on-finished/2.3.0: /on-finished/2.3.0:
dependencies: dependencies:
ee-first: 1.1.1 ee-first: 1.1.1
@ -695,7 +679,6 @@ packages:
integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A== integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
specifiers: specifiers:
cookie-parser: ^1.4.5 cookie-parser: ^1.4.5
cors: ^2.8.5
express: ^4.17.1 express: ^4.17.1
pg: ^8.6.0 pg: ^8.6.0
sequelize: ^6.6.2 sequelize: ^6.6.2

39
src/index.js Normal file
View file

@ -0,0 +1,39 @@
const express = require('express');
const cookieParser = require('cookie-parser');
const Config = require('./config.js');
const UserInterface = require('./user.js');
let app = express();
app.use(cookieParser());
// force https
app.use((req, res, next) => {
if (Config.config.https) {
if (req.headers['x-forwarded-proto'] !== 'https') {
return res.redirect(`https://${req.headers.host}${req.url}`);
}
}
return next();
});
if (!Config.config.secret) {
console.error("No password secret found. please set `secret` in config.json");
process.exit();
}
else if(Config.config.https && Config.config.secret == "TEST_SECRET") {
console.error("please do not use the testing secret in production.");
process.exit();
}
app.use("/user", UserInterface.router);
// serve static files last
// app.use(express.static("./static"));
// DISABLED: no longer needs to serve static files
// due to frontend being employed in elm
app.listen(Config.config.port || 8080, () => {
console.log(`listening on port ${Config.config.port || 8080}`);
});

View file

@ -13,19 +13,16 @@ user_cache = {};
email_cache = {}; email_cache = {};
async function get_user_details(id) { async function get_user_details(id) {
if (!id) {
return undefined;
}
console.log(`search for user with id ${id}`); console.log(`search for user with id ${id}`);
if (!user_cache[id]) { if (!user_cache[id]) {
let user = await Database.schemas.user.findOne({where: {id: id}}); let user = await Database.schemas.user.findOne({ where: { id: id } });
if (!user) { if (!user) {
return undefined; return undefined;
} }
user_cache[user.id] = { user_cache[user.id] = {
id: user.id, id: user.id,
email: user.email, email: user.email,
password_hash: user.password_hash, password_hash: user.password_hash
}; };
email_cache[user.email] = user.id; email_cache[user.email] = user.id;
} }
@ -34,19 +31,16 @@ async function get_user_details(id) {
} }
async function get_user_details_by_email(email) { async function get_user_details_by_email(email) {
if (!email) {
return undefined;
}
console.log(`search for user with email ${email}}`); console.log(`search for user with email ${email}}`);
if (!email_cache[email] || !user_cache[email_cache[email]]) { if (!email_cache[email] || !user_cache[email_cache[email]]) {
let user = await Database.schemas.user.findOne({where: {email: email}}); let user = await Database.schemas.user.findOne({ where: { email: email } });
if (!user) { if (!user) {
return undefined; return undefined;
} }
user_cache[user.id] = { user_cache[user.id] = {
id: user.id, id: user.id,
email: user.email, email: user.email,
password_hash: user.password_hash, password_hash: user.password_hash
}; };
email_cache[user.email] = user.id; email_cache[user.email] = user.id;
} }
@ -54,10 +48,10 @@ async function get_user_details_by_email(email) {
return user_cache[email_cache[email]]; return user_cache[email_cache[email]];
} }
router.get('/byEmail/:email', async (req, res) => { router.get("/byEmail/:email", async (req, res) => {
if (!req.params?.email) { if (!req.params?.email) {
res.status(400).json({ res.status(400).json({
error: 'email is a required parameter', error: "email is a required parameter"
}); });
} }
let user = get_user_details_by_email(req.params.email); let user = get_user_details_by_email(req.params.email);
@ -65,33 +59,30 @@ router.get('/byEmail/:email', async (req, res) => {
if (user != null) { if (user != null) {
res.json({ res.json({
id: user.id, id: user.id,
email: user.email, email: user.email
}); });
} else { }
else {
res.sendStatus(404); res.sendStatus(404);
} }
}); });
function hash(secret, password) { function hash(secret, password) {
let pw_hash = crypto.pbkdf2Sync( let pw_hash = crypto.pbkdf2Sync(password,
password,
secret, secret,
Config.config.key?.iterations || 1000, Config.config.key?.iterations || 1000,
Config.config.key?.length || 64, Config.config.key?.length || 64,
'sha512' "sha512");
);
return pw_hash.toString('base64'); return pw_hash.toString('base64');
} }
function verify(secret, password, hash) { function verify(secret, password, hash) {
let pw_hash = crypto.pbkdf2Sync( let pw_hash = crypto.pbkdf2Sync(password,
password,
secret, secret,
Config.config.key?.iterations || 1000, Config.config.key?.iterations || 1000,
Config.config.key?.length || 64, Config.config.key?.length || 64,
'sha512' "sha512");
);
return hash === pw_hash.toString('base64'); return hash === pw_hash.toString('base64');
} }
@ -112,7 +103,8 @@ function get_session_token(id, token) {
function verify_session_token(id, hash, token) { function verify_session_token(id, hash, token) {
if (session_entropy[id]) { if (session_entropy[id]) {
return verify(session_entropy[id], hash, token); return verify(session_entropy[id], hash, token);
} else { }
else {
return false; return false;
} }
} }
@ -120,116 +112,106 @@ function verify_session_token(id, hash, token) {
async function enforce_session_login(req, res, next) { async function enforce_session_login(req, res, next) {
let userid = req.cookies?.userid; let userid = req.cookies?.userid;
let session_token = req.cookies?._session; let session_token = req.cookies?._session;
console.log('a', userid, session_token); console.log("a", userid, session_token);
if (!userid || !session_token) { if (!userid || !session_token) {
return res.sendStatus(401); res.sendStatus(401);
} }
let user = await get_user_details(userid); let user = await get_user_details(userid);
if (!user) { if (!user) {
return res.sendStatus(401); res.sendStatus(401);
} }
let verified_session = verify_session_token( let verified_session = verify_session_token(userid, user.password_hash, session_token);
userid,
user.password_hash,
session_token
);
if (!verified_session) { if (!verified_session) {
return res.sendStatus(401); res.sendStatus(401);
} }
return next(); return next();
} }
router.post('/new', async (req, res) => { router.post("/new", async (req, res) => {
if (!req.body?.email || !req.body?.password) { if (!req.body?.email || !req.body?.password) {
return res.status(400).json({ res.status(400).json({
error: 'must have email and password fields', error: "must have email and password fields"
}); });
} }
let user = await get_user_details_by_email(req.body.email); let user = await get_user_details_by_email(req.body.email);
console.log(user); console.log(user);
if (user != null) { if (user != null) {
return res.status(403).json({ res.status(403).json({
error: `email ${req.body.email} is already in use.`, error: `email ${req.body.email} is already in use.`
}); });
} else { }
else {
let user = await Database.schemas.user.create({ let user = await Database.schemas.user.create({
email: String(req.body.email), email: String(req.body.email),
password_hash: hash_password(req.body.password), password_hash: hash_password(req.body.password)
}); });
return res.json({ res.json({
id: user.id, id: user.id,
email: user.email, email: user.email
}); });
} }
}); });
router.post('/login', async (req, res) => { router.post("/login", async (req, res) => {
if (!req.body?.email || !req.body?.password) { if (!req.body?.email || !req.body?.password) {
return res.status(400).json({ res.status(400).json({
error: 'must have email and password fields', error: "must have email and password fields"
}); });
} }
let user = await get_user_details_by_email(req.body.email); let user = await get_user_details_by_email(req.body.email);
if (!user) { if (!user) {
return res.status(401).json({ res.status(401).json({
error: 'incorrect email or password', error: "incorrect email or password"
}); });
} }
let verified = verify_password(req.body.password, user.password_hash); let verified = verify_password(req.body.password, user.password_hash);
if (!verified) { if (!verified) {
return res.status(401).json({ res.status(401).json({
error: 'incorrect email or password', error: "incorrect email or password"
}); });
} }
res.cookie('userid', user.id, { res.cookie("userid", user.id);
httpOnly: true, res.cookie("_session", get_session_token(user.id, user.password_hash));
secure: true, res.sendStatus(204);
});
res.cookie('_session', get_session_token(user.id, user.password_hash), {
httpOnly: true,
secure: true,
});
return res.sendStatus(204);
}); });
router.get('/:id([a-f0-9-]+)', async (req, res) => { router.get("/:id(([a-f0-9\-])+)", async (req, res) => {
console.log(req.params);
if (!req.params?.id) { if (!req.params?.id) {
return res.status(400).json({ res.status(400).json({
error: 'must have id parameter', error: "must have id parameter"
}); });
} }
let id = req.params?.id; let user = await get_user_details(req.body.id);
console.log(id);
let user = await get_user_details(id);
console.log(user); console.log(user);
if (user != null) { if (user != null) {
return res.json({ res.json({
id: user.id, id: user.id,
email: user.email, email: user.email
}); });
} else { }
return res.sendStatus(404); else {
res.sendStatus(404);
} }
}); });
router.use('/authorized', enforce_session_login); router.use("/authorized", enforce_session_login);
router.get('/authorized', async (req, res) => { router.get("/authorized", async (req, res) => {
let userid = req.cookies?.userid; let userid = req.cookies?.userid;
let user = await get_user_details(userid); let user = get_user_details(userid);
return res.json({ res.json({
authorized: true, authorized: true,
user: { user: {
id: user.id, id: user.id,
email: user.email, email: user.email
}, }
}); });
}); });
module.exports = { module.exports = {
router: router, router: router,
enforce_session_login: enforce_session_login, enforce_session_login: enforce_session_login
}; };

8
static/index.html Normal file
View file

@ -0,0 +1,8 @@
<html>
<head>
</head>
<body>
</body>
</html>

View file

@ -1,62 +0,0 @@
const http = require('http');
const https = require('https');
const cors = require('cors');
const express = require('express');
const cookieParser = require('cookie-parser');
const Config = require('./config.js');
const UserInterface = require('./user.js');
let credentials = {};
if (Config.config.https) {
if (
fs.existsSync(Config.config.cert) &&
fs.existsSync(Config.config.cert_key)
) {
credentials.key = fs.readFileSync(Config.config.cert_key);
credentials.cert = fs.readFileSync(Config.config.cert);
}
}
let app = express();
app.use(cors());
app.use(cookieParser());
// force https
app.use((req, res, next) => {
if (Config.config.https) {
if (req.headers['x-forwarded-proto'] !== 'https') {
return res.redirect(`https://${req.headers.host}${req.url}`);
}
}
return next();
});
if (!Config.config.secret) {
console.error('No password secret found. please set `secret` in config.json');
process.exit();
} else if (Config.config.https && Config.config.secret == 'TEST_SECRET') {
console.error('please do not use the testing secret in production.');
process.exit();
}
app.use('/api/user', UserInterface.router);
// serve static files last
app.use(express.static('./static'));
// DISABLED: no longer needs to serve static files
// due to frontend being employed in elm
if (Config.config.https) {
var server = https.createServer(credentials, app);
server.listen(Config.config.port || 8080);
} else {
var server = http.createServer(app);
server.listen(Config.config.port || 8080);
}
console.log(
`listening on port ${Config.config.port || 8080}` +
` with https ${Config.config.https ? 'enabled' : 'disabled'}`
);