From 4a2fb7e82d5d617298cb28b66485fc6f30c55781 Mon Sep 17 00:00:00 2001 From: Julien Dessaux Date: Wed, 21 Apr 2021 17:23:07 +0200 Subject: Reworked the webui package, added authentication feature and tests --- internal/webui/app.go | 78 ----------------------- internal/webui/html/base.html | 18 ++++++ internal/webui/html/index.html | 24 -------- internal/webui/html/login.html | 13 ++++ internal/webui/html/root.html | 15 +++++ internal/webui/login.go | 85 ++++++++++++++++++++++++++ internal/webui/login_test.go | 136 +++++++++++++++++++++++++++++++++++++++++ internal/webui/root.go | 58 ++++++++++++++++++ internal/webui/root_test.go | 54 ++++++++++++++++ internal/webui/session.go | 19 ++++++ internal/webui/utils.go | 60 ++++++++++++++++++ internal/webui/utils_test.go | 71 +++++++++++++++++++++ internal/webui/webui.go | 25 ++++++++ 13 files changed, 554 insertions(+), 102 deletions(-) delete mode 100644 internal/webui/app.go create mode 100644 internal/webui/html/base.html delete mode 100644 internal/webui/html/index.html create mode 100644 internal/webui/html/login.html create mode 100644 internal/webui/html/root.html create mode 100644 internal/webui/login.go create mode 100644 internal/webui/login_test.go create mode 100644 internal/webui/root.go create mode 100644 internal/webui/root_test.go create mode 100644 internal/webui/session.go create mode 100644 internal/webui/utils.go create mode 100644 internal/webui/utils_test.go create mode 100644 internal/webui/webui.go (limited to 'internal') diff --git a/internal/webui/app.go b/internal/webui/app.go deleted file mode 100644 index f6082dc..0000000 --- a/internal/webui/app.go +++ /dev/null @@ -1,78 +0,0 @@ -package webui - -import ( - "embed" - "html/template" - "log" - "net/http" - "time" - - "git.adyxax.org/adyxax/trains/pkg/config" - "git.adyxax.org/adyxax/trains/pkg/navitia_api_client" -) - -// the api client object -var client *navitia_api_client.Client - -// the webui configuration -var conf *config.Config - -//go:embed html/* -var templatesFS embed.FS - -//go:embed static/* -var staticFS embed.FS - -// The page template variable -type Page struct { - Departures []Departure - Title string -} -type Departure struct { - DisplayName string - Arrival string - Odd bool -} - -// The root handler of the webui -func rootHandler(w http.ResponseWriter, r *http.Request) { - if r.URL.Path == "/" { - var p Page - if d, err := client.GetDepartures(conf.TrainStop); err != nil { - log.Printf("%+v\n%s\n", d, err) - } else { - for i := 0; i < len(d.Departures); i++ { - t, err := time.Parse("20060102T150405", d.Departures[i].StopDateTime.ArrivalDateTime) - if err != nil { - panic(err) - } - p.Departures = append(p.Departures, Departure{d.Departures[i].DisplayInformations.Direction, t.Format("Mon, 02 Jan 2006 15:04:05"), i%2 == 1}) - } - w.Header().Set("Cache-Control", "no-store, no-cache") - } - p.Title = "Horaires des prochains trains à Crépieux la Pape" - renderTemplate(w, "index", p) - } else { - http.Error(w, "404 Not Found", http.StatusNotFound) - } -} - -var templates = template.Must(template.ParseFS(templatesFS, "html/index.html")) - -func renderTemplate(w http.ResponseWriter, tmpl string, p Page) { - err := templates.ExecuteTemplate(w, tmpl+".html", p) - if err != nil { - http.Error(w, err.Error(), http.StatusInternalServerError) - } -} - -func Run(c *config.Config) { - conf = c - client = navitia_api_client.NewClient(c.Token) - http.Handle("/static/", http.FileServer(http.FS(staticFS))) - http.HandleFunc("/", rootHandler) - - listenStr := c.Address + ":" + c.Port - log.Printf("Starting webui on %s", listenStr) - log.Fatal(http.ListenAndServe(listenStr, nil)) -} diff --git a/internal/webui/html/base.html b/internal/webui/html/base.html new file mode 100644 index 0000000..7522e94 --- /dev/null +++ b/internal/webui/html/base.html @@ -0,0 +1,18 @@ +{{ define "base" }} + + + + + {{ .Title }} + + + + + + +
+ {{ template "main" . }} +
+ + +{{ end }} diff --git a/internal/webui/html/index.html b/internal/webui/html/index.html deleted file mode 100644 index a0a7d82..0000000 --- a/internal/webui/html/index.html +++ /dev/null @@ -1,24 +0,0 @@ - - - - - {{ .Title }} - - - - - - -

Horaires des prochains trains à Crépieux la Pape

- - - - - - {{ range .Departures }} - - {{ end }} - -
Arrivée en gareDirection
{{ .Arrival }}{{ .DisplayName }}
- - diff --git a/internal/webui/html/login.html b/internal/webui/html/login.html new file mode 100644 index 0000000..33e9f02 --- /dev/null +++ b/internal/webui/html/login.html @@ -0,0 +1,13 @@ +{{ template "base" . }} + +{{ define "main" }} +
+ + + + + + + +
+{{ end }} diff --git a/internal/webui/html/root.html b/internal/webui/html/root.html new file mode 100644 index 0000000..a293ee7 --- /dev/null +++ b/internal/webui/html/root.html @@ -0,0 +1,15 @@ +{{ template "base" . }} + +{{ define "main" }} +

Horaires des prochains trains à Crépieux la Pape

+ + + + + + {{ range .Departures }} + + {{ end }} + +
Arrivée en gareDirection
{{ .Arrival }}{{ .DisplayName }}
+{{ end }} diff --git a/internal/webui/login.go b/internal/webui/login.go new file mode 100644 index 0000000..c0eb109 --- /dev/null +++ b/internal/webui/login.go @@ -0,0 +1,85 @@ +package webui + +import ( + "fmt" + "html/template" + "net/http" + "regexp" + + "git.adyxax.org/adyxax/trains/pkg/database" + "git.adyxax.org/adyxax/trains/pkg/model" +) + +const sessionCookieName = "session-trains-webui" + +var validUsername = regexp.MustCompile(`^[a-zA-Z]\w*$`) +var validPassword = regexp.MustCompile(`^.+$`) + +var loginTemplate = template.Must(template.ParseFS(templatesFS, "html/base.html", "html/login.html")) + +// The login handler of the webui +func loginHandler(e *env, w http.ResponseWriter, r *http.Request) error { + if r.URL.Path == "/login" { + _, err := tryAndResumeSession(e, r) + if err == nil { + // already logged in + http.Redirect(w, r, "/", http.StatusFound) + return nil + } + if r.Method == http.MethodPost { + r.ParseForm() + // username + username, ok := r.Form["username"] + if !ok { + return newStatusError(http.StatusBadRequest, fmt.Errorf("No username field in POST")) + } + if len(username) != 1 { + return newStatusError(http.StatusBadRequest, fmt.Errorf("Invalid multiple username fields in POST")) + } + if ok := validUsername.MatchString(username[0]); !ok { + return newStatusError(http.StatusBadRequest, fmt.Errorf("Invalid username field in POST")) + } + // password + password, ok := r.Form["password"] + if !ok { + return newStatusError(http.StatusBadRequest, fmt.Errorf("No password field in POST")) + } + if len(password) != 1 { + return newStatusError(http.StatusBadRequest, fmt.Errorf("Invalid multiple password fields in POST")) + } + if ok := validPassword.MatchString(password[0]); !ok { + return newStatusError(http.StatusBadRequest, fmt.Errorf("Invalid password field in POST")) + } + // try to login + user, err := e.dbEnv.Login(&model.UserLogin{Username: username[0], Password: password[0]}) + if err != nil { + switch e := err.(type) { + case *database.PasswordError: + // TODO : handle in page + return e + case *database.QueryError: + return e + default: + return e + } + } + token, err := e.dbEnv.CreateSession(user) + if err != nil { + return newStatusError(http.StatusInternalServerError, err) + } + cookie := http.Cookie{Name: sessionCookieName, Value: *token, Path: "/", HttpOnly: true, SameSite: http.SameSiteStrictMode, MaxAge: 3600000} + http.SetCookie(w, &cookie) + http.Redirect(w, r, "/", http.StatusFound) + return nil + } else { + p := Page{Title: "Login"} + err := loginTemplate.ExecuteTemplate(w, "login.html", p) + if err != nil { + return newStatusError(http.StatusInternalServerError, err) + } + return nil + } + } else { + return newStatusError(http.StatusNotFound, fmt.Errorf("Invalid path in loginHandler")) + } +} diff --git a/internal/webui/login_test.go b/internal/webui/login_test.go new file mode 100644 index 0000000..562095f --- /dev/null +++ b/internal/webui/login_test.go @@ -0,0 +1,136 @@ +package webui + +import ( + "net/http" + "net/url" + "testing" + + "git.adyxax.org/adyxax/trains/pkg/database" + "git.adyxax.org/adyxax/trains/pkg/model" + "github.com/stretchr/testify/require" +) + +func TestLoginHandler(t *testing.T) { + // test environment setup + dbEnv, err := database.InitDB("sqlite3", "file::memory:?_foreign_keys=on") + require.Nil(t, err) + err = dbEnv.Migrate() + require.Nil(t, err) + user1, err := dbEnv.CreateUser(&model.UserRegistration{Username: "user1", Password: "password1", Email: "julien@adyxax.org"}) + require.Nil(t, err) + _, err = dbEnv.Login(&model.UserLogin{Username: "user1", Password: "password1"}) + require.Nil(t, err) + token1, err := dbEnv.CreateSession(user1) + require.Nil(t, err) + e := &env{dbEnv: dbEnv} + // test GET requests + runHttpTest(t, e, loginHandler, &httpTestCase{ + name: "a simple get should display the login page", + input: httpTestInput{ + method: http.MethodGet, + path: "/login", + }, + expect: httpTestExpect{ + code: http.StatusOK, + bodyString: "