frontend: introduce static frontend

This commit is contained in:
Cadey Ratio 2020-01-12 19:05:49 +00:00
parent 27a8595cbc
commit a364713031
9 changed files with 378 additions and 11 deletions

View File

@ -5,9 +5,17 @@ COPY go.sum .
RUN go mod download
COPY . .
RUN GOBIN=/mi/bin go install ./cmd/...
RUN apk --no-cache add curl \
&& curl -L -o elm.gz https://github.com/elm/compiler/releases/download/0.19.1/binary-for-linux-64-bit.gz \
&& gunzip elm.gz \
&& chmod +x ./elm \
&& mv elm /usr/local/bin \
&& cd ./frontend \
&& elm make --output ../static/main.js src/Main.elm
FROM xena/alpine
COPY --from=build /mi/bin /usr/local/bin
COPY ./run /run
RUN mkdir -p /mi
COPY --from=build /mi/static /mi/static
WORKDIR /mi
CMD ["/bin/sh", "/run/start.sh"]

View File

@ -2,6 +2,7 @@ package main
import (
"context"
"encoding/json"
"flag"
"fmt"
"net/http"
@ -74,6 +75,11 @@ func main() {
ln.FatalErr(ctx, err)
}
mux.Handle("/static/", http.FileServer(http.Dir(".")))
mux.HandleFunc("/", func(rw http.ResponseWriter, req *http.Request) {
http.ServeFile(rw, req, "./static/index.html")
})
tok, err := pm.CreateToken(
[][2]string{
{"from", "main"},
@ -96,6 +102,11 @@ func main() {
mi.RegisterRoutes()
sc := switchcounter.New(session, mux)
mux.HandleFunc("/.within/tokeninfo", func(rw http.ResponseWriter, req *http.Request) {
tokenData := req.Context().Value(tokenKey)
json.NewEncoder(rw).Encode(tokenData)
})
if *switchFile != "" {
fin, err := os.Open(*switchFile)
if err != nil {

View File

@ -1,8 +1,8 @@
package main
import (
"context"
"encoding/hex"
"encoding/json"
"flag"
"net/http"
"strings"
@ -11,7 +11,6 @@ import (
"github.com/google/uuid"
"github.com/o1egl/paseto/v2"
"golang.org/x/crypto/ed25519"
"within.website/ln"
)
var (
@ -66,31 +65,44 @@ func (pm PasetoMiddleware) CreateToken(data [][2]string, expiration time.Time) (
return tok, nil
}
var publicRoutes map[string]bool
func init() {
publicRoutes = map[string]bool{
"/webhooks/": true,
"/static/": true,
}
}
type ctxKey int
const (
tokenKey ctxKey = iota
)
func (pm PasetoMiddleware) ServeHTTP(w http.ResponseWriter, r *http.Request) {
tok := r.Header.Get("Authorization")
var newJsonToken paseto.JSONToken
var newFooter string
var err error
if r.URL.EscapedPath() == "/.within/botinfo" {
if r.URL.EscapedPath() == "/.within/botinfo" || r.URL.EscapedPath() == "/" {
goto ok
}
if strings.HasPrefix(r.URL.EscapedPath(), "/webhooks/") {
goto ok
for k := range publicRoutes {
if strings.HasPrefix(r.URL.EscapedPath(), k) {
goto ok
}
}
err = pm.v2.Verify(tok, pm.pubKey, &newJsonToken, &newFooter)
if err != nil {
ln.Error(r.Context(), err)
http.Error(w, "Not allowed", http.StatusForbidden)
return
}
if r.URL.EscapedPath() == "/.within/tokeninfo" {
json.NewEncoder(w).Encode(newJsonToken)
return
}
r = r.WithContext(context.WithValue(r.Context(), tokenKey, newJsonToken))
ok:
pm.next.ServeHTTP(w, r)

1
frontend/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
elm-stuff

27
frontend/elm.json Normal file
View File

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

215
frontend/src/Main.elm Normal file
View File

@ -0,0 +1,215 @@
module Main exposing (..)
import Browser
import Browser.Navigation as Nav
import Html exposing (..)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
import Http
import Json.Decode as D
import Url
-- MAIN
main : Program () Model Msg
main =
Browser.application
{ init = init
, view = view
, update = update
, subscriptions = subscriptions
, onUrlChange = UrlChanged
, onUrlRequest = LinkClicked
}
-- 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 )
-- UPDATE
type Msg
= LinkClicked Browser.UrlRequest
| UrlChanged Url.Url
| TokenInput String
| TokenValidate ( Result Http.Error TokenData )
| Logout
| NoSwitchData
| GotSwitchData ( List Switch )
type alias Switch
= { id : String
, who : String
, started_at : String
, ended_at : Maybe String
, duration : Int
}
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)
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
}
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) )
Browser.External href ->
( model, Nav.load href )
UrlChanged url ->
case url.path of
"/logout" ->
( model, Nav.load "/" )
default ->
( { model | url = url }
, Cmd.none
)
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
)
Err _ ->
( { model | token = Nothing }
, Cmd.none
)
Logout ->
( { model | token = Nothing, token_data = Nothing }
, Nav.load "/"
)
_ ->
( model, Cmd.none )
-- SUBSCRIPTIONS
subscriptions : Model -> Sub Msg
subscriptions _ =
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."
]
]
viewInput : String -> String -> String -> (String -> msg) -> Html msg
viewInput t p v toMsg =
input [ type_ t, placeholder p, value v, onInput toMsg ] []
viewLink : String -> String -> Html msg
viewLink path title =
a [ href path ] [ text title ]
template : String -> List (Html msg) -> Browser.Document msg
template title body =
{ title = title
, body =
[ node "main" []
body
]
}
navBar : Html msg
navBar =
node "nav" []
[ p []
[ viewLink "/" "Mi"
, text " - "
, viewLink "/switch" "Switch tracker"
, text " - "
, viewLink "/logout" "Logout"
]
]
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.Timeout_ ->
Err Http.Timeout
Http.NetworkError_ ->
Err Http.NetworkError
Http.BadStatus_ metadata body ->
Err (Http.BadStatus metadata.statusCode)
Http.GoodStatus_ metadata body ->
case D.decodeString decoder body of
Ok value ->
Ok value
Err err ->
Err (Http.BadBody (D.errorToString err))

1
static/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
main.js

74
static/gruvbox.css Normal file
View File

@ -0,0 +1,74 @@
main {
font-family: monospace, monospace;
max-width: 38rem;
padding: 2rem;
margin: auto;
}
@media only screen and (max-device-width: 736px) {
main {
padding: 0rem;
}
}
::selection {
background: #d3869b;
}
body {
background: #282828;
color: #ebdbb2;
}
pre {
background-color: #3c3836;
padding: 1em;
border: 0;
}
a, a:active, a:visited {
color: #b16286;
background-color: #1d2021;
}
h1, h2, h3, h4, h5 {
margin-bottom: .1rem;
}
blockquote {
border-left: 1px solid #bdae93;
margin: 0.5em 10px;
padding: 0.5em 10px;
}
footer {
align: center;
}
@media (prefers-color-scheme: light) {
body {
background: #fbf1c7;
color: #3c3836;
}
pre {
background-color: #ebdbb2;
padding: 1em;
border: 0;
}
a, a:active, a:visited {
color: #b16286;
background-color: #f9f5d7;
}
h1, h2, h3, h4, h5 {
margin-bottom: .1rem;
}
blockquote {
border-left: 1px solid #655c54;
margin: 0.5em 10px;
padding: 0.5em 10px;
}
}

18
static/index.html Normal file
View File

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<title>Mi</title>
<link type="text/css" rel="stylesheet" href="/static/gruvbox.css">
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script src="/static/main.js"></script>
</head>
<body id="top">
<main>
<script>var app = Elm.Main.init();</script>
<footer>
<p><a href="https://christine.website">From Within</a> - <a href="#top">Go to the top</a></p>
</footer>
</main>
</body>
</html>