Add the simplest possible web server to test login

This is a very rough test to actually check the login function outside
pgTAP; it is very ugly, in both design and code, and (i hope) does not
reflect future quality.

I was about to use Echo[0] as a “web framework”, but something feels
wrong when using a framework with Go—i do not know what.  I actually
tried it and was even more put off by the JSON-formatted logger that can
not be disabled; i was already losing control of the application!

I created the folder following the apparently de facto guidelines for Go
projects, and i see no problem with mixing Go’s folders with Sqitch’s:
both are part of the same application and there are not conflicts.

[0]: https://echo.labstack.com/
[1]: https://github.com/golang-standards/project-layout
This commit is contained in:
jordi fita mas 2023-01-13 20:43:42 +01:00
parent 2ec343beee
commit 9d202e82ca
6 changed files with 223 additions and 0 deletions

48
cmd/numerus/main.go Normal file
View File

@ -0,0 +1,48 @@
package main
import (
"context"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/jackc/pgx/v5/pgxpool"
"dev.tandem.ws/tandem/numerus/pkg/router"
)
func main() {
dbpool, err := pgxpool.New(context.Background(), os.Getenv("DATABASE_URL"))
if err != nil {
log.Fatal(err)
}
defer dbpool.Close()
srv := http.Server{
Addr: "localhost:8080",
Handler: router.NewRouter(dbpool),
ReadTimeout: 5 * time.Second,
WriteTimeout: 10 * time.Second,
IdleTimeout: 2 * time.Minute,
}
go func() {
log.Printf("INFO - listening on %s\n", srv.Addr)
if err := srv.ListenAndServe(); err != http.ErrServerClosed {
log.Fatalf("http server: %v", err)
}
}()
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
defer cancel()
log.Print("INFO - stopping server")
if err := srv.Shutdown(ctx); err != nil {
log.Fatal(err)
}
}

15
go.mod Normal file
View File

@ -0,0 +1,15 @@
module dev.tandem.ws/tandem/numerus
go 1.18
require github.com/jackc/pgx/v5 v5.2.0
require (
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
github.com/jackc/puddle/v2 v2.1.2 // indirect
go.uber.org/atomic v1.10.0 // indirect
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 // indirect
golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7 // indirect
golang.org/x/text v0.3.8 // indirect
)

27
go.sum Normal file
View File

@ -0,0 +1,27 @@
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM=
github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b h1:C8S2+VttkHFdOOCXJe+YGfa4vHYwlt4Zx+IVXQ97jYg=
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E=
github.com/jackc/pgx/v5 v5.2.0 h1:NdPpngX0Y6z6XDFKqmFQaE+bCtkqzvQIOt1wvBlAqs8=
github.com/jackc/pgx/v5 v5.2.0/go.mod h1:Ptn7zmohNsWEsdxRawMzk3gaKma2obW+NWTnKa0S4nk=
github.com/jackc/puddle/v2 v2.1.2 h1:0f7vaaXINONKTsxYDn4otOAiJanX/BMeAtY//BXqzlg=
github.com/jackc/puddle/v2 v2.1.2/go.mod h1:2lpufsF5mRHO6SuZkm0fNYxM6SWHfvyFj62KwNzgels=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90 h1:Y/gsMcFOcR+6S6f3YeMKl5g+dZMEWqcz5Czj/GWYbkM=
golang.org/x/crypto v0.0.0-20220829220503-c86fa9a7ed90/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4=
golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7 h1:ZrnxWX62AgTKOSagEqxvb3ffipvEDX2pl7E1TdqLqIc=
golang.org/x/sync v0.0.0-20220923202941-7f9b1623fab7/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/text v0.3.8 h1:nAL+RVCQ9uMn3vJZbV+MRnydTJFPf8qqY42YiA6MrqY=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=

60
pkg/logger/middleware.go Normal file
View File

@ -0,0 +1,60 @@
package logger
import (
"log"
"net/http"
"time"
)
type loggerResponseWriter struct {
http.ResponseWriter
statusCode int
responseSize int
}
func (w *loggerResponseWriter) WriteHeader(statusCode int) {
w.statusCode = statusCode
w.ResponseWriter.WriteHeader(statusCode)
}
func (w *loggerResponseWriter) Write(b []byte) (int, error) {
w.responseSize += len(b)
return w.ResponseWriter.Write(b)
}
func (w *loggerResponseWriter) Flush() {
if f, ok := w.ResponseWriter.(http.Flusher); ok {
f.Flush()
}
}
func Logger(handler http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
t := time.Now()
logger := loggerResponseWriter{w, 0, 0}
handler.ServeHTTP(&logger, r)
statusCode := logger.statusCode
if statusCode == 0 {
statusCode = http.StatusOK
}
referer := r.Referer()
if referer == "" {
referer = "-"
}
log.Printf("HTTP - %s - - [%s] \"%s %s %s\" %d %d \"%s\" \"%s\" %s\n",
r.RemoteAddr,
t.Format("02/Jan/2006:15:04:05 -0700"),
r.Method,
r.URL.Path,
r.Proto,
statusCode,
logger.responseSize,
referer,
r.UserAgent(),
time.Since(t),
)
})
}

52
pkg/router/router.go Normal file
View File

@ -0,0 +1,52 @@
package router
import (
"context"
"html/template"
"log"
"net/http"
"dev.tandem.ws/tandem/numerus/pkg/logger"
"github.com/jackc/pgx/v5/pgxpool"
)
func NewRouter(db *pgxpool.Pool) http.Handler {
router := http.NewServeMux()
router.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
r.ParseForm()
email := r.FormValue("email")
password := r.FormValue("password")
var role string
if _, err := db.Exec(context.Background(), "select set_config('search_path', 'numerus, public', false)"); err != nil {
log.Printf("ERROR - %s", err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err := db.QueryRow(context.Background(), "select login($1, $2)", email, password).Scan(&role)
if err != nil {
log.Printf("ERROR - %s", err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.WriteHeader(http.StatusOK)
w.Write([]byte(role))
})
router.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
t, err := template.ParseFiles("web/template/index.html")
if err != nil {
log.Printf("ERROR - %s", err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
err = t.Execute(w, nil)
if err != nil {
log.Printf("ERROR - %s", err.Error())
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
})
var handler http.Handler = router
handler = logger.Logger(handler)
return handler
}

21
web/template/index.html Normal file
View File

@ -0,0 +1,21 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Numerus</title>
</head>
<body>
<h1>Numerus</h1>
<h2>Login</h2>
<form method="POST" action="/login">
<label for="user_email">Email</label>
<input id="user_email" type="email" required autofocus name="email" autocapitalize="none">
<label for="user_password">Password</label>
<input id="user_password" type="password" required name="password" autocomplete="current-password">
<button type="submit">Login</button>
</form>
</body>
</html>