Browse Source

sklad: implement login and logout

So far there are no other pages, and nothing links to the logout.
master
Přemysl Janouch 1 year ago
parent
commit
bcfb9fbc2b
Signed by: Přemysl Janouch <p@janouch.name> GPG Key ID: A0420B94F92B9493
5 changed files with 207 additions and 12 deletions
  1. +25
    -0
      sklad/base.tmpl
  2. +6
    -0
      sklad/container.tmpl
  3. +14
    -0
      sklad/login.tmpl
  4. +96
    -12
      sklad/main.go
  5. +66
    -0
      sklad/session.go

+ 25
- 0
sklad/base.tmpl 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
- 0
sklad/container.tmpl View File

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

<p>TODO

{{ end }}

+ 14
- 0
sklad/login.tmpl 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 }}

+ 96
- 12
sklad/main.go 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
- 0
sklad/session.go 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)))
}
}

Loading…
Cancel
Save