diff --git a/cmd/mi/middleware.go b/cmd/mi/middleware.go index 62b69d7..5eab31c 100644 --- a/cmd/mi/middleware.go +++ b/cmd/mi/middleware.go @@ -5,7 +5,6 @@ import ( "encoding/hex" "flag" "net/http" - "strings" "time" "github.com/google/uuid" @@ -69,8 +68,10 @@ var publicRoutes map[string]bool func init() { publicRoutes = map[string]bool{ - "/webhooks/": true, - "/static/": true, + "/webhooks/": true, + "/static/": true, + "/static/main.js": true, + "/static/gruvbox.css": true, } } @@ -91,7 +92,7 @@ func (pm PasetoMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) { } for k := range publicRoutes { - if strings.HasPrefix(r.URL.EscapedPath(), k) { + if r.URL.EscapedPath() == k { goto ok } } diff --git a/frontend/elm.json b/frontend/elm.json index 7e5229d..13fdd97 100644 --- a/frontend/elm.json +++ b/frontend/elm.json @@ -11,12 +11,14 @@ "elm/html": "1.0.0", "elm/http": "2.0.0", "elm/json": "1.1.3", - "elm/url": "1.0.0" + "elm/time": "1.0.0", + "elm/url": "1.0.0", + "rtfeldman/elm-iso8601-date-strings": "1.1.3" }, "indirect": { "elm/bytes": "1.0.8", "elm/file": "1.0.5", - "elm/time": "1.0.0", + "elm/parser": "1.1.0", "elm/virtual-dom": "1.0.2" } }, diff --git a/frontend/src/Main.elm b/frontend/src/Main.elm index 864ed21..658c60d 100644 --- a/frontend/src/Main.elm +++ b/frontend/src/Main.elm @@ -7,209 +7,278 @@ import Html.Attributes exposing (..) import Html.Events exposing (onInput) import Http import Json.Decode as D +import SwitchData import Url +import Url.Builder + --- MAIN main : Program () Model Msg main = - Browser.application - { init = init - , view = view - , update = update - , subscriptions = subscriptions - , onUrlChange = UrlChanged - , onUrlRequest = LinkClicked + Browser.application + { init = init + , view = view + , update = update + , subscriptions = subscriptions + , onUrlChange = UrlChanged + , onUrlRequest = LinkClicked + } + + +type alias Model = + { key : Nav.Key + , url : Url.Url + , token : Maybe String + , token_data : Maybe TokenData + , switch_cursor : ( Int, Int ) + , switch_data : Maybe (List SwitchData.Switch) } --- MODEL -type alias Model = - { key : Nav.Key - , url : Url.Url - , token: Maybe String - , token_data: Maybe TokenData - , switch_data : Maybe ( List Switch ) - } init : () -> Url.Url -> Nav.Key -> ( Model, Cmd Msg ) init flags url key = - ( Model key url Nothing Nothing Nothing, Cmd.none ) + ( Model + key + url + Nothing + Nothing + ( 40, 0 ) + Nothing + , Cmd.none + ) + --- UPDATE type Msg - = LinkClicked Browser.UrlRequest - | UrlChanged Url.Url - | TokenInput String - | TokenValidate ( Result Http.Error TokenData ) - | Logout - | NoSwitchData - | GotSwitchData ( List Switch ) + = LinkClicked Browser.UrlRequest + | UrlChanged Url.Url + | TokenInput String + | TokenValidate (Result Http.Error TokenData) + | Logout + | GetSwitchData ( Int, Int ) + | GotSwitchData (Result Http.Error (List SwitchData.Switch)) -type alias Switch - = { id : String - , who : String - , started_at : String - , ended_at : Maybe String - , duration : Int - } -type alias TokenData - = { sub : String +type alias TokenData = + { sub : String , jti : String } + tokenDecoder : D.Decoder TokenData tokenDecoder = - D.map2 TokenData - (D.field "sub" D.string) - (D.field "jti" D.string) + D.map2 TokenData + (D.field "sub" D.string) + (D.field "jti" D.string) + request method token path body expect = - Http.request - { method = method - , body = body - , headers = - [ Http.header "Authorization" token - ] - , url = path - , expect = expect - , timeout = Nothing - , tracker = Nothing - } + Http.request + { method = method + , body = body + , headers = + [ Http.header "Authorization" token + ] + , url = path + , expect = expect + , timeout = Nothing + , tracker = Nothing + } + update : Msg -> Model -> ( Model, Cmd Msg ) update msg model = - case msg of - LinkClicked urlRequest -> - case urlRequest of - Browser.Internal url -> - ( model, Nav.pushUrl model.key (Url.toString url) ) + case msg of + LinkClicked urlRequest -> + case urlRequest of + Browser.Internal url -> + ( model, Nav.pushUrl model.key (Url.toString url) ) - Browser.External href -> - ( model, Nav.load href ) + Browser.External href -> + ( model, Nav.load href ) - UrlChanged url -> - case url.path of - "/logout" -> - ( model, Nav.load "/" ) + UrlChanged url -> + case url.path of + "/logout" -> + ( model, Nav.load "/" ) - default -> - ( { model | url = url } - , Cmd.none - ) + default -> + ( { model | url = url } + , Cmd.none + ) - TokenInput token -> - ( { model | token = Just token } - , request "GET" token "/.within/tokeninfo" Http.emptyBody (expectJson TokenValidate tokenDecoder) - ) + TokenInput token -> + ( { model | token = Just token } + , request "GET" token "/.within/tokeninfo" Http.emptyBody (expectJson TokenValidate tokenDecoder) + ) - TokenValidate result -> - case result of - Ok data -> - ( { model | token_data = Just data } - , Cmd.none - ) + TokenValidate result -> + case result of + Ok data -> + ( { model | token_data = Just data } + , Cmd.none + ) - Err _ -> - ( { model | token = Nothing } - , Cmd.none - ) + Err _ -> + ( { model | token = Nothing } + , Cmd.none + ) + + Logout -> + ( { model | token = Nothing, token_data = Nothing } + , Nav.load "/" + ) + + GetSwitchData ( limit, page ) -> + case model.token of + Just token -> + ( model + , request "GET" + token + (SwitchData.dataURL limit page) + Http.emptyBody + (expectJson GotSwitchData (D.list SwitchData.decoder)) + ) + + Nothing -> + ( model, Cmd.none ) + + _ -> + ( model, Cmd.none ) - Logout -> - ( { model | token = Nothing, token_data = Nothing } - , Nav.load "/" - ) - _ -> - ( model, Cmd.none ) --- SUBSCRIPTIONS subscriptions : Model -> Sub Msg subscriptions _ = - Sub.none + Sub.none + --- VIEW view : Model -> Browser.Document Msg view model = - case model.token of - Nothing -> - template "Login" - [ h1 [] [ text "Login" ] - , viewInput "password" "API Token" "" TokenInput - ] - Just token -> - case model.url.path of - "/" -> - template "Mi" - [ navBar - , h1 [] [ text "Mi" ] - , p [] [ text "TODO: everything" ] - , p [] - [ text "Token sub: " - , text (Maybe.withDefault (TokenData "" "") model.token_data).sub - , text " ID: " - , text (Maybe.withDefault (TokenData "" "") model.token_data).jti - ] - ] - other -> - template "Not found" - [ navBar - , h1 [] [ text "Not found" ] - , p [] - [ text "The requested URL " - , b [] [ text other ] - , text " was not found." - ] - ] + case model.token_data of + Nothing -> + { title = "Login" + , body = + [ node "main" + [ style "align" "center" ] + [ h1 [] [ text "Login" ] + , viewInput "password" "API Token" "" TokenInput + ] + ] + } + + Just token_data -> + case model.url.path of + "/" -> + template "Mi" + [ h1 [] [ text "Mi" ] + , h2 [] [ text "TODO" ] + , ul [] + [ li [] [ text "Switch CRUD" ] + , li [] [ text "POSSE manual announcement" ] + ] + , h2 [] [ text "Token data" ] + , p [] + [ text "Token sub: " + , text token_data.sub + , Html.br [] [] + , text "ID: " + , text token_data.jti + ] + ] + + "/switch" -> + case model.switch_data of + Nothing -> + template "Switch counter" + [ h1 [] [ text "Switch counter" ] + , p [] [ text "loading..." ] + ] + + Just switches -> + template "Switch counter" + [ h1 [] [ text "TODO: table" ] + ] + + other -> + template "Not found" + [ h1 [] [ text "Not found" ] + , p [] + [ text "The requested URL " + , b [] [ text other ] + , text " was not found." + ] + ] + viewInput : String -> String -> String -> (String -> msg) -> Html msg viewInput t p v toMsg = - input [ type_ t, placeholder p, value v, onInput toMsg ] [] + input [ type_ t, placeholder p, value v, onInput toMsg ] [] + viewLink : String -> String -> Html msg viewLink path title = - a [ href path ] [ text title ] + a [ href path ] [ text title ] + template : String -> List (Html msg) -> Browser.Document msg template title body = - { title = title - , body = - [ node "main" [] - body - ] - } + { title = title + , body = + [ node "main" + [] + [ navBar + , div [] body + , footer + ] + ] + } + navBar : Html msg navBar = - node "nav" [] - [ p [] - [ viewLink "/" "Mi" - , text " - " - , viewLink "/switch" "Switch tracker" - , text " - " - , viewLink "/logout" "Logout" - ] - ] + node "nav" + [] + [ p [] + [ viewLink "/" "Mi" + , text " - " + , viewLink "/switch" "Switch tracker" + , text " - " + , viewLink "/logout" "Logout" + ] + ] + + +footer : Html msg +footer = + node "footer" + [] + [ p [] + [ a [ href "https://within.website" ] [ text "From Within" ] + , text " - " + , a [ href "https://tulpa.dev/cadey/mi" ] [ text "Source code" ] + ] + ] + expectJson : (Result Http.Error a -> msg) -> D.Decoder a -> Http.Expect msg expectJson toMsg decoder = - Http.expectStringResponse toMsg <| - \response -> - case response of - Http.BadUrl_ url -> - Err (Http.BadUrl url) + Http.expectStringResponse toMsg <| + \response -> + case response of + Http.BadUrl_ url -> + Err (Http.BadUrl url) - Http.Timeout_ -> - Err Http.Timeout + Http.Timeout_ -> + Err Http.Timeout - Http.NetworkError_ -> - Err Http.NetworkError + Http.NetworkError_ -> + Err Http.NetworkError - Http.BadStatus_ metadata body -> - Err (Http.BadStatus metadata.statusCode) + Http.BadStatus_ metadata body -> + Err (Http.BadStatus metadata.statusCode) - Http.GoodStatus_ metadata body -> - case D.decodeString decoder body of - Ok value -> - Ok value + Http.GoodStatus_ metadata body -> + case D.decodeString decoder body of + Ok value -> + Ok value - Err err -> - Err (Http.BadBody (D.errorToString err)) + Err err -> + Err (Http.BadBody (D.errorToString err)) diff --git a/frontend/src/SwitchData.elm b/frontend/src/SwitchData.elm new file mode 100644 index 0000000..9cd0f95 --- /dev/null +++ b/frontend/src/SwitchData.elm @@ -0,0 +1,30 @@ +module SwitchData exposing (..) + +import Iso8601 +import Json.Decode exposing (Decoder, field, int, map5, nullable, string) +import Time exposing (Posix) +import Url.Builder as UB + + +type alias Switch = + { id : String + , who : String + , started_at : Posix + , ended_at : Maybe Posix + , duration : Int + } + + +decoder : Decoder Switch +decoder = + map5 Switch + (field "id" string) + (field "who" string) + (field "started_at" Iso8601.decoder) + (field "ended_at" (nullable Iso8601.decoder)) + (field "duration" int) + + +dataURL : Int -> Int -> String +dataURL limit page = + UB.absolute [ "switches" ] [ UB.int "limit" limit, UB.int "page" page ]