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,
+};