commit 70a315165f90a6350a11bddd53b81233d4114ca3 Author: Jane Petrovna Date: Mon Jun 14 16:58:17 2021 -0400 refactor diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..4d318a8 --- /dev/null +++ b/.gitignore @@ -0,0 +1,123 @@ +# ---> Node +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env +.env.test + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.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/todo/LICENSE b/todo/LICENSE
new file mode 100644
index 0000000..212a7ec
--- /dev/null
+++ b/todo/LICENSE
@@ -0,0 +1,21 @@
+ANTI-CAPITALIST SOFTWARE LICENSE (v 1.4)
+
+Copyright ©  
+
+This is anti-capitalist software, released for free use by individuals and organizations that do not operate by capitalist principles.
+
+Permission is hereby granted, free of charge, to any person or organization (the "User") obtaining a copy of this software and associated documentation files (the "Software"), to use, copy, modify, merge, distribute, and/or sell copies of the Software, subject to the following conditions:
+
+1. The above copyright notice and this permission notice shall be included in all copies or modified versions of the Software.
+
+2. The User is one of the following:
+a. An individual person, laboring for themselves
+b. A non-profit organization
+c. An educational institution
+d. An organization that seeks shared profit for all of its members, and allows non-members to set the cost of their labor
+
+3. If the User is an organization with owners, then all owners are workers and all workers are owners with equal equity and/or equal vote.
+
+4. If the User is an organization, then the User is not law enforcement or military, or working for or under either.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT EXPRESS OR IMPLIED WARRANTY OF ANY KIND, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
diff --git a/todo/README.md b/todo/README.md
new file mode 100644
index 0000000..85dfcb7
--- /dev/null
+++ b/todo/README.md
@@ -0,0 +1,2 @@
+# todo
+
diff --git a/todo/config.json b/todo/config.json
new file mode 100644
index 0000000..948073a
--- /dev/null
+++ b/todo/config.json
@@ -0,0 +1,9 @@
+{
+  "secret": "TEST_SECRET",
+  "https": false,
+  "alter_db": true,
+  "port": 8080,
+  "db_url": "postgres://postgres:@127.0.0.1/todo",
+  "cert": "",
+  "cert_key": ""
+}
\ No newline at end of file
diff --git a/todo/package.json b/todo/package.json
new file mode 100644
index 0000000..fd35f15
--- /dev/null
+++ b/todo/package.json
@@ -0,0 +1,26 @@
+{
+  "name": "todo",
+  "version": "1.0.0",
+  "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": {
+    "type": "git",
+    "url": "git@ssh.gitdab.com:jane/todo.git"
+  },
+  "author": "jane ",
+  "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/todo/pnpm-lock.yaml b/todo/pnpm-lock.yaml
new file mode 100644
index 0000000..1b8148f
--- /dev/null
+++ b/todo/pnpm-lock.yaml
@@ -0,0 +1,701 @@
+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
+lockfileVersion: 5.2
+packages:
+  /@types/node/15.12.1:
+    dev: false
+    resolution:
+      integrity: sha512-zyxJM8I1c9q5sRMtVF+zdd13Jt6RU4r4qfhTd7lQubyThvLfx6yYekWSQjGCGV2Tkecgxnlpl/DNlb6Hg+dmEw==
+  /accepts/1.3.7:
+    dependencies:
+      mime-types: 2.1.31
+      negotiator: 0.6.2
+    dev: false
+    engines:
+      node: '>= 0.6'
+    resolution:
+      integrity: sha512-Il80Qs2WjYlJIBNzNkK6KYqlVMTbZLXgHx2oT0pU/fjRHyEp+PEfEPY0R3WCwAGVOtauxh1hOxNgIf5bv7dQpA==
+  /any-promise/1.3.0:
+    dev: false
+    resolution:
+      integrity: sha1-q8av7tzqUugJzcA3au0845Y10X8=
+  /array-flatten/1.1.1:
+    dev: false
+    resolution:
+      integrity: sha1-ml9pkFGx5wczKPKgCJaLZOopVdI=
+  /body-parser/1.19.0:
+    dependencies:
+      bytes: 3.1.0
+      content-type: 1.0.4
+      debug: 2.6.9
+      depd: 1.1.2
+      http-errors: 1.7.2
+      iconv-lite: 0.4.24
+      on-finished: 2.3.0
+      qs: 6.7.0
+      raw-body: 2.4.0
+      type-is: 1.6.18
+    dev: false
+    engines:
+      node: '>= 0.8'
+    resolution:
+      integrity: sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw==
+  /buffer-writer/2.0.0:
+    dev: false
+    engines:
+      node: '>=4'
+    resolution:
+      integrity: sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==
+  /bytes/3.1.0:
+    dev: false
+    engines:
+      node: '>= 0.8'
+    resolution:
+      integrity: sha512-zauLjrfCG+xvoyaqLoV8bLVXXNGC4JqlxFCutSDWA6fJrTo2ZuvLYTqZ7aHBLZSMOopbzwv8f+wZcVzfVTI2Dg==
+  /content-disposition/0.5.3:
+    dependencies:
+      safe-buffer: 5.1.2
+    dev: false
+    engines:
+      node: '>= 0.6'
+    resolution:
+      integrity: sha512-ExO0774ikEObIAEV9kDo50o+79VCUdEB6n6lzKgGwupcVeRlhrj3qGAfwq8G6uBJjkqLrhT0qEYFcWng8z1z0g==
+  /content-type/1.0.4:
+    dev: false
+    engines:
+      node: '>= 0.6'
+    resolution:
+      integrity: sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==
+  /cookie-parser/1.4.5:
+    dependencies:
+      cookie: 0.4.0
+      cookie-signature: 1.0.6
+    dev: false
+    engines:
+      node: '>= 0.8.0'
+    resolution:
+      integrity: sha512-f13bPUj/gG/5mDr+xLmSxxDsB9DQiTIfhJS/sqjrmfAWiAN+x2O4i/XguTL9yDZ+/IFDanJ+5x7hC4CXT9Tdzw==
+  /cookie-signature/1.0.6:
+    dev: false
+    resolution:
+      integrity: sha1-4wOogrNCzD7oylE6eZmXNNqzriw=
+  /cookie/0.4.0:
+    dev: false
+    engines:
+      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
+    dev: false
+    resolution:
+      integrity: sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==
+  /debug/4.3.1:
+    dependencies:
+      ms: 2.1.2
+    dev: false
+    engines:
+      node: '>=6.0'
+    peerDependencies:
+      supports-color: '*'
+    peerDependenciesMeta:
+      supports-color:
+        optional: true
+    resolution:
+      integrity: sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==
+  /depd/1.1.2:
+    dev: false
+    engines:
+      node: '>= 0.6'
+    resolution:
+      integrity: sha1-m81S4UwJd2PnSbJ0xDRu0uVgtak=
+  /destroy/1.0.4:
+    dev: false
+    resolution:
+      integrity: sha1-l4hXRCxEdJ5CBmE+N5RiBYJqvYA=
+  /dottie/2.0.2:
+    dev: false
+    resolution:
+      integrity: sha512-fmrwR04lsniq/uSr8yikThDTrM7epXHBAAjH9TbeH3rEA8tdCO7mRzB9hdmdGyJCxF8KERo9CITcm3kGuoyMhg==
+  /ee-first/1.1.1:
+    dev: false
+    resolution:
+      integrity: sha1-WQxhFWsK4vTwJVcyoViyZrxWsh0=
+  /encodeurl/1.0.2:
+    dev: false
+    engines:
+      node: '>= 0.8'
+    resolution:
+      integrity: sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=
+  /escape-html/1.0.3:
+    dev: false
+    resolution:
+      integrity: sha1-Aljq5NPQwJdN4cFpGI7wBR0dGYg=
+  /etag/1.8.1:
+    dev: false
+    engines:
+      node: '>= 0.6'
+    resolution:
+      integrity: sha1-Qa4u62XvpiJorr/qg6x9eSmbCIc=
+  /express/4.17.1:
+    dependencies:
+      accepts: 1.3.7
+      array-flatten: 1.1.1
+      body-parser: 1.19.0
+      content-disposition: 0.5.3
+      content-type: 1.0.4
+      cookie: 0.4.0
+      cookie-signature: 1.0.6
+      debug: 2.6.9
+      depd: 1.1.2
+      encodeurl: 1.0.2
+      escape-html: 1.0.3
+      etag: 1.8.1
+      finalhandler: 1.1.2
+      fresh: 0.5.2
+      merge-descriptors: 1.0.1
+      methods: 1.1.2
+      on-finished: 2.3.0
+      parseurl: 1.3.3
+      path-to-regexp: 0.1.7
+      proxy-addr: 2.0.7
+      qs: 6.7.0
+      range-parser: 1.2.1
+      safe-buffer: 5.1.2
+      send: 0.17.1
+      serve-static: 1.14.1
+      setprototypeof: 1.1.1
+      statuses: 1.5.0
+      type-is: 1.6.18
+      utils-merge: 1.0.1
+      vary: 1.1.2
+    dev: false
+    engines:
+      node: '>= 0.10.0'
+    resolution:
+      integrity: sha512-mHJ9O79RqluphRrcw2X/GTh3k9tVv8YcoyY4Kkh4WDMUYKRZUq0h1o0w2rrrxBqM7VoeUVqgb27xlEMXTnYt4g==
+  /finalhandler/1.1.2:
+    dependencies:
+      debug: 2.6.9
+      encodeurl: 1.0.2
+      escape-html: 1.0.3
+      on-finished: 2.3.0
+      parseurl: 1.3.3
+      statuses: 1.5.0
+      unpipe: 1.0.0
+    dev: false
+    engines:
+      node: '>= 0.8'
+    resolution:
+      integrity: sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==
+  /forwarded/0.2.0:
+    dev: false
+    engines:
+      node: '>= 0.6'
+    resolution:
+      integrity: sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==
+  /fresh/0.5.2:
+    dev: false
+    engines:
+      node: '>= 0.6'
+    resolution:
+      integrity: sha1-PYyt2Q2XZWn6g1qx+OSyOhBWBac=
+  /http-errors/1.7.2:
+    dependencies:
+      depd: 1.1.2
+      inherits: 2.0.3
+      setprototypeof: 1.1.1
+      statuses: 1.5.0
+      toidentifier: 1.0.0
+    dev: false
+    engines:
+      node: '>= 0.6'
+    resolution:
+      integrity: sha512-uUQBt3H/cSIVfch6i1EuPNy/YsRSOUBXTVfZ+yR7Zjez3qjBz6i9+i4zjNaoqcoFVI4lQJ5plg63TvGfRSDCRg==
+  /http-errors/1.7.3:
+    dependencies:
+      depd: 1.1.2
+      inherits: 2.0.4
+      setprototypeof: 1.1.1
+      statuses: 1.5.0
+      toidentifier: 1.0.0
+    dev: false
+    engines:
+      node: '>= 0.6'
+    resolution:
+      integrity: sha512-ZTTX0MWrsQ2ZAhA1cejAwDLycFsd7I7nVtnkT3Ol0aqodaKW+0CTZDQ1uBv5whptCnc8e8HeRRJxRs0kmm/Qfw==
+  /iconv-lite/0.4.24:
+    dependencies:
+      safer-buffer: 2.1.2
+    dev: false
+    engines:
+      node: '>=0.10.0'
+    resolution:
+      integrity: sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==
+  /inflection/1.12.0:
+    dev: false
+    engines:
+      '0': node >= 0.4.0
+    resolution:
+      integrity: sha1-ogCTVlbW9fa8TcdQLhrstwMihBY=
+  /inherits/2.0.3:
+    dev: false
+    resolution:
+      integrity: sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=
+  /inherits/2.0.4:
+    dev: false
+    resolution:
+      integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+  /ipaddr.js/1.9.1:
+    dev: false
+    engines:
+      node: '>= 0.10'
+    resolution:
+      integrity: sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==
+  /lodash/4.17.21:
+    dev: false
+    resolution:
+      integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
+  /lru-cache/6.0.0:
+    dependencies:
+      yallist: 4.0.0
+    dev: false
+    engines:
+      node: '>=10'
+    resolution:
+      integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==
+  /media-typer/0.3.0:
+    dev: false
+    engines:
+      node: '>= 0.6'
+    resolution:
+      integrity: sha1-hxDXrwqmJvj/+hzgAWhUUmMlV0g=
+  /merge-descriptors/1.0.1:
+    dev: false
+    resolution:
+      integrity: sha1-sAqqVW3YtEVoFQ7J0blT8/kMu2E=
+  /methods/1.1.2:
+    dev: false
+    engines:
+      node: '>= 0.6'
+    resolution:
+      integrity: sha1-VSmk1nZUE07cxSZmVoNbD4Ua/O4=
+  /mime-db/1.48.0:
+    dev: false
+    engines:
+      node: '>= 0.6'
+    resolution:
+      integrity: sha512-FM3QwxV+TnZYQ2aRqhlKBMHxk10lTbMt3bBkMAp54ddrNeVSfcQYOOKuGuy3Ddrm38I04If834fOUSq1yzslJQ==
+  /mime-types/2.1.31:
+    dependencies:
+      mime-db: 1.48.0
+    dev: false
+    engines:
+      node: '>= 0.6'
+    resolution:
+      integrity: sha512-XGZnNzm3QvgKxa8dpzyhFTHmpP3l5YNusmne07VUOXxou9CqUqYa/HBy124RqtVh/O2pECas/MOcsDgpilPOPg==
+  /mime/1.6.0:
+    dev: false
+    engines:
+      node: '>=4'
+    hasBin: true
+    resolution:
+      integrity: sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==
+  /moment-timezone/0.5.33:
+    dependencies:
+      moment: 2.29.1
+    dev: false
+    resolution:
+      integrity: sha512-PTc2vcT8K9J5/9rDEPe5czSIKgLoGsH8UNpA4qZTVw0Vd/Uz19geE9abbIOQKaAQFcnQ3v5YEXrbSc5BpshH+w==
+  /moment/2.29.1:
+    dev: false
+    resolution:
+      integrity: sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==
+  /ms/2.0.0:
+    dev: false
+    resolution:
+      integrity: sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=
+  /ms/2.1.1:
+    dev: false
+    resolution:
+      integrity: sha512-tgp+dl5cGk28utYktBsrFqA7HKgrhgPsg6Z/EfhWI4gl1Hwq8B/GmY/0oXZ6nF8hDVesS/FpnYaD/kOWhYQvyg==
+  /ms/2.1.2:
+    dev: false
+    resolution:
+      integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==
+  /negotiator/0.6.2:
+    dev: false
+    engines:
+      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
+    dev: false
+    engines:
+      node: '>= 0.8'
+    resolution:
+      integrity: sha1-IPEzZIGwg811M3mSoWlxqi2QaUc=
+  /packet-reader/1.0.0:
+    dev: false
+    resolution:
+      integrity: sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==
+  /parseurl/1.3.3:
+    dev: false
+    engines:
+      node: '>= 0.8'
+    resolution:
+      integrity: sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==
+  /path-to-regexp/0.1.7:
+    dev: false
+    resolution:
+      integrity: sha1-32BBeABfUi8V60SQ5yR6G/qmf4w=
+  /pg-connection-string/2.5.0:
+    dev: false
+    resolution:
+      integrity: sha512-r5o/V/ORTA6TmUnyWZR9nCj1klXCO2CEKNRlVuJptZe85QuhFayC7WeMic7ndayT5IRIR0S0xFxFi2ousartlQ==
+  /pg-int8/1.0.1:
+    dev: false
+    engines:
+      node: '>=4.0.0'
+    resolution:
+      integrity: sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==
+  /pg-pool/3.3.0_pg@8.6.0:
+    dependencies:
+      pg: 8.6.0
+    dev: false
+    peerDependencies:
+      pg: '>=8.0'
+    resolution:
+      integrity: sha512-0O5huCql8/D6PIRFAlmccjphLYWC+JIzvUhSzXSpGaf+tjTZc4nn+Lr7mLXBbFJfvwbP0ywDv73EiaBsxn7zdg==
+  /pg-protocol/1.5.0:
+    dev: false
+    resolution:
+      integrity: sha512-muRttij7H8TqRNu/DxrAJQITO4Ac7RmX3Klyr/9mJEOBeIpgnF8f9jAfRz5d3XwQZl5qBjF9gLsUtMPJE0vezQ==
+  /pg-types/2.2.0:
+    dependencies:
+      pg-int8: 1.0.1
+      postgres-array: 2.0.0
+      postgres-bytea: 1.0.0
+      postgres-date: 1.0.7
+      postgres-interval: 1.2.0
+    dev: false
+    engines:
+      node: '>=4'
+    resolution:
+      integrity: sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==
+  /pg/8.6.0:
+    dependencies:
+      buffer-writer: 2.0.0
+      packet-reader: 1.0.0
+      pg-connection-string: 2.5.0
+      pg-pool: 3.3.0_pg@8.6.0
+      pg-protocol: 1.5.0
+      pg-types: 2.2.0
+      pgpass: 1.0.4
+    dev: false
+    engines:
+      node: '>= 8.0.0'
+    peerDependencies:
+      pg-native: '>=2.0.0'
+    peerDependenciesMeta:
+      pg-native:
+        optional: true
+    resolution:
+      integrity: sha512-qNS9u61lqljTDFvmk/N66EeGq3n6Ujzj0FFyNMGQr6XuEv4tgNTXvJQTfJdcvGit5p5/DWPu+wj920hAJFI+QQ==
+  /pgpass/1.0.4:
+    dependencies:
+      split2: 3.2.2
+    dev: false
+    resolution:
+      integrity: sha512-YmuA56alyBq7M59vxVBfPJrGSozru8QAdoNlWuW3cz8l+UX3cWge0vTvjKhsSHSJpo3Bom8/Mm6hf0TR5GY0+w==
+  /postgres-array/2.0.0:
+    dev: false
+    engines:
+      node: '>=4'
+    resolution:
+      integrity: sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==
+  /postgres-bytea/1.0.0:
+    dev: false
+    engines:
+      node: '>=0.10.0'
+    resolution:
+      integrity: sha1-AntTPAqokOJtFy1Hz5zOzFIazTU=
+  /postgres-date/1.0.7:
+    dev: false
+    engines:
+      node: '>=0.10.0'
+    resolution:
+      integrity: sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==
+  /postgres-interval/1.2.0:
+    dependencies:
+      xtend: 4.0.2
+    dev: false
+    engines:
+      node: '>=0.10.0'
+    resolution:
+      integrity: sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==
+  /proxy-addr/2.0.7:
+    dependencies:
+      forwarded: 0.2.0
+      ipaddr.js: 1.9.1
+    dev: false
+    engines:
+      node: '>= 0.10'
+    resolution:
+      integrity: sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==
+  /qs/6.7.0:
+    dev: false
+    engines:
+      node: '>=0.6'
+    resolution:
+      integrity: sha512-VCdBRNFTX1fyE7Nb6FYoURo/SPe62QCaAyzJvUjwRaIsc+NePBEniHlvxFmmX56+HZphIGtV0XeCirBtpDrTyQ==
+  /range-parser/1.2.1:
+    dev: false
+    engines:
+      node: '>= 0.6'
+    resolution:
+      integrity: sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==
+  /raw-body/2.4.0:
+    dependencies:
+      bytes: 3.1.0
+      http-errors: 1.7.2
+      iconv-lite: 0.4.24
+      unpipe: 1.0.0
+    dev: false
+    engines:
+      node: '>= 0.8'
+    resolution:
+      integrity: sha512-4Oz8DUIwdvoa5qMJelxipzi/iJIi40O5cGV1wNYp5hvZP8ZN0T+jiNkL0QepXs+EsQ9XJ8ipEDoiH70ySUJP3Q==
+  /readable-stream/3.6.0:
+    dependencies:
+      inherits: 2.0.4
+      string_decoder: 1.3.0
+      util-deprecate: 1.0.2
+    dev: false
+    engines:
+      node: '>= 6'
+    resolution:
+      integrity: sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==
+  /retry-as-promised/3.2.0:
+    dependencies:
+      any-promise: 1.3.0
+    dev: false
+    resolution:
+      integrity: sha512-CybGs60B7oYU/qSQ6kuaFmRd9sTZ6oXSc0toqePvV74Ac6/IFZSI1ReFQmtCN+uvW1Mtqdwpvt/LGOiCBAY2Mg==
+  /safe-buffer/5.1.2:
+    dev: false
+    resolution:
+      integrity: sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
+  /safe-buffer/5.2.1:
+    dev: false
+    resolution:
+      integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==
+  /safer-buffer/2.1.2:
+    dev: false
+    resolution:
+      integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
+  /semver/7.3.5:
+    dependencies:
+      lru-cache: 6.0.0
+    dev: false
+    engines:
+      node: '>=10'
+    hasBin: true
+    resolution:
+      integrity: sha512-PoeGJYh8HK4BTO/a9Tf6ZG3veo/A7ZVsYrSA6J8ny9nb3B1VrpkuN+z9OE5wfE5p6H4LchYZsegiQgbJD94ZFQ==
+  /send/0.17.1:
+    dependencies:
+      debug: 2.6.9
+      depd: 1.1.2
+      destroy: 1.0.4
+      encodeurl: 1.0.2
+      escape-html: 1.0.3
+      etag: 1.8.1
+      fresh: 0.5.2
+      http-errors: 1.7.3
+      mime: 1.6.0
+      ms: 2.1.1
+      on-finished: 2.3.0
+      range-parser: 1.2.1
+      statuses: 1.5.0
+    dev: false
+    engines:
+      node: '>= 0.8.0'
+    resolution:
+      integrity: sha512-BsVKsiGcQMFwT8UxypobUKyv7irCNRHk1T0G680vk88yf6LBByGcZJOTJCrTP2xVN6yI+XjPJcNuE3V4fT9sAg==
+  /sequelize-pool/6.1.0:
+    dev: false
+    engines:
+      node: '>= 10.0.0'
+    resolution:
+      integrity: sha512-4YwEw3ZgK/tY/so+GfnSgXkdwIJJ1I32uZJztIEgZeAO6HMgj64OzySbWLgxj+tXhZCJnzRfkY9gINw8Ft8ZMg==
+  /sequelize/6.6.2_pg@8.6.0:
+    dependencies:
+      debug: 4.3.1
+      dottie: 2.0.2
+      inflection: 1.12.0
+      lodash: 4.17.21
+      moment: 2.29.1
+      moment-timezone: 0.5.33
+      pg: 8.6.0
+      retry-as-promised: 3.2.0
+      semver: 7.3.5
+      sequelize-pool: 6.1.0
+      toposort-class: 1.0.1
+      uuid: 8.3.2
+      validator: 10.11.0
+      wkx: 0.5.0
+    dev: false
+    engines:
+      node: '>=10.0.0'
+    peerDependencies:
+      mariadb: '*'
+      mysql2: '*'
+      pg: '*'
+      pg-hstore: '*'
+      sqlite3: '*'
+      tedious: '*'
+    peerDependenciesMeta:
+      mariadb:
+        optional: true
+      mysql2:
+        optional: true
+      pg:
+        optional: true
+      pg-hstore:
+        optional: true
+      sqlite3:
+        optional: true
+      tedious:
+        optional: true
+    resolution:
+      integrity: sha512-H/zrzmTK+tis9PJaSigkuXI57nKBvNCtPQol0yxCvau1iWLzSOuq8t3tMOVeQ+Ep8QH2HoD9/+FCCIAqzUr/BQ==
+  /serve-static/1.14.1:
+    dependencies:
+      encodeurl: 1.0.2
+      escape-html: 1.0.3
+      parseurl: 1.3.3
+      send: 0.17.1
+    dev: false
+    engines:
+      node: '>= 0.8.0'
+    resolution:
+      integrity: sha512-JMrvUwE54emCYWlTI+hGrGv5I8dEwmco/00EvkzIIsR7MqrHonbD9pO2MOfFnpFntl7ecpZs+3mW+XbQZu9QCg==
+  /setprototypeof/1.1.1:
+    dev: false
+    resolution:
+      integrity: sha512-JvdAWfbXeIGaZ9cILp38HntZSFSo3mWg6xGcJJsd+d4aRMOqauag1C63dJfDw7OaMYwEbHMOxEZ1lqVRYP2OAw==
+  /split2/3.2.2:
+    dependencies:
+      readable-stream: 3.6.0
+    dev: false
+    resolution:
+      integrity: sha512-9NThjpgZnifTkJpzTZ7Eue85S49QwpNhZTq6GRJwObb6jnLFNGB7Qm73V5HewTROPyxD0C29xqmaI68bQtV+hg==
+  /statuses/1.5.0:
+    dev: false
+    engines:
+      node: '>= 0.6'
+    resolution:
+      integrity: sha1-Fhx9rBd2Wf2YEfQ3cfqZOBR4Yow=
+  /string_decoder/1.3.0:
+    dependencies:
+      safe-buffer: 5.2.1
+    dev: false
+    resolution:
+      integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==
+  /toidentifier/1.0.0:
+    dev: false
+    engines:
+      node: '>=0.6'
+    resolution:
+      integrity: sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==
+  /toposort-class/1.0.1:
+    dev: false
+    resolution:
+      integrity: sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg=
+  /type-is/1.6.18:
+    dependencies:
+      media-typer: 0.3.0
+      mime-types: 2.1.31
+    dev: false
+    engines:
+      node: '>= 0.6'
+    resolution:
+      integrity: sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==
+  /unpipe/1.0.0:
+    dev: false
+    engines:
+      node: '>= 0.8'
+    resolution:
+      integrity: sha1-sr9O6FFKrmFltIF4KdIbLvSZBOw=
+  /util-deprecate/1.0.2:
+    dev: false
+    resolution:
+      integrity: sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=
+  /utils-merge/1.0.1:
+    dev: false
+    engines:
+      node: '>= 0.4.0'
+    resolution:
+      integrity: sha1-n5VxD1CiZ5R7LMwSR0HBAoQn5xM=
+  /uuid/8.3.2:
+    dev: false
+    hasBin: true
+    resolution:
+      integrity: sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==
+  /validator/10.11.0:
+    dev: false
+    engines:
+      node: '>= 0.10'
+    resolution:
+      integrity: sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw==
+  /vary/1.1.2:
+    dev: false
+    engines:
+      node: '>= 0.8'
+    resolution:
+      integrity: sha1-IpnwLG3tMNSllhsLn3RSShj2NPw=
+  /wkx/0.5.0:
+    dependencies:
+      '@types/node': 15.12.1
+    dev: false
+    resolution:
+      integrity: sha512-Xng/d4Ichh8uN4l0FToV/258EjMGU9MGcA0HV2d9B/ZpZB3lqQm7nkOdZdm5GhKtLLhAE7PiVQwN4eN+2YJJUg==
+  /xtend/4.0.2:
+    dev: false
+    engines:
+      node: '>=0.4'
+    resolution:
+      integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==
+  /yallist/4.0.0:
+    dev: false
+    resolution:
+      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/todo/src/config.js b/todo/src/config.js
new file mode 100644
index 0000000..9343cec
--- /dev/null
+++ b/todo/src/config.js
@@ -0,0 +1,21 @@
+const fs = require('fs');
+
+if (!global.config) {
+  global.config = {}
+  const cfg = JSON.parse(fs.readFileSync('./config.json'));
+  if (cfg) {
+    global.config = cfg;
+  }
+}
+
+class Config {
+  get config() {
+    return global.config;
+  }
+
+  set config(dat) {
+    global.config = dat; 
+  }
+}
+
+module.exports = new Config();
\ No newline at end of file
diff --git a/todo/src/db_interface.js b/todo/src/db_interface.js
new file mode 100644
index 0000000..d841c92
--- /dev/null
+++ b/todo/src/db_interface.js
@@ -0,0 +1,77 @@
+const Sequelize = require('sequelize');
+const Config = require('./config.js');
+
+if (!Config.config.db_url) {
+  console.error("No database url found. please set `db_url` in config.json");
+  process.exit();
+}
+
+const db = new Sequelize(Config.config.db_url);
+
+const User = db.define('User', {
+  id: {
+    type: Sequelize.DataTypes.UUID,
+    defaultValue: Sequelize.UUIDV4,
+    allowNull: false,
+    primaryKey: true,
+    unique: true
+  },
+  email: {
+    type: Sequelize.DataTypes.STRING,
+    allowNull: false,
+    unique: true
+  },
+  password_hash: {
+    type: Sequelize.DataTypes.STRING,
+    allowNull: true
+  }
+});
+
+const Todo = db.define('Todo', {
+  id: {
+    type: Sequelize.DataTypes.UUID,
+    defaultValue: Sequelize.UUIDV4,
+    allowNull: false,
+    primaryKey: true,
+    unique: true
+  },
+  content: {
+    type: Sequelize.DataTypes.TEXT,
+    allowNull: false
+  }
+});
+
+const Tag = db.define('Tag', {
+  id: {
+    type: Sequelize.DataTypes.UUID,
+    defaultValue: Sequelize.UUIDV4,
+    allowNull: false,
+    primaryKey: true,
+    unique: true
+  },
+  content: {
+    type: Sequelize.DataTypes.STRING,
+    allowNull: false
+  }
+});
+
+User.hasMany(Todo);
+Todo.hasMany(Tag);
+
+let options = {
+  alter: false
+};
+if (Config.config.alter_db) {
+  options.alter = true;
+}
+
+User.sync(options);
+module.exports = {
+  db: db,
+  constructors: {
+    user: () => { return User.build(); }
+  },
+  schemas: {
+    user: User
+  }
+}
\ No newline at end of file
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/todo/src/user.js b/todo/src/user.js
new file mode 100644
index 0000000..980cb77
--- /dev/null
+++ b/todo/src/user.js
@@ -0,0 +1,235 @@
+const express = require('express');
+const crypto = require('crypto');
+const Config = require('./config.js');
+const Database = require('./db_interface.js');
+
+let router = express.Router();
+
+router.use(express.json());
+
+let session_entropy = {};
+
+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}});
+    if (!user) {
+      return undefined;
+    }
+    user_cache[user.id] = {
+      id: user.id,
+      email: user.email,
+      password_hash: user.password_hash,
+    };
+    email_cache[user.email] = user.id;
+  }
+  console.log(`returning ${JSON.stringify(user_cache[id])}`);
+  return user_cache[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}});
+    if (!user) {
+      return undefined;
+    }
+    user_cache[user.id] = {
+      id: user.id,
+      email: user.email,
+      password_hash: user.password_hash,
+    };
+    email_cache[user.email] = user.id;
+  }
+  console.log(`returning ${JSON.stringify(user_cache[email_cache[email]])}`);
+  return user_cache[email_cache[email]];
+}
+
+router.get('/byEmail/:email', async (req, res) => {
+  if (!req.params?.email) {
+    res.status(400).json({
+      error: 'email is a required parameter',
+    });
+  }
+  let user = get_user_details_by_email(req.params.email);
+  console.log(user);
+  if (user != null) {
+    res.json({
+      id: user.id,
+      email: user.email,
+    });
+  } else {
+    res.sendStatus(404);
+  }
+});
+
+function hash(secret, password) {
+  let pw_hash = crypto.pbkdf2Sync(
+    password,
+    secret,
+    Config.config.key?.iterations || 1000,
+    Config.config.key?.length || 64,
+    'sha512'
+  );
+
+  return pw_hash.toString('base64');
+}
+
+function verify(secret, password, hash) {
+  let pw_hash = crypto.pbkdf2Sync(
+    password,
+    secret,
+    Config.config.key?.iterations || 1000,
+    Config.config.key?.length || 64,
+    'sha512'
+  );
+
+  return hash === pw_hash.toString('base64');
+}
+
+function hash_password(password) {
+  return hash(Config.config.secret, password);
+}
+
+function verify_password(password, hash) {
+  return verify(Config.config.secret, password, hash);
+}
+
+function get_session_token(id, token) {
+  session_entropy[id] = crypto.randomBytes(Config.config.session_entropy || 32);
+  return hash(session_entropy[id], token);
+}
+
+function verify_session_token(id, hash, token) {
+  if (session_entropy[id]) {
+    return verify(session_entropy[id], hash, token);
+  } else {
+    return false;
+  }
+}
+
+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);
+  if (!userid || !session_token) {
+    return res.sendStatus(401);
+  }
+  let user = await get_user_details(userid);
+  if (!user) {
+    return res.sendStatus(401);
+  }
+  let verified_session = verify_session_token(
+    userid,
+    user.password_hash,
+    session_token
+  );
+  if (!verified_session) {
+    return res.sendStatus(401);
+  }
+  return next();
+}
+
+router.post('/new', async (req, res) => {
+  if (!req.body?.email || !req.body?.password) {
+    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) {
+    return res.status(403).json({
+      error: `email ${req.body.email} is already in use.`,
+    });
+  } else {
+    let user = await Database.schemas.user.create({
+      email: String(req.body.email),
+      password_hash: hash_password(req.body.password),
+    });
+
+    return res.json({
+      id: user.id,
+      email: user.email,
+    });
+  }
+});
+
+router.post('/login', async (req, res) => {
+  if (!req.body?.email || !req.body?.password) {
+    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) {
+    return res.status(401).json({
+      error: 'incorrect email or password',
+    });
+  }
+  let verified = verify_password(req.body.password, user.password_hash);
+
+  if (!verified) {
+    return res.status(401).json({
+      error: 'incorrect email or password',
+    });
+  }
+
+  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) => {
+  console.log(req.params);
+  if (!req.params?.id) {
+    return res.status(400).json({
+      error: 'must have id parameter',
+    });
+  }
+  let id = req.params?.id;
+  console.log(id);
+  let user = await get_user_details(id);
+  console.log(user);
+  if (user != null) {
+    return res.json({
+      id: user.id,
+      email: user.email,
+    });
+  } else {
+    return res.sendStatus(404);
+  }
+});
+
+router.use('/authorized', enforce_session_login);
+router.get('/authorized', async (req, res) => {
+  let userid = req.cookies?.userid;
+  let user = await get_user_details(userid);
+  return res.json({
+    authorized: true,
+    user: {
+      id: user.id,
+      email: user.email,
+    },
+  });
+});
+
+module.exports = {
+  router: router,
+  enforce_session_login: enforce_session_login,
+};