diff --git a/.gitignore b/.gitignore index 6f92a01..4d318a8 100644 --- a/.gitignore +++ b/.gitignore @@ -116,3 +116,8 @@ dist .yarn/install-state.gz .pnp.* +static/ + +.idea + +elm-stuff/ diff --git a/elm-deprecated/elm.json b/elm-deprecated/elm.json new file mode 100644 index 0000000..01ba003 --- /dev/null +++ b/elm-deprecated/elm.json @@ -0,0 +1,29 @@ +{ + "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": {} + } +} diff --git a/elm-deprecated/index.html b/elm-deprecated/index.html new file mode 100644 index 0000000..6c718e8 --- /dev/null +++ b/elm-deprecated/index.html @@ -0,0 +1,14025 @@ + + + + + Main + + + + + +

+
+
+
+
+
\ No newline at end of file
diff --git a/elm-deprecated/src/About.elm b/elm-deprecated/src/About.elm
new file mode 100644
index 0000000..b65252d
--- /dev/null
+++ b/elm-deprecated/src/About.elm
@@ -0,0 +1,9 @@
+module About exposing (..)
+
+import Element exposing (..)
+import Model exposing (..)
+
+
+getPage : Model -> Element Msg
+getPage _ =
+    text "About page."
diff --git a/elm-deprecated/src/Account.elm b/elm-deprecated/src/Account.elm
new file mode 100644
index 0000000..35a5d69
--- /dev/null
+++ b/elm-deprecated/src/Account.elm
@@ -0,0 +1,9 @@
+module Account exposing (..)
+
+import Element exposing (..)
+import Model exposing (..)
+
+
+getPage : Model -> Element Msg
+getPage _ =
+    text "Account page."
diff --git a/elm-deprecated/src/Api.elm b/elm-deprecated/src/Api.elm
new file mode 100644
index 0000000..acdbec6
--- /dev/null
+++ b/elm-deprecated/src/Api.elm
@@ -0,0 +1,290 @@
+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"
diff --git a/elm-deprecated/src/Api/Endpoint.elm b/elm-deprecated/src/Api/Endpoint.elm
new file mode 100644
index 0000000..f4a4f05
--- /dev/null
+++ b/elm-deprecated/src/Api/Endpoint.elm
@@ -0,0 +1,99 @@
+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" ] []
diff --git a/elm-deprecated/src/Asset.elm b/elm-deprecated/src/Asset.elm
new file mode 100644
index 0000000..daa023b
--- /dev/null
+++ b/elm-deprecated/src/Asset.elm
@@ -0,0 +1,46 @@
+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
diff --git a/elm-deprecated/src/Avatar.elm b/elm-deprecated/src/Avatar.elm
new file mode 100644
index 0000000..7ecafb3
--- /dev/null
+++ b/elm-deprecated/src/Avatar.elm
@@ -0,0 +1,56 @@
+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
diff --git a/elm-deprecated/src/CommonElements.elm b/elm-deprecated/src/CommonElements.elm
new file mode 100644
index 0000000..2a9bdd1
--- /dev/null
+++ b/elm-deprecated/src/CommonElements.elm
@@ -0,0 +1,31 @@
+-- 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
diff --git a/elm-deprecated/src/Email.elm b/elm-deprecated/src/Email.elm
new file mode 100644
index 0000000..0181842
--- /dev/null
+++ b/elm-deprecated/src/Email.elm
@@ -0,0 +1,38 @@
+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
diff --git a/elm-deprecated/src/Home.elm b/elm-deprecated/src/Home.elm
new file mode 100644
index 0000000..462b0a2
--- /dev/null
+++ b/elm-deprecated/src/Home.elm
@@ -0,0 +1,9 @@
+module Home exposing (..)
+
+import Element exposing (..)
+import Model exposing (..)
+
+
+getPage : Model -> Element Msg
+getPage _ =
+    text "Home page."
diff --git a/elm-deprecated/src/Login.elm b/elm-deprecated/src/Login.elm
new file mode 100644
index 0000000..9aa8067
--- /dev/null
+++ b/elm-deprecated/src/Login.elm
@@ -0,0 +1,40 @@
+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"
+            }
+        ]
diff --git a/elm-deprecated/src/Main.elm b/elm-deprecated/src/Main.elm
new file mode 100644
index 0000000..c1166ca
--- /dev/null
+++ b/elm-deprecated/src/Main.elm
@@ -0,0 +1,178 @@
+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
+        }
diff --git a/elm-deprecated/src/NavRow.elm b/elm-deprecated/src/NavRow.elm
new file mode 100644
index 0000000..9edcd2d
--- /dev/null
+++ b/elm-deprecated/src/NavRow.elm
@@ -0,0 +1,57 @@
+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"
+        )
diff --git a/elm-deprecated/src/PageState.elm b/elm-deprecated/src/PageState.elm
new file mode 100644
index 0000000..e2982fd
--- /dev/null
+++ b/elm-deprecated/src/PageState.elm
@@ -0,0 +1,66 @@
+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
diff --git a/elm-deprecated/src/Route.elm b/elm-deprecated/src/Route.elm
new file mode 100644
index 0000000..484d8a9
--- /dev/null
+++ b/elm-deprecated/src/Route.elm
@@ -0,0 +1,98 @@
+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 ]
diff --git a/elm-deprecated/src/Session.elm b/elm-deprecated/src/Session.elm
new file mode 100644
index 0000000..2a8e994
--- /dev/null
+++ b/elm-deprecated/src/Session.elm
@@ -0,0 +1,75 @@
+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
diff --git a/elm-deprecated/src/Signup.elm b/elm-deprecated/src/Signup.elm
new file mode 100644
index 0000000..64218be
--- /dev/null
+++ b/elm-deprecated/src/Signup.elm
@@ -0,0 +1,53 @@
+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
diff --git a/elm-deprecated/src/Todo.elm b/elm-deprecated/src/Todo.elm
new file mode 100644
index 0000000..2e97eef
--- /dev/null
+++ b/elm-deprecated/src/Todo.elm
@@ -0,0 +1,253 @@
+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  - 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 ))
diff --git a/elm-deprecated/src/Todo/UUID.elm b/elm-deprecated/src/Todo/UUID.elm
new file mode 100644
index 0000000..bd200f1
--- /dev/null
+++ b/elm-deprecated/src/Todo/UUID.elm
@@ -0,0 +1,35 @@
+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
diff --git a/elm-deprecated/src/Username.elm b/elm-deprecated/src/Username.elm
new file mode 100644
index 0000000..781104d
--- /dev/null
+++ b/elm-deprecated/src/Username.elm
@@ -0,0 +1,47 @@
+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
diff --git a/elm-deprecated/src/Viewer.elm b/elm-deprecated/src/Viewer.elm
new file mode 100644
index 0000000..58ec005
--- /dev/null
+++ b/elm-deprecated/src/Viewer.elm
@@ -0,0 +1,66 @@
+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
diff --git a/src/index.js b/src/index.js
deleted file mode 100644
index af37430..0000000
--- a/src/index.js
+++ /dev/null
@@ -1,39 +0,0 @@
-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}`);
-});
\ No newline at end of file
diff --git a/static/index.html b/static/index.html
deleted file mode 100644
index ec57bad..0000000
--- a/static/index.html
+++ /dev/null
@@ -1,8 +0,0 @@
-
-  
-    
-  
-  
-
-  
-
\ No newline at end of file
diff --git a/LICENSE b/todo/LICENSE
similarity index 100%
rename from LICENSE
rename to todo/LICENSE
diff --git a/README.md b/todo/README.md
similarity index 100%
rename from README.md
rename to todo/README.md
diff --git a/config.json b/todo/config.json
similarity index 50%
rename from config.json
rename to todo/config.json
index 87abf0b..948073a 100644
--- a/config.json
+++ b/todo/config.json
@@ -3,5 +3,7 @@
   "https": false,
   "alter_db": true,
   "port": 8080,
-  "db_url": "postgres://postgres:@127.0.0.1/todo"
+  "db_url": "postgres://postgres:@127.0.0.1/todo",
+  "cert": "",
+  "cert_key": ""
 }
\ No newline at end of file
diff --git a/package.json b/todo/package.json
similarity index 62%
rename from package.json
rename to todo/package.json
index 2bcaeaf..fd35f15 100644
--- a/package.json
+++ b/todo/package.json
@@ -4,6 +4,10 @@
   "description": "todo list app (because it hasnt been done before)",
   "main": "src/index.js",
   "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"
   },
   "repository": {
@@ -14,8 +18,9 @@
   "license": "SEE LICENSE IN LICENSE",
   "dependencies": {
     "cookie-parser": "^1.4.5",
+    "cors": "^2.8.5",
     "express": "^4.17.1",
     "pg": "^8.6.0",
     "sequelize": "^6.6.2"
   }
-}
+}
\ No newline at end of file
diff --git a/pnpm-lock.yaml b/todo/pnpm-lock.yaml
similarity index 97%
rename from pnpm-lock.yaml
rename to todo/pnpm-lock.yaml
index 7711e79..1b8148f 100644
--- a/pnpm-lock.yaml
+++ b/todo/pnpm-lock.yaml
@@ -1,5 +1,6 @@
 dependencies:
   cookie-parser: 1.4.5
+  cors: 2.8.5
   express: 4.17.1
   pg: 8.6.0
   sequelize: 6.6.2_pg@8.6.0
@@ -88,6 +89,15 @@ packages:
       node: '>= 0.6'
     resolution:
       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:
     dependencies:
       ms: 2.0.0
@@ -333,6 +343,12 @@ packages:
       node: '>= 0.6'
     resolution:
       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:
     dependencies:
       ee-first: 1.1.1
@@ -679,6 +695,7 @@ packages:
       integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==
 specifiers:
   cookie-parser: ^1.4.5
+  cors: ^2.8.5
   express: ^4.17.1
   pg: ^8.6.0
   sequelize: ^6.6.2
diff --git a/src/config.js b/todo/src/config.js
similarity index 100%
rename from src/config.js
rename to todo/src/config.js
diff --git a/src/db_interface.js b/todo/src/db_interface.js
similarity index 100%
rename from src/db_interface.js
rename to todo/src/db_interface.js
diff --git a/todo/src/index.js b/todo/src/index.js
new file mode 100644
index 0000000..8c86ac0
--- /dev/null
+++ b/todo/src/index.js
@@ -0,0 +1,62 @@
+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'}`
+);
diff --git a/src/user.js b/todo/src/user.js
similarity index 59%
rename from src/user.js
rename to todo/src/user.js
index 2563baf..980cb77 100644
--- a/src/user.js
+++ b/todo/src/user.js
@@ -13,16 +13,19 @@ user_cache = {};
 email_cache = {};
 
 async function get_user_details(id) {
+  if (!id) {
+    return undefined;
+  }
   console.log(`search for user with id ${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) {
       return undefined;
     }
     user_cache[user.id] = {
       id: user.id,
       email: user.email,
-      password_hash: user.password_hash
+      password_hash: user.password_hash,
     };
     email_cache[user.email] = user.id;
   }
@@ -31,16 +34,19 @@ async function get_user_details(id) {
 }
 
 async function get_user_details_by_email(email) {
+  if (!email) {
+    return undefined;
+  }
   console.log(`search for user with email ${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) {
       return undefined;
     }
     user_cache[user.id] = {
       id: user.id,
       email: user.email,
-      password_hash: user.password_hash
+      password_hash: user.password_hash,
     };
     email_cache[user.email] = user.id;
   }
@@ -48,10 +54,10 @@ async function get_user_details_by_email(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) {
     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);
@@ -59,30 +65,33 @@ router.get("/byEmail/:email", async (req, res) => {
   if (user != null) {
     res.json({
       id: user.id,
-      email: user.email
+      email: user.email,
     });
-  }
-  else {
+  } else {
     res.sendStatus(404);
   }
 });
 
 function hash(secret, password) {
-  let pw_hash = crypto.pbkdf2Sync(password,
+  let pw_hash = crypto.pbkdf2Sync(
+    password,
     secret,
     Config.config.key?.iterations || 1000,
     Config.config.key?.length || 64,
-    "sha512");
+    'sha512'
+  );
 
   return pw_hash.toString('base64');
 }
 
 function verify(secret, password, hash) {
-  let pw_hash = crypto.pbkdf2Sync(password,
+  let pw_hash = crypto.pbkdf2Sync(
+    password,
     secret,
     Config.config.key?.iterations || 1000,
     Config.config.key?.length || 64,
-    "sha512");
+    'sha512'
+  );
 
   return hash === pw_hash.toString('base64');
 }
@@ -103,8 +112,7 @@ function get_session_token(id, token) {
 function verify_session_token(id, hash, token) {
   if (session_entropy[id]) {
     return verify(session_entropy[id], hash, token);
-  }
-  else {
+  } else {
     return false;
   }
 }
@@ -112,106 +120,116 @@ function verify_session_token(id, hash, token) {
 async function enforce_session_login(req, res, next) {
   let userid = req.cookies?.userid;
   let session_token = req.cookies?._session;
-  console.log("a", userid, session_token);
+  console.log('a', userid, session_token);
   if (!userid || !session_token) {
-    res.sendStatus(401);
+    return res.sendStatus(401);
   }
   let user = await get_user_details(userid);
   if (!user) {
-    res.sendStatus(401);
+    return res.sendStatus(401);
   }
-  let verified_session = verify_session_token(userid, user.password_hash, session_token);
+  let verified_session = verify_session_token(
+    userid,
+    user.password_hash,
+    session_token
+  );
   if (!verified_session) {
-    res.sendStatus(401);
+    return res.sendStatus(401);
   }
   return next();
 }
 
-router.post("/new", async (req, res) => {
+router.post('/new', async (req, res) => {
   if (!req.body?.email || !req.body?.password) {
-    res.status(400).json({
-      error: "must have email and password fields"
+    return res.status(400).json({
+      error: 'must have email and password fields',
     });
   }
   let user = await get_user_details_by_email(req.body.email);
   console.log(user);
   if (user != null) {
-    res.status(403).json({
-      error: `email ${req.body.email} is already in use.`
+    return res.status(403).json({
+      error: `email ${req.body.email} is already in use.`,
     });
-  }
-  else {
+  } else {
     let user = await Database.schemas.user.create({
       email: String(req.body.email),
-      password_hash: hash_password(req.body.password)
+      password_hash: hash_password(req.body.password),
     });
 
-    res.json({
+    return res.json({
       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) {
-    res.status(400).json({
-      error: "must have email and password fields"
+    return res.status(400).json({
+      error: 'must have email and password fields',
     });
   }
   let user = await get_user_details_by_email(req.body.email);
   if (!user) {
-    res.status(401).json({
-      error: "incorrect email or password"
+    return res.status(401).json({
+      error: 'incorrect email or password',
     });
   }
   let verified = verify_password(req.body.password, user.password_hash);
 
   if (!verified) {
-    res.status(401).json({
-      error: "incorrect email or password"
+    return res.status(401).json({
+      error: 'incorrect email or password',
     });
   }
 
-  res.cookie("userid", user.id);
-  res.cookie("_session", get_session_token(user.id, user.password_hash));
-  res.sendStatus(204);
+  res.cookie('userid', user.id, {
+    httpOnly: true,
+    secure: true,
+  });
+  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) {
-    res.status(400).json({
-      error: "must have id parameter"
+    return res.status(400).json({
+      error: 'must have id parameter',
     });
   }
-  let user = await get_user_details(req.body.id);
+  let id = req.params?.id;
+  console.log(id);
+  let user = await get_user_details(id);
   console.log(user);
   if (user != null) {
-    res.json({
+    return res.json({
       id: user.id,
-      email: user.email
+      email: user.email,
     });
-  }
-  else {
-    res.sendStatus(404);
+  } else {
+    return res.sendStatus(404);
   }
 });
 
-router.use("/authorized", enforce_session_login);
-router.get("/authorized", async (req, res) => {
+router.use('/authorized', enforce_session_login);
+router.get('/authorized', async (req, res) => {
   let userid = req.cookies?.userid;
-  let user = get_user_details(userid);
-  res.json({
+  let user = await get_user_details(userid);
+  return res.json({
     authorized: true,
     user: {
       id: user.id,
-      email: user.email
-    }
+      email: user.email,
+    },
   });
 });
 
-
 module.exports = {
   router: router,
-  enforce_session_login: enforce_session_login
-};
\ No newline at end of file
+  enforce_session_login: enforce_session_login,
+};