sklad: implement login and logout
So far there are no other pages, and nothing links to the logout.
This commit is contained in:
parent
f5790dbff9
commit
bcfb9fbc2b
|
@ -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>
|
|
@ -0,0 +1,6 @@
|
|||
{{ define "Title" }}Přehled{{ end }}
|
||||
{{ define "Content" }}
|
||||
|
||||
<p>TODO
|
||||
|
||||
{{ end }}
|
|
@ -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 }}
|
108
sklad/main.go
108
sklad/main.go
|
@ -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, ¶ms)
|
||||
}
|
||||
|
||||
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, ¶ms)
|
||||
}
|
||||
|
||||
// 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))
|
||||
}
|
||||
|
|
|
@ -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)))
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue