aboutsummaryrefslogtreecommitdiff
path: root/sklad
diff options
context:
space:
mode:
authorPřemysl Janouch <p@janouch.name>2019-04-14 22:30:40 +0200
committerPřemysl Janouch <p@janouch.name>2019-04-14 22:30:40 +0200
commit1331f3b5642f521236fcb1ec21ee43d5b76c0b91 (patch)
tree1213c2f2014cf0eff5a4a881b02cbebf91a604b8 /sklad
parent7d9410c6b3a724e3670941f7ec2d00e7966d0b1a (diff)
downloadsklad-1331f3b5642f521236fcb1ec21ee43d5b76c0b91.tar.gz
sklad-1331f3b5642f521236fcb1ec21ee43d5b76c0b91.tar.xz
sklad-1331f3b5642f521236fcb1ec21ee43d5b76c0b91.zip
Move commands under cmd/
Diffstat (limited to 'sklad')
-rw-r--r--sklad/base.tmpl68
-rw-r--r--sklad/container.tmpl99
-rw-r--r--sklad/db.go225
-rw-r--r--sklad/label.tmpl13
-rw-r--r--sklad/login.tmpl17
-rw-r--r--sklad/main.go271
-rw-r--r--sklad/search.tmpl38
-rw-r--r--sklad/series.tmpl43
-rw-r--r--sklad/session.go66
9 files changed, 0 insertions, 840 deletions
diff --git a/sklad/base.tmpl b/sklad/base.tmpl
deleted file mode 100644
index d92a818..0000000
--- a/sklad/base.tmpl
+++ /dev/null
@@ -1,68 +0,0 @@
-<!DOCTYPE html>
-<html>
-<head>
- <title>{{ template "Title" . }} - sklad</title>
- <meta http-equiv=Content-Type content="text/html; charset=utf-8">
- <meta name=viewport content="width=device-width, initial-scale=1">
- <style>
- html, body { min-height: 100vh; }
- body { padding: 1rem; box-sizing: border-box;
- margin: 0 auto; max-width: 50rem;
- border-left: 1px solid #ccc; border-right: 1px solid #ccc;
- font-family: sans-serif; }
-
- section { border: 1px outset #ccc; padding: 0 .5rem; margin: 1rem 0; }
- section > p { margin: 0 0 .5rem 0; }
-
- header, footer { display: flex; justify-content: space-between;
- align-items: center; flex-wrap: wrap; padding-top: .5em; }
- header { margin: 0 -.5rem; padding: .5rem .5rem 0 .5rem;
- background: linear-gradient(0deg, transparent, #f8f8f8); }
- body > header { margin: -1rem -1rem 0 -1rem; padding: 1rem 1rem 0 1rem;
- background: linear-gradient(0deg, transparent, #eeeeee); }
-
- header *,
- footer * { display: inline-block; }
- header > *,
- footer > * { margin: 0 0 .5rem 0; }
- header > *:not(:last-child),
- footer > *:not(:last-child) { margin-right: .5rem; }
-
- header > h2,
- header > h3 { flex-grow: 1; }
-
- /* Don't ask me why this is an improvement on mobile browsers. */
- input[type=submit], input[type=text], input[type=password],
- select, textarea { border: 1px inset #ccc; padding: .25rem; }
- input[type=submit] { border-style: outset; }
- select { border-style: solid; }
-
- a { color: inherit; }
- textarea { padding: .5rem; box-sizing: border-box; width: 100%;
- font-family: inherit; resize: vertical; }
- select { max-width: 15rem; }
- </style>
-</head>
-<body>
-
-<header>
- <h1>sklad</h1>
-
-{{ block "HeaderControls" . }}
- <a href=/>Obaly</a>
- <a href=/series>Řady</a>
-
- <form method=get action=/search>
- <input type=text name=q autofocus><input type=submit value="Hledat">
- </form>
-
- <form method=post action=/logout>
- <input type=submit value="Odhlásit">
- </form>
-{{ end }}
-
-</header>
-
-{{ template "Content" . }}
-</body>
-</html>
diff --git a/sklad/container.tmpl b/sklad/container.tmpl
deleted file mode 100644
index 4bacae8..0000000
--- a/sklad/container.tmpl
+++ /dev/null
@@ -1,99 +0,0 @@
-{{ define "Title" }}{{/*
-*/}}{{ if .Container }}{{ .Container.Id }}{{ else }}Obaly{{ end }}{{ end }}
-{{ define "Content" }}
-
-{{ if .Container }}
-
-<section>
-<header>
- <h2>{{ .Container.Id }}</h2>
- <form method=post action="/label?id={{ .Container.Id }}">
- <input type=submit value="Vytisknout štítek">
- </form>
- <form method=post action="/?id={{ .Container.Id }}&amp;remove">
- <input type=submit value="Odstranit">
- </form>
-</header>
-
-<form method=post action="/?id={{ .Container.Id }}">
-<textarea name=description rows=5>
-{{ .Container.Description }}
-</textarea>
-<footer>
- <div>
- <label for=series>Řada:</label>
- <select name=series id=series>
-{{ range $prefix, $desc := .AllSeries }}
- <option value="{{ $prefix }}"
- {{ if eq $prefix $.Container.Series }}selected{{ end }}
- >{{ $prefix }} &mdash; {{ $desc }}</option>
-{{ end }}
- </select>
- </div>
- <div>
- <label for=parent>Nadobal:</label>
- <input type=text name=parent id=parent value="{{ .Container.Parent }}">
- </div>
- <input type=submit value="Uložit">
-</footer>
-</form>
-</section>
-
-<h2>Podobaly</h3>
-
-{{ else }}
-<section>
-<header>
- <h2>Nový obal</h2>
-</header>
-<form method=post action="/">
-<textarea name=description rows=5
- placeholder="Popis obalu nebo jeho obsahu"></textarea>
-<footer>
- <div>
- <label for=series>Řada:</label>
- <select name=series id=series>
-{{ range $prefix, $desc := .AllSeries }}
- <option value="{{ $prefix }}"
- >{{ $prefix }} &mdash; {{ $desc }}</option>
-{{ end }}
- </select>
- </div>
- <div>
- <label for=parent>Nadobal:</label>
- <input type=text name=parent id=parent value="">
- </div>
- <input type=submit value="Uložit">
-</footer>
-</form>
-</section>
-
-<h2>Obaly nejvyšší úrovně</h2>
-{{ end }}
-
-{{ range .Children }}
-<section>
-<header>
- <h3><a href="/?id={{ .Id }}">{{ .Id }}</a></h3>
- <form method=post action="/label?id={{ .Id }}">
- <input type=submit value="Vytisknout štítek">
- </form>
- <form method=post action="/?id={{ .Id }}&amp;remove">
- <input type=submit value="Odstranit">
- </form>
-</header>
-{{ if .Description }}
-<p>{{ .Description }}
-{{ end }}
-{{ if .Children }}
-<p>
-{{ range .Children }}
-<a href="/?id={{ .Id }}">{{ .Id }}</a>
-{{ end }}
-{{ end }}
-</section>
-{{ else }}
-<p>Obal je prázdný.
-{{ end }}
-
-{{ end }}
diff --git a/sklad/db.go b/sklad/db.go
deleted file mode 100644
index def18a5..0000000
--- a/sklad/db.go
+++ /dev/null
@@ -1,225 +0,0 @@
-package main
-
-import (
- "encoding/json"
- "errors"
- "fmt"
- "os"
- "strings"
- "time"
-
- "janouch.name/sklad/bdf"
-)
-
-type Series struct {
- Prefix string // PK: prefix
- Description string // what kind of containers this is for
-}
-
-type ContainerId string
-
-type Container struct {
- Series string // PK: what series does this belong to
- Number uint // PK: order within the series
- Parent ContainerId // the container we're in, if any, otherwise ""
- Description string // description and/or contents of this container
-}
-
-func (c *Container) Id() ContainerId {
- return ContainerId(fmt.Sprintf("%s%s%d", db.Prefix, c.Series, c.Number))
-}
-
-func (c *Container) Children() []*Container {
- // TODO: Sort this by Id, or maybe even return a map[string]*Container,
- // text/template would sort that automatically.
- return indexChildren[c.Id()]
-}
-
-func (c *Container) Path() (result []ContainerId) {
- for c != nil && c.Parent != "" {
- c = indexContainer[c.Parent]
- result = append(result, c.Id())
- }
- return
-}
-
-type Database struct {
- Password string // password for web users
- Prefix string // prefix for all container IDs
- Series []*Series // all known series
- Containers []*Container // all known containers
-
- BDFPath string // path to bitmap font file
- BDFScale int // integer scaling for the bitmap font
-}
-
-var (
- dbPath string
- db Database
- dbLast Database
- dbLog *os.File
-
- indexSeries = map[string]*Series{}
- indexContainer = map[ContainerId]*Container{}
- indexChildren = map[ContainerId][]*Container{}
-
- labelFont *bdf.Font
-)
-
-// TODO: Some functions to add, remove and change things in the database.
-// Indexes must be kept valid, just like any invariants.
-
-func dbSearchSeries(query string) (result []*Series) {
- query = strings.ToLower(query)
- added := map[string]bool{}
- for _, s := range db.Series {
- if query == strings.ToLower(s.Prefix) {
- result = append(result, s)
- added[s.Prefix] = true
- }
- }
- for _, s := range db.Series {
- if strings.Contains(
- strings.ToLower(s.Description), query) && !added[s.Prefix] {
- result = append(result, s)
- }
- }
- return
-}
-
-func dbSearchContainers(query string) (result []*Container) {
- query = strings.ToLower(query)
- added := map[ContainerId]bool{}
- for id, c := range indexContainer {
- if query == strings.ToLower(string(id)) {
- result = append(result, c)
- added[id] = true
- }
- }
- for id, c := range indexContainer {
- if strings.Contains(
- strings.ToLower(c.Description), query) && !added[id] {
- result = append(result, c)
- }
- }
- return
-}
-
-func dbCommit() error {
- // Write a timestamp.
- e := json.NewEncoder(dbLog)
- e.SetIndent("", " ")
- if err := e.Encode(time.Now().Format(time.RFC3339)); err != nil {
- return err
- }
-
- // Back up the current database contents.
- if err := e.Encode(&dbLast); err != nil {
- return err
- }
- if err := dbLog.Sync(); err != nil {
- return err
- }
-
- // Atomically replace the current database file.
- tempPath := dbPath + ".new"
- temp, err := os.OpenFile(tempPath, os.O_WRONLY|os.O_CREATE, 0644)
- if err != nil {
- return err
- }
- defer temp.Close()
-
- e = json.NewEncoder(temp)
- e.SetIndent("", " ")
- if err := e.Encode(&db); err != nil {
- return err
- }
-
- if err := os.Rename(tempPath, dbPath); err != nil {
- return err
- }
-
- dbLast = db
- return nil
-}
-
-// loadDatabase loads the database from a simple JSON file. We do not use
-// any SQL stuff or even external KV storage because there is no real need
-// for our trivial use case, with our general amount of data.
-func loadDatabase() error {
- dbFile, err := os.Open(dbPath)
- if err != nil {
- return err
- }
- if err := json.NewDecoder(dbFile).Decode(&db); err != nil {
- return err
- }
-
- // Further validate the database.
- if db.Prefix == "" {
- return errors.New("misconfigured prefix")
- }
-
- // Construct indexes for primary keys, validate against duplicates.
- for _, pv := range db.Series {
- if _, ok := indexSeries[pv.Prefix]; ok {
- return fmt.Errorf("duplicate series: %s", pv.Prefix)
- }
- indexSeries[pv.Prefix] = pv
- }
- for _, pv := range db.Containers {
- id := pv.Id()
- if _, ok := indexContainer[id]; ok {
- return fmt.Errorf("duplicate container: %s", id)
- }
- indexContainer[id] = pv
- }
-
- // Construct an index that goes from parent containers to their children.
- for _, pv := range db.Containers {
- if pv.Parent != "" {
- if _, ok := indexContainer[pv.Parent]; !ok {
- return fmt.Errorf("container %s has a nonexistent parent %s",
- pv.Id(), pv.Parent)
- }
- }
- indexChildren[pv.Parent] = append(indexChildren[pv.Parent], pv)
- }
-
- // Validate that no container is a parent of itself on any level.
- // This could probably be optimized but it would stop being obvious.
- for _, pv := range db.Containers {
- parents := map[ContainerId]bool{pv.Id(): true}
- for pv.Parent != "" {
- if parents[pv.Parent] {
- return fmt.Errorf("%s contains itself", pv.Parent)
- }
- parents[pv.Parent] = true
- pv = indexContainer[pv.Parent]
- }
- }
-
- // Prepare label printing.
- if db.BDFScale <= 0 {
- db.BDFScale = 1
- }
-
- if f, err := os.Open(db.BDFPath); err != nil {
- return fmt.Errorf("cannot load label font: %s", err)
- } else {
- defer f.Close()
- if labelFont, err = bdf.NewFromBDF(f); err != nil {
- return fmt.Errorf("cannot load label font: %s", err)
- }
- }
-
- // Open database log file for appending.
- if dbLog, err = os.OpenFile(dbPath+".log",
- os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644); err != nil {
- return err
- }
-
- // Remember the current state of the database.
- dbLast = db
- return nil
-}
diff --git a/sklad/label.tmpl b/sklad/label.tmpl
deleted file mode 100644
index da17c58..0000000
--- a/sklad/label.tmpl
+++ /dev/null
@@ -1,13 +0,0 @@
-{{ define "Title" }}Tisk štítku{{ end }}
-{{ define "Content" }}
-<h2>Tisk štítku pro {{ .Id }}</h2>
-
-{{ if .UnknownId }}
-<p>Neznámý obal.
-{{ else if .Error }}
-<p>Tisk selhal: {{ .Error }}
-{{ else }}
-<p>Tisk proběhl úspěšně.
-{{ end }}
-
-{{ end }}
diff --git a/sklad/login.tmpl b/sklad/login.tmpl
deleted file mode 100644
index c34ab53..0000000
--- a/sklad/login.tmpl
+++ /dev/null
@@ -1,17 +0,0 @@
-{{ define "Title" }}Přihlášení{{ end }}
-{{ define "HeaderControls" }}<!-- text/template requires content -->{{ end }}
-{{ define "Content" }}
-
-<h2>Přihlášení</h2>
-
-<form method=post>
-<label for=password>Heslo:</label>
-<input type=password name=password id=password autofocus
-><input type=submit value="Přihlásit">
-</form>
-
-{{ if .IncorrectPassword }}
-<p>Bylo zadáno nesprávné heslo.
-{{ end }}
-
-{{ end }}
diff --git a/sklad/main.go b/sklad/main.go
deleted file mode 100644
index 32dd68b..0000000
--- a/sklad/main.go
+++ /dev/null
@@ -1,271 +0,0 @@
-package main
-
-import (
- "errors"
- "html/template"
- "io"
- "log"
- "math/rand"
- "net/http"
- "os"
- "path/filepath"
- "time"
-
- "janouch.name/sklad/imgutil"
- "janouch.name/sklad/label"
- "janouch.name/sklad/ql"
-)
-
-var 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 wrap(inner func(http.ResponseWriter, *http.Request)) func(
- http.ResponseWriter, *http.Request) {
- return func(w http.ResponseWriter, r *http.Request) {
- if err := r.ParseForm(); err != nil {
- http.Error(w, err.Error(), http.StatusInternalServerError)
- return
- }
- if r.Method == http.MethodGet {
- w.Header().Set("Cache-Control", "no-store")
- }
- inner(w, r)
- }
-}
-
-func handleLogin(w http.ResponseWriter, r *http.Request) {
- redirect := r.FormValue("redirect")
- if redirect == "" {
- redirect = "/"
- }
-
- session := sessionGet(w, r)
- if session.LoggedIn {
- http.Redirect(w, r, redirect, http.StatusSeeOther)
- return
- }
-
- params := struct {
- IncorrectPassword bool
- }{}
-
- switch r.Method {
- case http.MethodGet:
- // We're just going to render the template.
- 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.MethodPost {
- // TODO
- }
- if r.Method != http.MethodGet {
- w.WriteHeader(http.StatusMethodNotAllowed)
- return
- }
-
- allSeries := map[string]string{}
- for _, s := range indexSeries {
- allSeries[s.Prefix] = s.Description
- }
-
- var container *Container
- children := []*Container{}
-
- if id := ContainerId(r.FormValue("id")); id == "" {
- children = indexChildren[""]
- } else if c, ok := indexContainer[id]; ok {
- children = c.Children()
- container = c
- }
-
- params := struct {
- Container *Container
- Children []*Container
- AllSeries map[string]string
- }{
- Container: container,
- Children: children,
- AllSeries: allSeries,
- }
-
- executeTemplate("container.tmpl", w, &params)
-}
-
-func handleSeries(w http.ResponseWriter, r *http.Request) {
- if r.Method == http.MethodPost {
- // TODO
- }
- if r.Method != http.MethodGet {
- w.WriteHeader(http.StatusMethodNotAllowed)
- return
- }
-
- allSeries := map[string]string{}
- for _, s := range indexSeries {
- allSeries[s.Prefix] = s.Description
- }
-
- prefix := r.FormValue("prefix")
- description := ""
-
- if prefix == "" {
- } else if series, ok := indexSeries[prefix]; ok {
- description = series.Description
- }
-
- params := struct {
- Prefix string
- Description string
- AllSeries map[string]string
- }{
- Prefix: prefix,
- Description: description,
- AllSeries: allSeries,
- }
-
- executeTemplate("series.tmpl", w, &params)
-}
-
-func handleSearch(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodGet {
- w.WriteHeader(http.StatusMethodNotAllowed)
- return
- }
-
- query := r.FormValue("q")
- params := struct {
- Query string
- Series []*Series
- Containers []*Container
- }{
- Query: query,
- Series: dbSearchSeries(query),
- Containers: dbSearchContainers(query),
- }
-
- executeTemplate("search.tmpl", w, &params)
-}
-
-func printLabel(id string) error {
- printer, err := ql.Open()
- if err != nil {
- return err
- }
- if printer == nil {
- return errors.New("no suitable printer found")
- }
- defer printer.Close()
-
- /*
- printer.StatusNotify = func(status *ql.Status) {
- log.Printf("\x1b[1mreceived status\x1b[m\n%+v\n%s",
- status[:], status)
- }
- */
-
- if err := printer.Initialize(); err != nil {
- return err
- }
- if err := printer.UpdateStatus(); err != nil {
- return err
- }
-
- mediaInfo := ql.GetMediaInfo(
- printer.LastStatus.MediaWidthMM(),
- printer.LastStatus.MediaLengthMM(),
- )
- if mediaInfo == nil {
- return errors.New("unknown media")
- }
-
- return printer.Print(&imgutil.LeftRotate{Image: label.GenLabelForHeight(
- labelFont, id, mediaInfo.PrintAreaPins, db.BDFScale)})
-}
-
-func handleLabel(w http.ResponseWriter, r *http.Request) {
- if r.Method != http.MethodPost {
- w.WriteHeader(http.StatusMethodNotAllowed)
- return
- }
-
- params := struct {
- Id string
- UnknownId bool
- Error error
- }{
- Id: r.FormValue("id"),
- }
-
- if c := indexContainer[ContainerId(params.Id)]; c == nil {
- params.UnknownId = true
- } else {
- params.Error = printLabel(params.Id)
- }
-
- executeTemplate("label.tmpl", w, &params)
-}
-
-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-FILE\n", os.Args[0])
- }
-
- var address string
- address, dbPath = os.Args[1], os.Args[2]
-
- // Load database.
- if err := loadDatabase(); err != nil {
- log.Fatalln(err)
- }
-
- // Load HTML templates from the current working directory.
- m, err := filepath.Glob("*.tmpl")
- if err != nil {
- log.Fatalln(err)
- }
- for _, name := range m {
- templates[name] = template.Must(template.ParseFiles("base.tmpl", name))
- }
-
- http.HandleFunc("/login", wrap(handleLogin))
- http.HandleFunc("/logout", sessionWrap(wrap(handleLogout)))
-
- http.HandleFunc("/", sessionWrap(wrap(handleContainer)))
- http.HandleFunc("/series", sessionWrap(wrap(handleSeries)))
- http.HandleFunc("/search", sessionWrap(wrap(handleSearch)))
- http.HandleFunc("/label", sessionWrap(wrap(handleLabel)))
-
- log.Fatalln(http.ListenAndServe(address, nil))
-}
diff --git a/sklad/search.tmpl b/sklad/search.tmpl
deleted file mode 100644
index cf704cf..0000000
--- a/sklad/search.tmpl
+++ /dev/null
@@ -1,38 +0,0 @@
-{{ define "Title" }}&bdquo;{{ .Query }}&ldquo; &mdash; Vyhledávání{{ end }}
-{{ define "Content" }}
-
-<h2>Vyhledávání: &bdquo;{{ .Query }}&ldquo;<h2>
-
-<h3>Řady</h3>
-
-{{ range .Series }}
-<section>
-<header>
- <h3><a href="/series?prefix={{ .Prefix }}">{{ .Prefix }}</a></h3>
- <p>{{ .Description }}
-</header>
-</section>
-{{ else }}
-<p>Neodpovídají žádné řady.
-{{ end }}
-
-<h3>Obaly</h3>
-
-{{ range .Containers }}
-<section>
-<header>
- <h3><a href="/?id={{ .Id }}">{{ .Id }}</a>
-{{ range .Path }}
- <small>&laquo; <a href="/?id={{ . }}">{{ . }}</a></small>
-{{ end }}
- </h3>
-</header>
-{{ if .Description }}
-<p>{{ .Description }}
-{{ end }}
-</section>
-{{ else }}
-<p>Neodpovídají žádné obaly.
-{{ end }}
-
-{{ end }}
diff --git a/sklad/series.tmpl b/sklad/series.tmpl
deleted file mode 100644
index 4956e3a..0000000
--- a/sklad/series.tmpl
+++ /dev/null
@@ -1,43 +0,0 @@
-{{ define "Title" }}{{ or .Prefix "Řady" }}{{ end }}
-{{ define "Content" }}
-
-{{ if .Prefix }}
-<h2>{{ .Prefix }}</h2>
-
-{{ if .Description }}
-<p>{{ .Description }}
-{{ end }}
-{{ else }}
-
-<section>
-<form method=post action="/series">
-<header>
- <h3>Nová řada</h3>
- <input type=text name=prefix placeholder="Prefix řady">
- <input type=text name=description placeholder="Popis řady"
- ><input type=submit value="Uložit">
- </form>
-</header>
-</form>
-</section>
-
-{{ range $prefix, $desc := .AllSeries }}
-<section>
-<header>
- <h3><a href="/series?prefix={{ $prefix }}">{{ $prefix }}</a></h3>
- <form method=post action="/series?prefix={{ $prefix }}">
- <input type=text name=description value="{{ $desc }}"
- ><input type=submit value="Uložit">
- </form>
- <form method=post action="/series?prefix={{ $prefix }}&amp;remove">
- <input type=submit value="Odstranit">
- </form>
-</header>
-</section>
-{{ else }}
-<p>Nejsou žádné řady.
-{{ end }}
-
-{{ end }}
-
-{{ end }}
diff --git a/sklad/session.go b/sklad/session.go
deleted file mode 100644
index 02fe0b0..0000000
--- a/sklad/session.go
+++ /dev/null
@@ -1,66 +0,0 @@
-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 != "/" && r.Method == http.MethodGet {
- 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)))
- }
-}