sklad: implement login and logout

So far there are no other pages, and nothing links to the logout.
This commit is contained in:
Přemysl Eric Janouch 2019-04-13 22:50:08 +02:00
parent f5790dbff9
commit bcfb9fbc2b
Signed by: p
GPG Key ID: A0420B94F92B9493
5 changed files with 207 additions and 12 deletions

25
sklad/base.tmpl Normal file
View File

@ -0,0 +1,25 @@
<!DOCTYPE html>
<html>
<head>
<title>{{ template "Title" }} - sklad</title>
<meta http-equiv=Content-Type content="text/html; charset=utf-8">
<style>
html, body { min-height: 100vh; }
body { padding: 1em; box-sizing: border-box;
margin: 0 auto; max-width: 50em;
border-left: 1px solid gray; border-right: 1px solid gray;
font-family: sans-serif; }
</style>
</head>
<body>
<h1>sklad</h1>
{{ if .LoggedIn }}
<form method=post action=/logout>
<input type=submit value="Odhlásit">
</form>
{{ end }}
{{ template "Content" . }}
</body>
</html>

6
sklad/container.tmpl Normal file
View File

@ -0,0 +1,6 @@
{{ define "Title" }}Přehled{{ end }}
{{ define "Content" }}
<p>TODO
{{ end }}

14
sklad/login.tmpl Normal file
View File

@ -0,0 +1,14 @@
{{ define "Title" }}Přihlášení{{ end }}
{{ define "Content" }}
<form method=post>
<label for=password>Heslo:</label>
<input type=password name=password id=password>
<input type=submit value="Přihlásit">
</form>
{{ if .IncorrectPassword }}
<p>Bylo zadáno nesprávné heslo.
{{ end }}
{{ end }}

View File

@ -2,21 +2,100 @@ package main
import (
"html/template"
"io"
"log"
"math/rand"
"net/http"
"os"
"path/filepath"
"time"
)
var (
templates *template.Template
// session storage: UUID -> net.SplitHostPort(http.Server.RemoteAddr)[0]
sessions = map[string]string{}
templates = map[string]*template.Template{}
)
func executeTemplate(name string, w io.Writer, data interface{}) {
if err := templates[name].Execute(w, data); err != nil {
panic(err)
}
}
func handleLogin(w http.ResponseWriter, r *http.Request) {
if err := r.ParseForm(); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
redirect := r.FormValue("redirect")
if redirect == "" {
redirect = "/"
}
session := sessionGet(w, r)
if session.LoggedIn {
http.Redirect(w, r, redirect, http.StatusSeeOther)
return
}
params := struct {
LoggedIn bool
IncorrectPassword bool
}{}
switch r.Method {
case http.MethodGet:
w.Header().Set("Cache-Control", "no-store")
case http.MethodPost:
if r.FormValue("password") == db.Password {
session.LoggedIn = true
http.Redirect(w, r, redirect, http.StatusSeeOther)
return
}
params.IncorrectPassword = true
default:
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
executeTemplate("login.tmpl", w, &params)
}
func handleLogout(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
session := r.Context().Value(sessionContextKey{}).(*Session)
session.LoggedIn = false
http.Redirect(w, r, "/", http.StatusSeeOther)
}
func handleContainer(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodGet {
w.WriteHeader(http.StatusMethodNotAllowed)
return
}
params := struct {
LoggedIn bool
}{
LoggedIn: true,
}
executeTemplate("container.tmpl", w, &params)
}
// TODO: Consider a wrapper function that automatically calls ParseForm
// and disables client-side caching.
func main() {
// Randomize the RNG for session string generation.
rand.Seed(time.Now().UnixNano())
if len(os.Args) != 3 {
log.Fatalf("usage: %s ADDRESS DATABASE\n", os.Args[0])
log.Fatalf("Usage: %s ADDRESS DATABASE-FILE\n", os.Args[0])
}
var address string
@ -28,26 +107,31 @@ func main() {
}
// Load HTML templates from the current working directory.
var err error
templates, err = template.ParseGlob("*.tmpl")
m, err := filepath.Glob("*.tmpl")
if err != nil {
log.Fatalln(err)
}
for _, name := range m {
templates[name] = template.Must(template.ParseFiles("base.tmpl", name))
}
// TODO: Eventually we will need to load a font file for label printing.
// - The path might be part of configuration, or implicit by filename.
// TODO: Some routing, don't forget about sessions.
// - https://stackoverflow.com/a/33880971/76313
// TODO: Some routing and pages.
//
// - GET /login
// - GET /container?id=UA1
// - GET /series?id=A
// - GET /search?q=bottle
//
// - POST /login?pass=hue
// - POST /logout
// - https://stackoverflow.com/a/33880971/76313
// - POST /label?id=UA1
http.HandleFunc("/", sessionWrap(handleContainer))
http.HandleFunc("/container", sessionWrap(handleContainer))
http.HandleFunc("/login", handleLogin)
http.HandleFunc("/logout", sessionWrap(handleLogout))
log.Fatalln(http.ListenAndServe(address, nil))
}

66
sklad/session.go Normal file
View File

@ -0,0 +1,66 @@
package main
import (
"context"
"encoding/hex"
"math/rand"
"net/http"
"net/url"
)
// session storage indexed by a random UUID
var sessions = map[string]*Session{}
type Session struct {
LoggedIn bool // may access the DB
}
type sessionContextKey struct{}
func sessionGenId() string {
u := make([]byte, 16)
if _, err := rand.Read(u); err != nil {
panic("cannot generate random bytes")
}
return hex.EncodeToString(u)
}
// TODO: We don't want to keep an unlimited amount of cookies in the storage.
// - The essential question is: how do we avoid DoS?
// - Which cookies are worth keeping?
// - Definitely logged-in users, only one person should know the password.
// - Evict by FIFO? LRU?
func sessionGet(w http.ResponseWriter, r *http.Request) (session *Session) {
if c, _ := r.Cookie("sessionid"); c != nil {
session, _ = sessions[c.Value]
}
if session == nil {
id := sessionGenId()
session = &Session{LoggedIn: false}
sessions[id] = session
http.SetCookie(w, &http.Cookie{Name: "sessionid", Value: id})
}
return
}
func sessionWrap(inner func(http.ResponseWriter, *http.Request)) func(
http.ResponseWriter, *http.Request) {
return func(w http.ResponseWriter, r *http.Request) {
// We might also try no-cache with an ETag for the whole database,
// though I don't expect any substantial improvements of anything.
w.Header().Set("Cache-Control", "no-store")
redirect := "/login"
if r.RequestURI != "/" {
redirect += "?redirect=" + url.QueryEscape(r.RequestURI)
}
session := sessionGet(w, r)
if !session.LoggedIn {
http.Redirect(w, r, redirect, http.StatusSeeOther)
return
}
inner(w, r.WithContext(
context.WithValue(r.Context(), sessionContextKey{}, session)))
}
}