aboutsummaryrefslogtreecommitdiff
path: root/internal/webui
diff options
context:
space:
mode:
authorJulien Dessaux2021-04-21 17:23:07 +0200
committerJulien Dessaux2021-04-21 17:23:07 +0200
commit4a2fb7e82d5d617298cb28b66485fc6f30c55781 (patch)
tree4ae55d8208e3ebf603f3a17ac56f6efd0e011efc /internal/webui
parentImplemented the ResumeSession function (diff)
downloadtrains-4a2fb7e82d5d617298cb28b66485fc6f30c55781.tar.gz
trains-4a2fb7e82d5d617298cb28b66485fc6f30c55781.tar.bz2
trains-4a2fb7e82d5d617298cb28b66485fc6f30c55781.zip
Reworked the webui package, added authentication feature and tests
Diffstat (limited to '')
-rw-r--r--internal/webui/app.go78
-rw-r--r--internal/webui/html/base.html18
-rw-r--r--internal/webui/html/index.html24
-rw-r--r--internal/webui/html/login.html13
-rw-r--r--internal/webui/html/root.html15
-rw-r--r--internal/webui/login.go85
-rw-r--r--internal/webui/login_test.go136
-rw-r--r--internal/webui/root.go58
-rw-r--r--internal/webui/root_test.go54
-rw-r--r--internal/webui/session.go19
-rw-r--r--internal/webui/utils.go60
-rw-r--r--internal/webui/utils_test.go71
-rw-r--r--internal/webui/webui.go25
13 files changed, 554 insertions, 102 deletions
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" }}
+<!doctype html>
+<html lang="en">
+ <head>
+ <meta charset="utf-8">
+ <title>{{ .Title }}</title>
+ <meta name="viewport" content="width=device-width, initial-scale=1">
+
+ <link rel="icon" type="image/png" href="/static/favicon.png" />
+ <link rel="stylesheet" href="/static/main.css">
+ </head>
+ <body>
+ <main>
+ {{ template "main" . }}
+ </main>
+ </body>
+</html>
+{{ 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 @@
-<!doctype html>
-<html lang="en">
- <head>
- <meta charset="utf-8">
- <title>{{ .Title }}</title>
- <meta name="viewport" content="width=device-width, initial-scale=1">
-
- <link rel="icon" type="image/png" href="/static/favicon.png" />
- <link rel="stylesheet" href="/static/main.css">
- </head>
- <body>
- <h3>Horaires des prochains trains à Crépieux la Pape</h3>
- <table>
- <thead>
- <tr><th>Arrivée en gare</th><th>Direction</th></tr>
- </thead>
- <tbody>
- {{ range .Departures }}
- <tr{{ if .Odd }} style="color:#111111;"{{ end }}><td>{{ .Arrival }}</td><td>{{ .DisplayName }}</td></tr>
- {{ end }}
- </tbody>
- </table>
- </body>
-</html>
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" }}
+<form action="/login" method="post">
+ <label for="username"><b>Username</b></label>
+ <input type="text" placeholder="Enter Username" name="username" required>
+
+ <label for="password"><b>Password</b></label>
+ <input type="password" placeholder="Enter Password" name="password" required>
+
+ <button type="submit">Login</button>
+</form>
+{{ 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" }}
+<h3>Horaires des prochains trains à Crépieux la Pape</h3>
+<table>
+ <thead>
+ <tr><th>Arrivée en gare</th><th>Direction</th></tr>
+ </thead>
+ <tbody>
+ {{ range .Departures }}
+ <tr{{ if .Odd }} style="color:#111111;"{{ end }}><td>{{ .Arrival }}</td><td>{{ .DisplayName }}</td></tr>
+ {{ end }}
+ </tbody>
+</table>
+{{ 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: "<form action=\"/login\"",
+ },
+ })
+ runHttpTest(t, e, loginHandler, &httpTestCase{
+ name: "an invalid or expired token should also just display the login page",
+ input: httpTestInput{
+ method: http.MethodGet,
+ path: "/login",
+ cookie: &http.Cookie{Name: sessionCookieName, Value: "graou"},
+ },
+ expect: httpTestExpect{
+ code: http.StatusOK,
+ bodyString: "<form action=\"/login\"",
+ },
+ })
+ runHttpTest(t, e, loginHandler, &httpTestCase{
+ name: "if already logged in we should be redirected to /",
+ input: httpTestInput{
+ method: http.MethodGet,
+ path: "/login",
+ cookie: &http.Cookie{Name: sessionCookieName, Value: *token1},
+ },
+ expect: httpTestExpect{
+ code: http.StatusFound,
+ location: "/",
+ },
+ })
+ runHttpTest(t, e, loginHandler, &httpTestCase{
+ name: "an invalid path should get a 404",
+ input: httpTestInput{
+ method: http.MethodGet,
+ path: "/login/non_existent",
+ },
+ expect: httpTestExpect{
+ code: http.StatusNotFound,
+ err: &statusError{http.StatusNotFound, simpleErrorMessage},
+ },
+ })
+ runHttpTest(t, e, loginHandler, &httpTestCase{
+ name: "an invalid path should get a 404 even if we are already logged in",
+ input: httpTestInput{
+ method: http.MethodGet,
+ path: "/login/non_existent",
+ cookie: &http.Cookie{Name: sessionCookieName, Value: *token1},
+ },
+ expect: httpTestExpect{
+ code: http.StatusNotFound,
+ err: &statusError{http.StatusNotFound, simpleErrorMessage},
+ },
+ })
+ // Test POST requests
+ runHttpTest(t, e, loginHandler, &httpTestCase{
+ name: "a valid login attempt should succeed and redirect to /",
+ input: httpTestInput{
+ method: http.MethodPost,
+ path: "/login",
+ data: url.Values{
+ "username": []string{"user1"},
+ "password": []string{"password1"},
+ },
+ },
+ expect: httpTestExpect{
+ code: http.StatusFound,
+ location: "/",
+ setsCookie: true,
+ },
+ })
+ //errorNoUsername := newTestRequest(t, http.MethodPost, "/login", nil)
+ //// too many username fields
+ //dataWtfUsername := url.Values{"username": []string{"user1", "user2"}}
+ //errorWtfUsername, err := http.NewRequest(http.MethodPost, "/login", strings.NewReader(dataWtfUsername.Encode()))
+ //require.Nil(t, err)
+ //errorWtfUsername.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+ //// Invalid username
+ //dataInvalidUsername := url.Values{"username": []string{"%"}}
+ //errorInvalidUsername, err := http.NewRequest(http.MethodPost, "/login", strings.NewReader(dataInvalidUsername.Encode()))
+ //require.Nil(t, err)
+ //errorInvalidUsername.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+ //// no password field
+ //dataNoPassword := url.Values{"username": []string{"user1"}}
+ //errorNoPassword, err := http.NewRequest(http.MethodPost, "/login", strings.NewReader(dataNoPassword.Encode()))
+ //require.Nil(t, err)
+ //errorNoPassword.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+ //// too many password fields
+ //dataWtfPassword := url.Values{"username": []string{"user1"}, "password": []string{"user1", "user2"}}
+ //errorWtfPassword, err := http.NewRequest(http.MethodPost, "/login", strings.NewReader(dataWtfPassword.Encode()))
+ //require.Nil(t, err)
+ //errorWtfPassword.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+ //// Invalid password
+ //dataInvalidPassword := url.Values{"username": []string{"user1"}, "password": []string{""}}
+ //errorInvalidPassword, err := http.NewRequest(http.MethodPost, "/login", strings.NewReader(dataInvalidPassword.Encode()))
+ //require.Nil(t, err)
+ //errorInvalidPassword.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+ //// run the tests
+ //// {"error no username", &env{dbEnv: dbEnv}, errorNoUsername, &expected{err: &statusError{code: 500, err: simpleError}}},
+ //// {"error wtf username", &env{dbEnv: dbEnv}, errorWtfUsername, &expected{err: &statusError{code: 500, err: simpleError}}},
+ //// {"error invalid username", &env{dbEnv: dbEnv}, errorInvalidUsername, &expected{err: &statusError{code: 500, err: simpleError}}},
+ //// {"error no password", &env{dbEnv: dbEnv}, errorNoPassword, &expected{err: &statusError{code: 500, err: simpleError}}},
+ //// {"error wtf password", &env{dbEnv: dbEnv}, errorWtfPassword, &expected{err: &statusError{code: 500, err: simpleError}}},
+ //// {"error invalid password", &env{dbEnv: dbEnv}, errorInvalidPassword, &expected{err: &statusError{code: 500, err: simpleError}}},
+ ////}
+}
diff --git a/internal/webui/root.go b/internal/webui/root.go
new file mode 100644
index 0000000..c3a0ec5
--- /dev/null
+++ b/internal/webui/root.go
@@ -0,0 +1,58 @@
+package webui
+
+import (
+ "fmt"
+ "html/template"
+ "log"
+ "net/http"
+ "time"
+
+ "git.adyxax.org/adyxax/trains/pkg/model"
+)
+
+var rootTemplate = template.Must(template.ParseFS(templatesFS, "html/base.html", "html/root.html"))
+
+// The page template variable
+type Page struct {
+ User *model.User
+ Departures []Departure
+ Title string
+}
+type Departure struct {
+ DisplayName string
+ Arrival string
+ Odd bool
+}
+
+// The root handler of the webui
+func rootHandler(e *env, w http.ResponseWriter, r *http.Request) error {
+ if r.URL.Path == "/" {
+ var p Page
+ user, err := tryAndResumeSession(e, r)
+ if err != nil {
+ http.Redirect(w, r, "/login", http.StatusFound)
+ return nil
+ }
+ p.User = user
+ if d, err := e.navitia.GetDepartures(e.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"
+ err = rootTemplate.ExecuteTemplate(w, "root.html", p)
+ if err != nil {
+ return newStatusError(http.StatusInternalServerError, err)
+ }
+ return nil
+ } else {
+ return newStatusError(http.StatusNotFound, fmt.Errorf("Invalid path in rootHandler"))
+ }
+}
diff --git a/internal/webui/root_test.go b/internal/webui/root_test.go
new file mode 100644
index 0000000..8faee77
--- /dev/null
+++ b/internal/webui/root_test.go
@@ -0,0 +1,54 @@
+package webui
+
+import (
+ "net/http"
+ "testing"
+
+ "git.adyxax.org/adyxax/trains/pkg/database"
+ "git.adyxax.org/adyxax/trains/pkg/model"
+ "github.com/stretchr/testify/require"
+)
+
+func TestRootHandler(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,
+ // TODO mock navitia
+ }
+ // test GET requests
+ runHttpTest(t, e, rootHandler, &httpTestCase{
+ name: "a simple get when not logged in should redirect to the login page",
+ input: httpTestInput{
+ method: http.MethodGet,
+ path: "/",
+ },
+ expect: httpTestExpect{
+ code: http.StatusFound,
+ location: "/login",
+ },
+ })
+ // TODO mock navitia
+ _ = token1
+ //runHttpTest(t, e, rootHandler, &httpTestCase{
+ // name: "a simple get when logged in should display the departure times",
+ // input: httpTestInput{
+ // method: http.MethodGet,
+ // path: "/",
+ // cookie: &http.Cookie{Name: sessionCookieName, Value: *token1},
+ // },
+ // expect: httpTestExpect{
+ // code: http.StatusOK,
+ // bodyString: "Horaires des prochains trains",
+ // },
+ //})
+}
diff --git a/internal/webui/session.go b/internal/webui/session.go
new file mode 100644
index 0000000..58256f2
--- /dev/null
+++ b/internal/webui/session.go
@@ -0,0 +1,19 @@
+package webui
+
+import (
+ "net/http"
+
+ "git.adyxax.org/adyxax/trains/pkg/model"
+)
+
+func tryAndResumeSession(e *env, r *http.Request) (*model.User, error) {
+ cookie, err := r.Cookie(sessionCookieName)
+ if err != nil {
+ return nil, err
+ }
+ user, err := e.dbEnv.ResumeSession(cookie.Value)
+ if err != nil {
+ return nil, err
+ }
+ return user, nil
+}
diff --git a/internal/webui/utils.go b/internal/webui/utils.go
new file mode 100644
index 0000000..7152401
--- /dev/null
+++ b/internal/webui/utils.go
@@ -0,0 +1,60 @@
+package webui
+
+import (
+ "embed"
+ "log"
+ "net/http"
+
+ "git.adyxax.org/adyxax/trains/pkg/config"
+ "git.adyxax.org/adyxax/trains/pkg/database"
+ "git.adyxax.org/adyxax/trains/pkg/navitia_api_client"
+)
+
+//go:embed html/*
+var templatesFS embed.FS
+
+//go:embed static/*
+var staticFS embed.FS
+
+// the environment that will be passed to our handlers
+type env struct {
+ conf *config.Config
+ dbEnv *database.DBEnv
+ navitia *navitia_api_client.Client
+}
+
+type handlerError interface {
+ error
+ Status() int
+}
+
+type statusError struct {
+ code int
+ err error
+}
+
+func (e *statusError) Error() string { return e.err.Error() }
+func (e *statusError) Status() int { return e.code }
+func newStatusError(code int, err error) error { return &statusError{code: code, err: err} }
+
+type handler struct {
+ e *env
+ h func(e *env, w http.ResponseWriter, r *http.Request) error
+}
+
+// ServeHTTP allows our handler type to satisfy http.Handler
+func (h handler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
+ path := r.URL.Path
+ err := h.h(h.e, w, r)
+ if err != nil {
+ switch e := err.(type) {
+ case handlerError:
+ log.Printf("HTTP %d - %s", e.Status(), e)
+ http.Error(w, e.Error(), e.Status())
+ default:
+ // Any error types we don't specifically look out for default to serving a HTTP 500
+ log.Printf("%s : handler returned an unexpected error : %+v", path, e)
+ http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
+ }
+ }
+}
diff --git a/internal/webui/utils_test.go b/internal/webui/utils_test.go
new file mode 100644
index 0000000..1e07aa2
--- /dev/null
+++ b/internal/webui/utils_test.go
@@ -0,0 +1,71 @@
+package webui
+
+import (
+ "fmt"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "reflect"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+var simpleErrorMessage = fmt.Errorf("")
+
+type httpTestCase struct {
+ name string
+ input httpTestInput
+ expect httpTestExpect
+}
+type httpTestInput struct {
+ method string
+ path string
+ cookie *http.Cookie
+ data url.Values
+}
+type httpTestExpect struct {
+ code int
+ bodyString string
+ location string
+ setsCookie bool
+ err interface{}
+}
+
+func runHttpTest(t *testing.T, e *env, h func(e *env, w http.ResponseWriter, r *http.Request) error, tc *httpTestCase) {
+ req, err := http.NewRequest(tc.input.method, tc.input.path, nil)
+ require.Nil(t, err)
+ if tc.input.data != nil {
+ req, err = http.NewRequest(tc.input.method, tc.input.path, strings.NewReader(tc.input.data.Encode()))
+ req.Header.Add("Content-Type", "application/x-www-form-urlencoded")
+ }
+ if tc.input.cookie != nil {
+ req.AddCookie(tc.input.cookie)
+ }
+ t.Run(tc.name, func(t *testing.T) {
+ rr := httptest.NewRecorder()
+ err := h(e, rr, req)
+ if tc.expect.err != nil {
+ require.Error(t, err)
+ assert.Equalf(t, reflect.TypeOf(err), reflect.TypeOf(tc.expect.err), "Invalid error type. Got %s but expected %s", reflect.TypeOf(err), reflect.TypeOf(tc.expect.err))
+ } else {
+ require.NoError(t, err)
+ assert.Equal(t, tc.expect.code, rr.Code)
+ if tc.expect.bodyString != "" {
+ assert.Contains(t, rr.Body.String(), tc.expect.bodyString)
+ }
+ if tc.expect.location != "" {
+ assert.Contains(t, rr.HeaderMap, "Location")
+ assert.Len(t, rr.HeaderMap["Location"], 1)
+ assert.Equal(t, rr.HeaderMap["Location"][0], tc.expect.location)
+ }
+ if tc.expect.setsCookie {
+ assert.Contains(t, rr.HeaderMap, "Set-Cookie")
+ } else {
+ assert.NotContains(t, rr.HeaderMap, "Set-Cookie")
+ }
+ }
+ })
+}
diff --git a/internal/webui/webui.go b/internal/webui/webui.go
new file mode 100644
index 0000000..6ce5bb4
--- /dev/null
+++ b/internal/webui/webui.go
@@ -0,0 +1,25 @@
+package webui
+
+import (
+ "log"
+ "net/http"
+
+ "git.adyxax.org/adyxax/trains/pkg/config"
+ "git.adyxax.org/adyxax/trains/pkg/database"
+ "git.adyxax.org/adyxax/trains/pkg/navitia_api_client"
+)
+
+func Run(c *config.Config, dbEnv *database.DBEnv) {
+ e := env{
+ conf: c,
+ dbEnv: dbEnv,
+ navitia: navitia_api_client.NewClient(c.Token),
+ }
+ http.Handle("/", handler{&e, rootHandler})
+ http.Handle("/login", handler{&e, loginHandler})
+ http.Handle("/static/", http.FileServer(http.FS(staticFS)))
+
+ listenStr := c.Address + ":" + c.Port
+ log.Printf("Starting webui on %s", listenStr)
+ log.Fatal(http.ListenAndServe(listenStr, nil))
+}