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 (
|
import (
|
||||||
"html/template"
|
"html/template"
|
||||||
|
"io"
|
||||||
"log"
|
"log"
|
||||||
|
"math/rand"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
templates *template.Template
|
templates = map[string]*template.Template{}
|
||||||
|
|
||||||
// session storage: UUID -> net.SplitHostPort(http.Server.RemoteAddr)[0]
|
|
||||||
sessions = map[string]string{}
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
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() {
|
func main() {
|
||||||
|
// Randomize the RNG for session string generation.
|
||||||
|
rand.Seed(time.Now().UnixNano())
|
||||||
|
|
||||||
if len(os.Args) != 3 {
|
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
|
var address string
|
||||||
|
@ -28,26 +107,31 @@ func main() {
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load HTML templates from the current working directory.
|
// Load HTML templates from the current working directory.
|
||||||
var err error
|
m, err := filepath.Glob("*.tmpl")
|
||||||
templates, err = template.ParseGlob("*.tmpl")
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalln(err)
|
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.
|
// TODO: Eventually we will need to load a font file for label printing.
|
||||||
// - The path might be part of configuration, or implicit by filename.
|
// - The path might be part of configuration, or implicit by filename.
|
||||||
|
|
||||||
// TODO: Some routing, don't forget about sessions.
|
// TODO: Some routing and pages.
|
||||||
// - https://stackoverflow.com/a/33880971/76313
|
|
||||||
//
|
//
|
||||||
// - GET /login
|
|
||||||
// - GET /container?id=UA1
|
// - GET /container?id=UA1
|
||||||
// - GET /series?id=A
|
// - GET /series?id=A
|
||||||
// - GET /search?q=bottle
|
// - GET /search?q=bottle
|
||||||
//
|
//
|
||||||
// - POST /login?pass=hue
|
// - https://stackoverflow.com/a/33880971/76313
|
||||||
// - POST /logout
|
|
||||||
// - POST /label?id=UA1
|
// - 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))
|
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