Compare commits
74 commits
| Author | SHA1 | Date | |
|---|---|---|---|
| cb611fbe3e | |||
| db6d76769b | |||
| 8675bc4b7a | |||
| 824f46979b | |||
| 204aec8135 | |||
| c9aa60ca28 | |||
| cff4b58f6d | |||
| 1410847c03 | |||
| 1ce33aa89a | |||
| 8e748aaa90 | |||
| 76360ddbc2 | |||
| 58caf0527e | |||
| 312005632a | |||
| a3dd0183a4 | |||
| 82fd34b288 | |||
| 0a55030ae5 | |||
| 320dc096dd | |||
| 0265d820fe | |||
| b1028b3d8a | |||
| f6dbdcf460 | |||
| d29ec19abc | |||
| 9ecaf33d86 | |||
| 94ee84414c | |||
| 94762c62e4 | |||
| d9d089be0d | |||
| 8143e484cd | |||
| 4812a60367 | |||
| c8adfd2ef9 | |||
| 1b2a24a901 | |||
| a8a4771ca4 | |||
| 0c1f6fe206 | |||
| 5e976064e1 | |||
| 98822b8e1b | |||
| c4ad04fd18 | |||
| 6e317eb08b | |||
| 9424084a36 | |||
| 16ea95bf09 | |||
| 6a2dc8146c | |||
| 799ff0fb2f | |||
| 72d00aeb21 | |||
| d8c4bef3a3 | |||
| 4e5d554105 | |||
| 2e70a95bcd | |||
| c28249e1ce | |||
| 095fb1573b | |||
| ab454f4b99 | |||
| 61fcf2dcfe | |||
| f90e8b84b2 | |||
| 35448db725 | |||
| d362159693 | |||
| ddb11f0619 | |||
| bac08dc7cf | |||
| 1681a2b4a0 | |||
| 375933c7bc | |||
| 62543e44da | |||
| 78680de687 | |||
| 16671b3406 | |||
| 8ef2a2547c | |||
| de20ea3a2a | |||
| 8d3187b160 | |||
| d4971cda89 | |||
| 62ff59957a | |||
| 97d914bce4 | |||
| 2a203f6f29 | |||
| 575a41127f | |||
| 3fb1cfc17d | |||
| 85f93bae69 | |||
| 14e0611f06 | |||
| 9d332bbda4 | |||
| 7e77fd7d27 | |||
| 07ff5ccffb | |||
| 38f7c31517 | |||
| c5d41b0965 | |||
| 2b56832843 |
1371 changed files with 521417 additions and 281491 deletions
5
.gitignore
vendored
5
.gitignore
vendored
|
|
@ -1 +1,4 @@
|
|||
gowiki
|
||||
/gowiki
|
||||
tmp
|
||||
run.sh
|
||||
TODO.txt
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
Copyright (C) 2016 Marius Schellenberger
|
||||
Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
This program is free software: you can redistribute it and/or modify
|
||||
it under the terms of the GNU General Public License as published by
|
||||
|
|
|
|||
232
Godeps/Godeps.json
generated
232
Godeps/Godeps.json
generated
|
|
@ -1,232 +0,0 @@
|
|||
{
|
||||
"ImportPath": "git.giftfish.de/ston1th/gowiki",
|
||||
"GoVersion": "go1.7",
|
||||
"GodepVersion": "v74",
|
||||
"Deps": [
|
||||
{
|
||||
"ImportPath": "git.giftfish.de/ston1th/godrop",
|
||||
"Comment": "v1.0",
|
||||
"Rev": "8666e4461c9612e1432f05af6e0abfd716adca36"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/analysis",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/analysis/analyzers/standard_analyzer",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/analysis/char_filters/html_char_filter",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/analysis/char_filters/regexp_char_filter",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/analysis/datetime_parsers/datetime_optional",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/analysis/datetime_parsers/flexible_go",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/analysis/language/en",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/analysis/token_filters/lower_case_filter",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/analysis/token_filters/porter",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/analysis/token_filters/stop_tokens_filter",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/analysis/tokenizers/unicode",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/document",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/index",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/index/store",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/index/store/boltdb",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/index/store/gtreap",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/index/upside_down",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/numeric_util",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/registry",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/search",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/search/collectors",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/search/facets",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/search/highlight",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/search/highlight/fragment_formatters/html",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/search/highlight/fragmenters/simple",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/search/highlight/highlighters/html",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/search/highlight/highlighters/simple",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/search/scorers",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/bleve/search/searchers",
|
||||
"Comment": "v0.5.0",
|
||||
"Rev": "97393d027342f43b17da2f02c090a4e728ac929f"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/go-porterstemmer",
|
||||
"Comment": "v1.0.1-9-g23a2c8e",
|
||||
"Rev": "23a2c8e5cf1f380f27722c6d2ae8896431dc7d0e"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/blevesearch/segment",
|
||||
"Rev": "762005e7a34fd909a84586299f1dd457371d36ee"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/boltdb/bolt",
|
||||
"Comment": "v1.3.0",
|
||||
"Rev": "583e8937c61f1af6513608ccc75c97b6abdf4ff9"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/golang/protobuf/proto",
|
||||
"Rev": "7cc19b78d562895b13596ddce7aafb59dd789318"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/gorilla/context",
|
||||
"Comment": "v1.1",
|
||||
"Rev": "1ea25387ff6f684839d82767c1733ff4d4d15d0a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/gorilla/mux",
|
||||
"Comment": "v1.1",
|
||||
"Rev": "0eeaf8392f5b04950925b8a69fe70f110fa7cbfc"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/gorilla/securecookie",
|
||||
"Comment": "v1.1",
|
||||
"Rev": "667fe4e3466a040b780561fe9b51a83a3753eefc"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/gorilla/sessions",
|
||||
"Rev": "f7261893ca3ea922c30eabe742c036d2c1de6e0a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/russross/blackfriday",
|
||||
"Comment": "v1.4-31-g2004188",
|
||||
"Rev": "2004188462c3946efefbdcd91aa83e49b4175bfb"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/shurcooL/sanitized_anchor_name",
|
||||
"Rev": "10ef21a441db47d8b13ebcc5fd2310f636973c77"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/steveyen/gtreap",
|
||||
"Rev": "0abe01ef9be25c4aedc174758ec2d917314d6d70"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/crypto/bcrypt",
|
||||
"Rev": "9477e0b78b9ac3d0b03822fd95422e2fe07627cd"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/crypto/blowfish",
|
||||
"Rev": "9477e0b78b9ac3d0b03822fd95422e2fe07627cd"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/crypto/sha3",
|
||||
"Rev": "9477e0b78b9ac3d0b03822fd95422e2fe07627cd"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/net/context",
|
||||
"Rev": "55cccaa02af1a99c69ba3213e33468628b61be4b"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/sys/unix",
|
||||
"Rev": "a646d33e2ee3172a661fc09bca23bb4889a41bc8"
|
||||
}
|
||||
]
|
||||
}
|
||||
5
Godeps/Readme
generated
5
Godeps/Readme
generated
|
|
@ -1,5 +0,0 @@
|
|||
This directory tree is generated automatically by godep.
|
||||
|
||||
Please do not edit.
|
||||
|
||||
See https://github.com/tools/godep for more information.
|
||||
57
Makefile
57
Makefile
|
|
@ -1,32 +1,59 @@
|
|||
CC=go
|
||||
BUILD=build -v
|
||||
LDFLAGS=--ldflags "-s -w"
|
||||
BUILD=build -v -trimpath
|
||||
VERSION=$(shell cat VERSION)
|
||||
GCFLAGS=-gcflags '-e'
|
||||
LDFLAGS=-ldflags '-buildid=$(VERSION) -X main.version=$(VERSION) -s -w'
|
||||
PROGRAM=gowiki
|
||||
CMD=-o $(PROGRAM) cmd/gowiki/main.go
|
||||
ENV=GOGC=0 CGO_ENABLED=0 GO111MODULE=on
|
||||
|
||||
all: $(PROGRAM)
|
||||
|
||||
setup:
|
||||
$(CC) get github.com/tools/godep
|
||||
$(CC) get github.com/client9/misspell
|
||||
$(CC) install github.com/client9/misspell/cmd/misspell@latest
|
||||
$(CC) install honnef.co/go/tools/cmd/staticcheck@latest
|
||||
|
||||
$(PROGRAM): clean codeqa
|
||||
CGO_ENABLED=0 $(CC) $(BUILD) $(LDFLAGS)
|
||||
release: clean gofmt codeqa
|
||||
$(ENV) $(CC) $(BUILD) $(GCFLAGS) $(LDFLAGS) $(CMD)
|
||||
|
||||
release-vendor: clean gofmt codeqa
|
||||
$(ENV) $(CC) $(BUILD) -mod=vendor $(GCFLAGS) $(LDFLAGS) $(CMD)
|
||||
|
||||
vendor: clean gofmt
|
||||
$(ENV) $(CC) $(BUILD) -mod=vendor $(GCFLAGS) $(LDFLAGS) $(CMD)
|
||||
|
||||
$(PROGRAM): clean gofmt
|
||||
$(ENV) $(CC) $(BUILD) $(GCFLAGS) $(LDFLAGS) $(CMD)
|
||||
|
||||
clean:
|
||||
$(CC) clean -x
|
||||
|
||||
codeqa: templates gofmt misspell golint
|
||||
|
||||
gofmt:
|
||||
gofmt -w .
|
||||
$(CC) fmt ./...
|
||||
|
||||
golint:
|
||||
$(GOPATH)/bin/golint .
|
||||
codeqa: govet misspell staticcheck test
|
||||
|
||||
govet:
|
||||
$(CC) vet ./...
|
||||
|
||||
update:
|
||||
$(ENV) $(CC) get -u ./...
|
||||
|
||||
misspell:
|
||||
$(GOPATH)/bin/misspell **/*
|
||||
$(GOPATH)/bin/misspell cmd/* pkg/* Makefile README.md sri.sh
|
||||
|
||||
templates:
|
||||
$(CC) generate
|
||||
staticcheck:
|
||||
$(GOPATH)/bin/staticcheck ./...
|
||||
|
||||
.PHONY: setup build clean templates gofmt misspell golint
|
||||
test:
|
||||
ifeq ($(shell go env GOARCH), $(shell go env GOHOSTARCH))
|
||||
ifeq ($(shell go env GOOS), $(shell go env GOHOSTOS))
|
||||
$(CC) test ./...
|
||||
else
|
||||
$(info skipping tests of other platforms)
|
||||
endif
|
||||
else
|
||||
$(info skipping tests of other platforms)
|
||||
endif
|
||||
|
||||
.PHONY: setup release release-vendor vendor build clean codeqa gofmt govet misspell staticcheck test
|
||||
|
|
|
|||
60
README.md
60
README.md
|
|
@ -1 +1,61 @@
|
|||
# GoWiki - a simple wiki engine written in Go
|
||||
|
||||
## Building
|
||||
|
||||
**Requirements:**
|
||||
|
||||
* Go (golang) >= 1.16
|
||||
|
||||
Linux:
|
||||
|
||||
```
|
||||
make
|
||||
|
||||
or offline
|
||||
|
||||
make vendor
|
||||
```
|
||||
|
||||
OpenBSD:
|
||||
|
||||
```
|
||||
GOOS=openbsd make
|
||||
|
||||
or offline
|
||||
|
||||
GOOS=openbsd make vendor
|
||||
```
|
||||
|
||||
## Install
|
||||
|
||||
See the `scripts/` directory on how to install gowiki on Linux or OpenBSD.
|
||||
|
||||
## Config
|
||||
|
||||
```
|
||||
{
|
||||
"data_dir": "/var/lib/gowiki", // running directory. data will be stored here
|
||||
"user": "wiki", // drop privileges to user
|
||||
"group": "wiki", // drop privileges to group
|
||||
"listen_addr": "127.0.0.1:8080", // listening <address>:<port>
|
||||
"log_file": "gowiki.log", // log file (use - for stdout, this only works in combination with 'foreground: true')
|
||||
"secret": "", // static hmac secret (at least 64 chars, used for JWT signing)
|
||||
"admin": true, // set to false to disable the default admin user
|
||||
"foreground": false, // do not fork into the background
|
||||
"secure_cookie": false, // enable secure cookie flag
|
||||
"debug": false // enable debugging
|
||||
}
|
||||
```
|
||||
|
||||
## Features
|
||||
|
||||
* Fulltext indexing of wiki pages
|
||||
* Password hashing using BCrypt
|
||||
* 2FA using TOTP
|
||||
* Content Security Policy
|
||||
* No Javascript (yet)
|
||||
* Private wiki pages
|
||||
|
||||
### GoWiki on OpenBSD
|
||||
|
||||
GoWiki makes use of the OpenBSD native security features [pledge(2)](https://man.openbsd.org/pledge) and [unveil(2)](https://man.openbsd.org/unveil).
|
||||
|
|
|
|||
1
VERSION
Normal file
1
VERSION
Normal file
|
|
@ -0,0 +1 @@
|
|||
v2.0
|
||||
11
cmd/gowiki/main.go
Normal file
11
cmd/gowiki/main.go
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package main
|
||||
|
||||
import "git.giftfish.de/ston1th/gowiki/pkg/cmd"
|
||||
|
||||
var version string
|
||||
|
||||
func main() {
|
||||
cmd.Run(version)
|
||||
}
|
||||
250
context.go
250
context.go
|
|
@ -1,250 +0,0 @@
|
|||
// Copyright (C) 2017 Marius Schellenberger
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"crypto/subtle"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"github.com/gorilla/mux"
|
||||
"github.com/gorilla/sessions"
|
||||
"html/template"
|
||||
"io"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
keyLen = 32
|
||||
authLen = keyLen * 2
|
||||
cookieName = "gosession"
|
||||
)
|
||||
|
||||
var store *sessions.CookieStore
|
||||
|
||||
func cookieStore() {
|
||||
options := &sessions.Options{
|
||||
Path: "/",
|
||||
MaxAge: 3600 * 12,
|
||||
Secure: secureCookie,
|
||||
HttpOnly: true,
|
||||
}
|
||||
authKey := genKey(authLen)
|
||||
encKey := genKey(keyLen)
|
||||
store = sessions.NewCookieStore(authKey, encKey)
|
||||
store.Options = options
|
||||
|
||||
go func() {
|
||||
for {
|
||||
time.Sleep(time.Hour * 12)
|
||||
newAuthKey := genKey(authLen)
|
||||
newEncKey := genKey(keyLen)
|
||||
newStore := sessions.NewCookieStore(newAuthKey, newEncKey, authKey, encKey)
|
||||
newStore.Options = options
|
||||
|
||||
store = newStore
|
||||
authKey = newAuthKey
|
||||
encKey = newEncKey
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
func genKey(length int) (bytes []byte) {
|
||||
bytes = make([]byte, length)
|
||||
if _, err := io.ReadFull(rand.Reader, bytes); err != nil {
|
||||
log.Println("genKey:", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func newContext(w http.ResponseWriter, r *http.Request) *Context {
|
||||
log.Printf("%s %s %s\n", r.RemoteAddr, r.Method, r.URL.Path)
|
||||
h := w.Header()
|
||||
h.Set("X-Frame-Options", "DENY")
|
||||
h.Set("X-Content-Type-Options", "nosniff")
|
||||
h.Set("X-XSS-Protection", "1; mode=block")
|
||||
h.Set("Content-Security-Policy", "default-src 'none';style-src 'self' 'unsafe-inline';frame-ancestors 'none'")
|
||||
return &Context{
|
||||
Request: r,
|
||||
Response: w,
|
||||
}
|
||||
}
|
||||
|
||||
type Context struct {
|
||||
Request *http.Request
|
||||
Response http.ResponseWriter
|
||||
T *template.Template
|
||||
|
||||
Data webData
|
||||
}
|
||||
|
||||
type webData struct {
|
||||
Title string
|
||||
BodyTitle string
|
||||
|
||||
Admin bool
|
||||
Login bool
|
||||
|
||||
User string
|
||||
Token string
|
||||
Msg string
|
||||
Search string
|
||||
|
||||
Data interface{}
|
||||
}
|
||||
|
||||
func (c *Context) Exec() {
|
||||
if c.T == nil {
|
||||
log.Println("Exec:", errors.New("template is nil"))
|
||||
return
|
||||
}
|
||||
c.Data.Admin = c.GetAdmin()
|
||||
c.Data.User = c.GetUser()
|
||||
c.Data.Login = c.LoggedOn()
|
||||
err := c.T.Execute(c.Response, c.Data)
|
||||
if err != nil {
|
||||
log.Println("Exec:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Context) Template(name string) {
|
||||
c.T = templ[name]
|
||||
}
|
||||
|
||||
func (c *Context) Write(buf []byte) (err error) {
|
||||
_, err = c.Response.Write(buf)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Context) SetHeader(name, value string) {
|
||||
c.Response.Header().Set(name, value)
|
||||
}
|
||||
|
||||
func (c *Context) Redirect(uri string, code int) {
|
||||
http.Redirect(c.Response, c.Request, uri, code)
|
||||
}
|
||||
|
||||
func (c *Context) Method() string {
|
||||
return c.Request.Method
|
||||
}
|
||||
|
||||
func (c *Context) Path() string {
|
||||
return c.Request.URL.Path
|
||||
}
|
||||
|
||||
func (c *Context) FormSlice(name string) []string {
|
||||
// does nothing if called twice
|
||||
c.Request.ParseForm()
|
||||
return c.Request.PostForm[name]
|
||||
}
|
||||
|
||||
func (c *Context) Form(name string) (ret string) {
|
||||
ret = c.Request.PostFormValue(name)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Context) Var(name string) (ret string) {
|
||||
ret, _ = mux.Vars(c.Request)[name]
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Context) InitToken() {
|
||||
token := c.getToken()
|
||||
if token == nil || len(token) != keyLen {
|
||||
s, _ := store.Get(c.Request, cookieName)
|
||||
s.Values["token"] = genKey(keyLen)
|
||||
s.Options = store.Options
|
||||
err := s.Save(c.Request, c.Response)
|
||||
if err != nil {
|
||||
log.Println("InitToken: ", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewToken sets and returns the xsrf token
|
||||
func (c *Context) NewToken() {
|
||||
rnd := genKey(keyLen)
|
||||
c.Data.Token = base64.StdEncoding.EncodeToString(append(rnd, xor(rnd, c.getToken())...))
|
||||
}
|
||||
|
||||
// CheckToken validates the xsrf token
|
||||
func (c *Context) CheckToken() bool {
|
||||
token, err := base64.StdEncoding.DecodeString(c.Form("token"))
|
||||
if err != nil {
|
||||
log.Println("CheckToken: ", err)
|
||||
return false
|
||||
}
|
||||
if len(token) != authLen {
|
||||
return false
|
||||
}
|
||||
return subtle.ConstantTimeCompare(c.getToken(), xor(token[keyLen:], token[:keyLen])) == 1
|
||||
}
|
||||
|
||||
func (c *Context) LoggedOn() (ok bool) {
|
||||
s, _ := store.Get(c.Request, cookieName)
|
||||
_, ok = s.Values["user"]
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Context) SetUdata(user string, admin bool) {
|
||||
s, _ := store.Get(c.Request, cookieName)
|
||||
s.Values["user"] = user
|
||||
s.Values["admin"] = admin
|
||||
s.Options = store.Options
|
||||
if err := s.Save(c.Request, c.Response); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Context) UnsetUdata() {
|
||||
s, _ := store.Get(c.Request, cookieName)
|
||||
s.Values = make(map[interface{}]interface{})
|
||||
s.Options = store.Options
|
||||
if err := s.Save(c.Request, c.Response); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Context) GetUser() string {
|
||||
s, _ := store.Get(c.Request, cookieName)
|
||||
if ret, ok := s.Values["user"].(string); ok {
|
||||
return ret
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (c *Context) GetAdmin() bool {
|
||||
s, _ := store.Get(c.Request, cookieName)
|
||||
if ret, ok := s.Values["admin"].(bool); ok {
|
||||
return ret
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (c *Context) getToken() []byte {
|
||||
s, _ := store.Get(c.Request, cookieName)
|
||||
t, ok := s.Values["token"]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
b, ok := t.([]byte)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return b
|
||||
}
|
||||
|
||||
func xor(a, b []byte) (c []byte) {
|
||||
n := len(a)
|
||||
if len(b) != n {
|
||||
return
|
||||
}
|
||||
c = make([]byte, n)
|
||||
for i := 0; i < n; i++ {
|
||||
c[i] = a[i] ^ b[i]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type ctxHandler func(*Context)
|
||||
86
dump.go
86
dump.go
|
|
@ -1,86 +0,0 @@
|
|||
// Copyright (C) 2017 Marius Schellenberger
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/blevesearch/bleve"
|
||||
)
|
||||
|
||||
type DumpArticle struct {
|
||||
Title string `json:"title"`
|
||||
LinkTitle string `json:"link_title"`
|
||||
MD string `json:"md"`
|
||||
Created string `json:"created"`
|
||||
Updated string `json:"updated"`
|
||||
Public bool `json:"public"`
|
||||
}
|
||||
|
||||
func verboseLog(prefix, title string) {
|
||||
if verbose {
|
||||
fmt.Fprintln(os.Stderr, prefix, title)
|
||||
}
|
||||
}
|
||||
|
||||
func dumpWiki() error {
|
||||
index, err := bleve.OpenUsing(bleveStore, map[string]interface{}{"read_only": true})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
engine = NewEngine(index)
|
||||
art, err := engine.Get(false, indexName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer engine.Close()
|
||||
var arts []DumpArticle
|
||||
titles, err := engine.Dump()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
for _, title := range titles {
|
||||
art, err = engine.Get(false, title)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
verboseLog("dump:", art.Title)
|
||||
arts = append(arts, DumpArticle{
|
||||
Title: art.Title,
|
||||
LinkTitle: art.LinkTitle,
|
||||
MD: art.MD,
|
||||
Created: art.Created,
|
||||
Updated: art.Updated,
|
||||
Public: art.Public,
|
||||
})
|
||||
}
|
||||
return json.NewEncoder(os.Stdout).Encode(arts)
|
||||
}
|
||||
|
||||
func restoreWiki() (err error) {
|
||||
index, err := bleve.Open(bleveStore)
|
||||
if err != nil {
|
||||
mapping := bleve.NewIndexMapping()
|
||||
index, err = bleve.New(bleveStore, mapping)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
engine = NewEngine(index)
|
||||
defer engine.Close()
|
||||
var arts []DumpArticle
|
||||
err = json.NewDecoder(os.Stdin).Decode(&arts)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, art := range arts {
|
||||
verboseLog("restore:", art.Title)
|
||||
err = engine.Restore(art)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
270
engine.go
270
engine.go
|
|
@ -1,270 +0,0 @@
|
|||
// Copyright (C) 2017 Marius Schellenberger
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"html/template"
|
||||
"regexp"
|
||||
"time"
|
||||
|
||||
"github.com/blevesearch/bleve"
|
||||
"github.com/blevesearch/bleve/document"
|
||||
)
|
||||
|
||||
const (
|
||||
timeFmt = "2006-01-02 15:04:05"
|
||||
maxAllResult = 1000000
|
||||
maxSearchResult = 101
|
||||
)
|
||||
|
||||
var titleRe = regexp.MustCompile("[^a-zA-Z0-9+ ]+")
|
||||
|
||||
func now() string {
|
||||
return time.Now().Format(timeFmt)
|
||||
}
|
||||
|
||||
type Article struct {
|
||||
Title string `json:"title"`
|
||||
LinkTitle string
|
||||
Search string `json:"search"`
|
||||
Index template.HTML `json:"index"`
|
||||
Text template.HTML `json:"text"`
|
||||
MD string `json:"md"`
|
||||
Created string `json:"created"`
|
||||
Updated string `json:"updated"`
|
||||
Public bool `json:"public"`
|
||||
}
|
||||
|
||||
type Engine struct {
|
||||
index bleve.Index
|
||||
}
|
||||
|
||||
func NewEngine(index bleve.Index) *Engine {
|
||||
return &Engine{index}
|
||||
}
|
||||
|
||||
func (e *Engine) Close() error {
|
||||
return e.index.Close()
|
||||
}
|
||||
|
||||
func (e *Engine) Restore(dumpArt DumpArticle) error {
|
||||
art := render(dumpArt.MD)
|
||||
art.Title = dumpArt.Title
|
||||
art.Created = dumpArt.Created
|
||||
art.Updated = dumpArt.Updated
|
||||
art.Public = dumpArt.Public
|
||||
return e.index.Index(dumpArt.LinkTitle, art)
|
||||
}
|
||||
|
||||
func (e *Engine) Index(title, text, user string, public bool) (string, error) {
|
||||
if user == "" {
|
||||
return "", errors.New("engine: user cannot be empty")
|
||||
}
|
||||
title = titleRe.ReplaceAllString(title, "")
|
||||
newTitle := makeLinkTitle(title)
|
||||
if err := e.Exists(false, newTitle); err == nil {
|
||||
return "", errors.New("engine: article '" + title + "' already exists")
|
||||
}
|
||||
art := render(text)
|
||||
art.Title = title
|
||||
art.Created = user + " " + now()
|
||||
art.Public = public
|
||||
err := e.index.Index(newTitle, art)
|
||||
return newTitle, err
|
||||
}
|
||||
|
||||
func (e *Engine) Exists(public bool, title string) error {
|
||||
_, err := e.Get(public, title)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (e *Engine) Delete(title string) error {
|
||||
if title == indexName {
|
||||
return errors.New("engine: page '" + indexName + "' cannot be deleted")
|
||||
}
|
||||
if err := e.Exists(false, title); err != nil {
|
||||
return err
|
||||
}
|
||||
return e.index.Delete(title)
|
||||
}
|
||||
|
||||
func (e *Engine) Update(title, text, user string, public bool) error {
|
||||
if user == "" {
|
||||
return errors.New("engine: user cannot be empty")
|
||||
}
|
||||
if title == indexName && !public {
|
||||
return errors.New("engine: page '" + indexName + "' must stay public")
|
||||
}
|
||||
if err := e.Exists(false, title); err != nil {
|
||||
return err
|
||||
}
|
||||
art, err := e.Get(false, title)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
rend := render(text)
|
||||
art.Updated = user + " " + now()
|
||||
art.Search = rend.Search
|
||||
art.Index = rend.Index
|
||||
art.Text = rend.Text
|
||||
art.MD = rend.MD
|
||||
art.Public = public
|
||||
return e.index.Index(title, art)
|
||||
}
|
||||
|
||||
func (e *Engine) Get(public bool, title string) (Article, error) {
|
||||
var art Article
|
||||
doc, err := e.index.Document(title)
|
||||
if err != nil {
|
||||
return art, err
|
||||
}
|
||||
if doc == nil || doc.Fields == nil {
|
||||
return art, errors.New("engine: article '" + title + "' does not esist")
|
||||
}
|
||||
for _, field := range doc.Fields {
|
||||
switch f := field.(type) {
|
||||
case *document.TextField:
|
||||
switch field.Name() {
|
||||
case "title":
|
||||
art.Title = string(f.Value())
|
||||
case "index":
|
||||
art.Index = template.HTML(string(f.Value()))
|
||||
case "text":
|
||||
art.Text = template.HTML(string(f.Value()))
|
||||
case "md":
|
||||
art.MD = string(f.Value())
|
||||
case "created":
|
||||
art.Created = string(f.Value())
|
||||
case "updated":
|
||||
art.Updated = string(f.Value())
|
||||
}
|
||||
case *document.BooleanField:
|
||||
if field.Name() == "public" {
|
||||
p, err := f.Boolean()
|
||||
if err != nil {
|
||||
return art, err
|
||||
}
|
||||
art.Public = p
|
||||
}
|
||||
}
|
||||
}
|
||||
art.LinkTitle = title
|
||||
if public && !art.Public {
|
||||
err = errors.New("engine: article '" + title + "' does not esist")
|
||||
}
|
||||
return art, err
|
||||
}
|
||||
|
||||
func (e *Engine) Search(public bool, search string) (string, error) {
|
||||
var (
|
||||
res *bleve.SearchResult
|
||||
req *bleve.SearchRequest
|
||||
err error
|
||||
)
|
||||
if public {
|
||||
req = bleve.NewSearchRequest(bleve.NewConjunctionQuery([]bleve.Query{
|
||||
bleve.NewBoolFieldQuery(true).SetField("public"),
|
||||
bleve.NewQueryStringQuery("title:" + search + " search:" + search),
|
||||
}))
|
||||
} else {
|
||||
req = bleve.NewSearchRequest(bleve.NewQueryStringQuery("title:" + search + " search:" + search))
|
||||
}
|
||||
req.Highlight = bleve.NewHighlightWithStyle("html")
|
||||
req.Size = maxSearchResult
|
||||
res, err = e.index.Search(req)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if res.Total == 0 {
|
||||
return "<b>No search results</b>", nil
|
||||
} else if res.Total == maxSearchResult {
|
||||
return "", errors.New("engine: too many search results")
|
||||
}
|
||||
html := ""
|
||||
for _, hit := range res.Hits {
|
||||
if hit == nil {
|
||||
continue
|
||||
}
|
||||
art, err := e.Get(false, hit.ID)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
html += `<a href="/wiki/` + hit.ID + `"><b>` + art.Title + "</b></a>"
|
||||
for fragField, frags := range hit.Fragments {
|
||||
if fragField == "title" || fragField == "search" {
|
||||
html += `<pre style="white-space: pre-wrap">`
|
||||
for _, f := range frags {
|
||||
html += f
|
||||
}
|
||||
html += "</pre>"
|
||||
break
|
||||
}
|
||||
}
|
||||
html += "<br>"
|
||||
}
|
||||
return html, nil
|
||||
}
|
||||
|
||||
type Title struct {
|
||||
Title string
|
||||
LinkTitle string
|
||||
}
|
||||
|
||||
func (e *Engine) GetAll(public bool) (t []Title, err error) {
|
||||
var (
|
||||
res *bleve.SearchResult
|
||||
req *bleve.SearchRequest
|
||||
)
|
||||
if public {
|
||||
req = bleve.NewSearchRequest(bleve.NewBoolFieldQuery(true).SetField("public"))
|
||||
} else {
|
||||
req = bleve.NewSearchRequest(bleve.NewMatchAllQuery())
|
||||
}
|
||||
req.Size = maxAllResult
|
||||
res, err = e.index.Search(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, v := range res.Hits {
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
if v.ID == indexName {
|
||||
continue
|
||||
}
|
||||
doc, err := e.index.Document(v.ID)
|
||||
if err != nil || doc == nil || doc.Fields == nil {
|
||||
continue
|
||||
}
|
||||
for _, field := range doc.Fields {
|
||||
if field.Name() == "title" {
|
||||
f, ok := field.(*document.TextField)
|
||||
if ok {
|
||||
t = append(t, Title{string(f.Value()), v.ID})
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (e *Engine) Dump() (titles []string, err error) {
|
||||
req := bleve.NewSearchRequest(bleve.NewMatchAllQuery())
|
||||
req.Size = maxAllResult
|
||||
res, err := e.index.Search(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, v := range res.Hits {
|
||||
if v == nil {
|
||||
continue
|
||||
}
|
||||
titles = append(titles, v.ID)
|
||||
}
|
||||
return
|
||||
}
|
||||
40
go.mod
Normal file
40
go.mod
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
module git.giftfish.de/ston1th/gowiki
|
||||
|
||||
go 1.24.0
|
||||
|
||||
require (
|
||||
git.giftfish.de/ston1th/godrop/v2 v2.2.0
|
||||
git.giftfish.de/ston1th/jwt/v3 v3.3.0
|
||||
github.com/blevesearch/bleve v1.0.14
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/pquerna/otp v1.5.0
|
||||
github.com/russross/blackfriday v1.6.0
|
||||
github.com/urfave/cli v1.22.17
|
||||
go.etcd.io/bbolt v1.4.3
|
||||
golang.org/x/crypto v0.43.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/RoaringBitmap/roaring v1.9.4 // indirect
|
||||
github.com/bits-and-blooms/bitset v1.24.1 // indirect
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3 // indirect
|
||||
github.com/blevesearch/mmap-go v1.0.4 // indirect
|
||||
github.com/blevesearch/segment v0.9.1 // indirect
|
||||
github.com/blevesearch/snowballstem v0.9.0 // indirect
|
||||
github.com/blevesearch/zap/v11 v11.0.14 // indirect
|
||||
github.com/blevesearch/zap/v12 v12.0.14 // indirect
|
||||
github.com/blevesearch/zap/v13 v13.0.6 // indirect
|
||||
github.com/blevesearch/zap/v14 v14.0.5 // indirect
|
||||
github.com/blevesearch/zap/v15 v15.0.3 // indirect
|
||||
github.com/boombuler/barcode v1.1.0 // indirect
|
||||
github.com/couchbase/vellum v1.0.2 // indirect
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 // indirect
|
||||
github.com/golang/protobuf v1.5.4 // indirect
|
||||
github.com/golang/snappy v1.0.0 // indirect
|
||||
github.com/mschoch/smat v0.2.0 // indirect
|
||||
github.com/russross/blackfriday/v2 v2.1.0 // indirect
|
||||
github.com/steveyen/gtreap v0.1.0 // indirect
|
||||
github.com/willf/bitset v1.1.11 // indirect
|
||||
golang.org/x/sys v0.37.0 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
)
|
||||
171
go.sum
Normal file
171
go.sum
Normal file
|
|
@ -0,0 +1,171 @@
|
|||
git.giftfish.de/ston1th/godrop/v2 v2.2.0 h1:jdcPJeotiuug3tC4zXvwnauen1Y7kfF9CbFKQbGttYU=
|
||||
git.giftfish.de/ston1th/godrop/v2 v2.2.0/go.mod h1:gDAD0cWURkN0cMAjjSKB6a6jqIXvOrY8Th25HdoJAyg=
|
||||
git.giftfish.de/ston1th/jwt/v3 v3.3.0 h1:lSBVbk1D+KmLHBlxhS7snK33+U975cNbx/Z4Md6S+0A=
|
||||
git.giftfish.de/ston1th/jwt/v3 v3.3.0/go.mod h1:jDALrW7OKeIaqYuMxs3GWyPRnQZmk/R+Lu9p/Em3Ziw=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/RoaringBitmap/roaring v0.4.23/go.mod h1:D0gp8kJQgE1A4LQ5wFLggQEyvDi06Mq5mKs52e1TwOo=
|
||||
github.com/RoaringBitmap/roaring v1.9.4 h1:yhEIoH4YezLYT04s1nHehNO64EKFTop/wBhxv2QzDdQ=
|
||||
github.com/RoaringBitmap/roaring v1.9.4/go.mod h1:6AXUsoIEzDTFFQCe1RbGA6uFONMhvejWj5rqITANK90=
|
||||
github.com/armon/consul-api v0.0.0-20180202201655-eb2c6b5be1b6/go.mod h1:grANhF5doyWs3UAsr3K4I6qtAmlQcZDesFNEHPZAzj8=
|
||||
github.com/bits-and-blooms/bitset v1.12.0/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/bits-and-blooms/bitset v1.24.1 h1:hqnfFbjjk3pxGa5E9Ho3hjoU7odtUuNmJ9Ao+Bo8s1c=
|
||||
github.com/bits-and-blooms/bitset v1.24.1/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8=
|
||||
github.com/blevesearch/bleve v1.0.14 h1:Q8r+fHTt35jtGXJUM0ULwM3Tzg+MRfyai4ZkWDy2xO4=
|
||||
github.com/blevesearch/bleve v1.0.14/go.mod h1:e/LJTr+E7EaoVdkQZTfoz7dt4KoDNvDbLb8MSKuNTLQ=
|
||||
github.com/blevesearch/blevex v1.0.0 h1:pnilj2Qi3YSEGdWgLj1Pn9Io7ukfXPoQcpAI1Bv8n/o=
|
||||
github.com/blevesearch/blevex v1.0.0/go.mod h1:2rNVqoG2BZI8t1/P1awgTKnGlx5MP9ZbtEciQaNhswc=
|
||||
github.com/blevesearch/cld2 v0.0.0-20200327141045-8b5f551d37f5/go.mod h1:PN0QNTLs9+j1bKy3d/GB/59wsNBFC4sWLWG3k69lWbc=
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3 h1:GtmsqID0aZdCSNiY8SkuPJ12pD4jI+DdXTAn4YRcHCo=
|
||||
github.com/blevesearch/go-porterstemmer v1.0.3/go.mod h1:angGc5Ht+k2xhJdZi511LtmxuEf0OVpvUUNrwmM1P7M=
|
||||
github.com/blevesearch/mmap-go v1.0.2/go.mod h1:ol2qBqYaOUsGdm7aRMRrYGgPvnwLe6Y+7LMvAB5IbSA=
|
||||
github.com/blevesearch/mmap-go v1.0.4 h1:OVhDhT5B/M1HNPpYPBKIEJaD0F3Si+CrEKULGCDPWmc=
|
||||
github.com/blevesearch/mmap-go v1.0.4/go.mod h1:EWmEAOmdAS9z/pi/+Toxu99DnsbhG1TIxUoRmJw/pSs=
|
||||
github.com/blevesearch/segment v0.9.0/go.mod h1:9PfHYUdQCgHktBgvtUOF4x+pc4/l8rdH0u5spnW85UQ=
|
||||
github.com/blevesearch/segment v0.9.1 h1:+dThDy+Lvgj5JMxhmOVlgFfkUtZV2kw49xax4+jTfSU=
|
||||
github.com/blevesearch/segment v0.9.1/go.mod h1:zN21iLm7+GnBHWTao9I+Au/7MBiL8pPFtJBJTsk6kQw=
|
||||
github.com/blevesearch/snowballstem v0.9.0 h1:lMQ189YspGP6sXvZQ4WZ+MLawfV8wOmPoD/iWeNXm8s=
|
||||
github.com/blevesearch/snowballstem v0.9.0/go.mod h1:PivSj3JMc8WuaFkTSRDW2SlrulNWPl4ABg1tC/hlgLs=
|
||||
github.com/blevesearch/zap/v11 v11.0.14 h1:IrDAvtlzDylh6H2QCmS0OGcN9Hpf6mISJlfKjcwJs7k=
|
||||
github.com/blevesearch/zap/v11 v11.0.14/go.mod h1:MUEZh6VHGXv1PKx3WnCbdP404LGG2IZVa/L66pyFwnY=
|
||||
github.com/blevesearch/zap/v12 v12.0.14 h1:2o9iRtl1xaRjsJ1xcqTyLX414qPAwykHNV7wNVmbp3w=
|
||||
github.com/blevesearch/zap/v12 v12.0.14/go.mod h1:rOnuZOiMKPQj18AEKEHJxuI14236tTQ1ZJz4PAnWlUg=
|
||||
github.com/blevesearch/zap/v13 v13.0.6 h1:r+VNSVImi9cBhTNNR+Kfl5uiGy8kIbb0JMz/h8r6+O4=
|
||||
github.com/blevesearch/zap/v13 v13.0.6/go.mod h1:L89gsjdRKGyGrRN6nCpIScCvvkyxvmeDCwZRcjjPCrw=
|
||||
github.com/blevesearch/zap/v14 v14.0.5 h1:NdcT+81Nvmp2zL+NhwSvGSLh7xNgGL8QRVZ67njR0NU=
|
||||
github.com/blevesearch/zap/v14 v14.0.5/go.mod h1:bWe8S7tRrSBTIaZ6cLRbgNH4TUDaC9LZSpRGs85AsGY=
|
||||
github.com/blevesearch/zap/v15 v15.0.3 h1:Ylj8Oe+mo0P25tr9iLPp33lN6d4qcztGjaIsP51UxaY=
|
||||
github.com/blevesearch/zap/v15 v15.0.3/go.mod h1:iuwQrImsh1WjWJ0Ue2kBqY83a0rFtJTqfa9fp1rbVVU=
|
||||
github.com/boombuler/barcode v1.0.1-0.20190219062509-6c824513bacc/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/boombuler/barcode v1.1.0 h1:ChaYjBR63fr4LFyGn8E8nt7dBSt3MiU3zMOZqFvVkHo=
|
||||
github.com/boombuler/barcode v1.1.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8=
|
||||
github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE=
|
||||
github.com/coreos/go-etcd v2.0.0+incompatible/go.mod h1:Jez6KQU2B/sWsbdaef3ED8NzMklzPG4d5KIOhIy30Tk=
|
||||
github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk=
|
||||
github.com/couchbase/ghistogram v0.1.0/go.mod h1:s1Jhy76zqfEecpNWJfWUiKZookAFaiGOEoyzgHt9i7k=
|
||||
github.com/couchbase/moss v0.1.0/go.mod h1:9MaHIaRuy9pvLPUJxB8sh8OrLfyDczECVL37grCIubs=
|
||||
github.com/couchbase/vellum v1.0.2 h1:BrbP0NKiyDdndMPec8Jjhy0U47CZ0Lgx3xUC2r9rZqw=
|
||||
github.com/couchbase/vellum v1.0.2/go.mod h1:FcwrEivFpNi24R3jLOs3n+fs5RnuQnQqCLBJ1uAg1W4=
|
||||
github.com/cpuguy83/go-md2man v1.0.10/go.mod h1:SmD6nW6nTyfqj6ABTjUi3V3JVMnlJmwcJI5acqYI6dE=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7 h1:zbFlGlXEAKlwXpmvle3d8Oe3YnkKIK4xSRTd3sHPnBo=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.7/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g=
|
||||
github.com/cznic/b v0.0.0-20181122101859-a26611c4d92d h1:SwD98825d6bdB+pEuTxWOXiSjBrHdOl/UVp75eI7JT8=
|
||||
github.com/cznic/b v0.0.0-20181122101859-a26611c4d92d/go.mod h1:URriBxXwVq5ijiJ12C7iIZqlA69nTlI+LgI6/pwftG8=
|
||||
github.com/cznic/mathutil v0.0.0-20181122101859-297441e03548/go.mod h1:e6NPNENfs9mPDVNRekM7lKScauxd5kXTr1Mfyig6TDM=
|
||||
github.com/cznic/strutil v0.0.0-20181122101858-275e90344537/go.mod h1:AHHPPPXTw0h6pVabbcbyGRK1DckRn7r/STdZEeIDzZc=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/facebookgo/ensure v0.0.0-20200202191622-63f1cf65ac4c/go.mod h1:Yg+htXGokKKdzcwhuNDwVvN+uBxDGXJ7G/VN1d8fa64=
|
||||
github.com/facebookgo/stack v0.0.0-20160209184415-751773369052/go.mod h1:UbMTZqLaRiH3MsBH8va0n7s1pQYcu3uTb8G4tygF4Zg=
|
||||
github.com/facebookgo/subset v0.0.0-20200203212716-c811ad88dec4/go.mod h1:5tD+neXqOorC30/tWg0LCSkrqj/AR6gu8yY8/fpw1q0=
|
||||
github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo=
|
||||
github.com/glycerine/go-unsnap-stream v0.0.0-20181221182339-f9677308dec2/go.mod h1:/20jfyN9Y5QPEAprSgKAUr+glWDY39ZiUEAYOEv5dsE=
|
||||
github.com/glycerine/goconvey v0.0.0-20190410193231-58a59202ab31/go.mod h1:Ogl1Tioa0aV7gstGFO7KhffUsb9M4ydbEbbxpcEDc24=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||
github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v0.0.1/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs=
|
||||
github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20190910122728-9d188e94fb99/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU=
|
||||
github.com/ikawaha/kagome.ipadic v1.1.2/go.mod h1:DPSBbU0czaJhAb/5uKQZHMc9MTVRpDugJfX+HddPHHg=
|
||||
github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8=
|
||||
github.com/jmhodges/levigo v1.0.0 h1:q5EC36kV79HWeTBWsod3mG11EgStG3qArTKcvlksN1U=
|
||||
github.com/jmhodges/levigo v1.0.0/go.mod h1:Q6Qx+uH3RAqyK4rFQroq9RL7mdkABMcfhEI+nNuzMJQ=
|
||||
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||
github.com/kljensen/snowball v0.6.0/go.mod h1:27N7E8fVU5H68RlUmnWwZCfxgt4POBJfENGMvNRhldw=
|
||||
github.com/magiconair/properties v1.8.0/go.mod h1:PppfXfuXeibc/6YijjN8zIbojt8czPbwD3XqdrwzmxQ=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y=
|
||||
github.com/mschoch/smat v0.0.0-20160514031455-90eadee771ae/go.mod h1:qAyveg+e4CE+eKJXWVjKXM4ck2QobLqTDytGJbLLhJg=
|
||||
github.com/mschoch/smat v0.2.0 h1:8imxQsjDm8yFEAVBe7azKmKSgzSkZXDuKkSq9374khM=
|
||||
github.com/mschoch/smat v0.2.0/go.mod h1:kc9mz7DoBKqDyiRL7VZN8KvXQMWeTaVnttLRXOlotKw=
|
||||
github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE=
|
||||
github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY=
|
||||
github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic=
|
||||
github.com/philhofer/fwd v1.0.0/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pquerna/otp v1.5.0 h1:NMMR+WrmaqXU4EzdGJEE1aUUI0AMRzsp96fFFWNPwxs=
|
||||
github.com/pquerna/otp v1.5.0/go.mod h1:dkJfzwRKNiegxyNb54X/3fLwhCynbMspSyWKnvi1AEg=
|
||||
github.com/rcrowley/go-metrics v0.0.0-20190826022208-cac0b30c2563/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/russross/blackfriday v1.6.0 h1:KqfZb0pUVN2lYqZUYRddxF4OR8ZMURnJIG5Y3VRLtww=
|
||||
github.com/russross/blackfriday v1.6.0/go.mod h1:ti0ldHuxg49ri4ksnFxlkCfN+hvslNlmVHqNRXXJNAY=
|
||||
github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/spf13/afero v1.1.2/go.mod h1:j4pytiNVoe2o6bmDsKpLACNPDBIoEAkihy7loJ1B0CQ=
|
||||
github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE=
|
||||
github.com/spf13/cobra v0.0.5/go.mod h1:3K3wKZymM7VvHMDS9+Akkh4K60UwM26emMESw8tLCHU=
|
||||
github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo=
|
||||
github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4=
|
||||
github.com/spf13/viper v1.3.2/go.mod h1:ZiWeW+zYFKm7srdB9IoDzzZXaJaI5eL9QjNiN/DMA2s=
|
||||
github.com/steveyen/gtreap v0.1.0 h1:CjhzTa274PyJLJuMZwIzCO1PfC00oRa8d1Kc78bFXJM=
|
||||
github.com/steveyen/gtreap v0.1.0/go.mod h1:kl/5J7XbrOmlIbYIXdRHDDE5QxHqpk0cmkT7Z4dM9/Y=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/syndtr/goleveldb v1.0.0 h1:fBdIW9lB4Iz0n9khmH8w27SJ3QEJ7+IgjPEwGSZiFdE=
|
||||
github.com/syndtr/goleveldb v1.0.0/go.mod h1:ZVVdQEZoIme9iO1Ch2Jdy24qqXrMMOU6lpPAyBWyWuQ=
|
||||
github.com/tebeka/snowball v0.4.2/go.mod h1:4IfL14h1lvwZcp1sfXuuc7/7yCsvVffTWxWxCLfFpYg=
|
||||
github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c h1:g+WoO5jjkqGAzHWCjJB1zZfXPIAaDpzXIEJ0eS6B5Ok=
|
||||
github.com/tecbot/gorocksdb v0.0.0-20191217155057-f0fad39f321c/go.mod h1:ahpPrc7HpcfEWDQRZEmnXMzHY03mLDYMCxeDzy46i+8=
|
||||
github.com/tinylib/msgp v1.1.0/go.mod h1:+d+yLhGm8mzTaHzB+wgMYrodPfmZrzkirds8fDWklFE=
|
||||
github.com/ugorji/go/codec v0.0.0-20181204163529-d75b2dcb6bc8/go.mod h1:VFNgLljTbGfSG7qAOspJ7OScBnGdDN/yBr0sguwnwf0=
|
||||
github.com/urfave/cli v1.22.17 h1:SYzXoiPfQjHBbkYxbew5prZHS1TOLT3ierW8SYLqtVQ=
|
||||
github.com/urfave/cli v1.22.17/go.mod h1:b0ht0aqgH/6pBYzzxURyrM4xXNgsoT/n2ZzwQiEhNVo=
|
||||
github.com/willf/bitset v1.1.10/go.mod h1:RjeCKbqT1RxIR/KWY6phxZiaY1IyutSBfGjNPySAYV4=
|
||||
github.com/willf/bitset v1.1.11 h1:N7Z7E9UvjW+sGsEl7k/SJrvY2reP1A07MrGuCjIOjRE=
|
||||
github.com/willf/bitset v1.1.11/go.mod h1:83CECat5yLh5zVOf4P1ErAgKA5UDvKtgyUABdr3+MjI=
|
||||
github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q=
|
||||
go.etcd.io/bbolt v1.3.5/go.mod h1:G5EMThwa9y8QZGBClrRx5EY+Yw9kAhnjy3bSjsnlVTQ=
|
||||
go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo=
|
||||
go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E=
|
||||
golang.org/x/crypto v0.0.0-20181203042331-505ab145d0a9/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4=
|
||||
golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04=
|
||||
golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.10.0 h1:3NQrjDixjgGwUOCaF8w2+VYHv0Ve/vGYSbdkTa98gmQ=
|
||||
golang.org/x/sync v0.10.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||
golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181205085412-a5c9d58dba9a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20181221143128-b4a75ba826a6/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ=
|
||||
golang.org/x/sys v0.37.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
515
handler.go
515
handler.go
|
|
@ -1,515 +0,0 @@
|
|||
// Copyright (C) 2017 Marius Schellenberger
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"net"
|
||||
"net/http"
|
||||
"regexp"
|
||||
"time"
|
||||
)
|
||||
|
||||
func webserver(l net.Listener) {
|
||||
cookieStore()
|
||||
loadTemplates()
|
||||
srv := &http.Server{
|
||||
Handler: buildRoutes(),
|
||||
}
|
||||
log.Fatal("webserver: ", srv.Serve(l))
|
||||
}
|
||||
|
||||
type notFoundHandler struct{}
|
||||
|
||||
func (notFoundHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
f := func(ctx *Context) {
|
||||
ctx.Template("notFoundHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "404",
|
||||
}
|
||||
ctx.Exec()
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
f(newContext(w, r))
|
||||
}
|
||||
|
||||
func contextWrapper(h ctxHandler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
h(newContext(w, r))
|
||||
}
|
||||
}
|
||||
|
||||
func adminAuthHandler(h ctxHandler) ctxHandler {
|
||||
return func(ctx *Context) {
|
||||
if ctx.GetAdmin() {
|
||||
h(ctx)
|
||||
return
|
||||
}
|
||||
ctx.Redirect("/404", 302)
|
||||
}
|
||||
}
|
||||
|
||||
func userAuthHandler(h ctxHandler) ctxHandler {
|
||||
return func(ctx *Context) {
|
||||
if ctx.GetAdmin() {
|
||||
h(ctx)
|
||||
return
|
||||
}
|
||||
if ctx.GetUser() == ctx.Var("user") {
|
||||
h(ctx)
|
||||
return
|
||||
}
|
||||
ctx.Redirect("/404", 302)
|
||||
}
|
||||
}
|
||||
|
||||
func staticHandler(ctx *Context) {
|
||||
var err error
|
||||
ctx.SetHeader("Content-Type", "text/css; charset=utf-8")
|
||||
ctx.SetHeader("Expires", time.Now().Add(time.Hour*12).Format(http.TimeFormat))
|
||||
switch ctx.Path() {
|
||||
case "/bootstrap.css":
|
||||
err = ctx.Write(res["bootstrap.css"])
|
||||
case "/custom.css":
|
||||
err = ctx.Write(res["custom.css"])
|
||||
}
|
||||
if err != nil {
|
||||
log.Println("static:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func indexHandler(ctx *Context) {
|
||||
ctx.InitToken()
|
||||
if !ctx.LoggedOn() {
|
||||
ctx.UnsetUdata()
|
||||
}
|
||||
ctx.Redirect("/wiki/Index", 302)
|
||||
}
|
||||
|
||||
func loginHandler(ctx *Context) {
|
||||
ctx.InitToken()
|
||||
if ctx.LoggedOn() {
|
||||
ctx.Redirect("/wiki/Index", 302)
|
||||
return
|
||||
}
|
||||
ctx.Template("loginHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Login",
|
||||
BodyTitle: "Login",
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.NewToken()
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
user := ctx.Form("user")
|
||||
password := ctx.Form("password")
|
||||
submit := ctx.Form("submit")
|
||||
if !ctx.CheckToken() {
|
||||
ctx.NewToken()
|
||||
ctx.Data.Msg = "wrong csrf token"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
if user == "" || password == "" || submit != "Login" {
|
||||
ctx.NewToken()
|
||||
ctx.Data.Msg = "wrong inputs"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
err := db.Login(user, password)
|
||||
if err != nil {
|
||||
ctx.NewToken()
|
||||
ctx.Data.Msg = err.Error()
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
ctx.SetUdata(user, db.UserIsAdmin(user))
|
||||
ctx.Redirect("/wiki/Index", 302)
|
||||
}
|
||||
}
|
||||
|
||||
func searchHandler(ctx *Context) {
|
||||
ctx.InitToken()
|
||||
ctx.Template("searchHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Search",
|
||||
BodyTitle: "Search",
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.NewToken()
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
search := ctx.Form("search")
|
||||
submit := ctx.Form("submit")
|
||||
if !ctx.CheckToken() {
|
||||
ctx.NewToken()
|
||||
ctx.Data.Msg = "wrong csrf token"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
if search == "" || submit != "Search" {
|
||||
ctx.NewToken()
|
||||
ctx.Data.Msg = "wrong inputs"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
|
||||
res, err := engine.Search(!ctx.LoggedOn(), search)
|
||||
if err != nil {
|
||||
ctx.NewToken()
|
||||
ctx.Data.Msg = err.Error()
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
ctx.Data.Search = search
|
||||
ctx.Data.Data = template.HTML(res)
|
||||
ctx.NewToken()
|
||||
ctx.Exec()
|
||||
}
|
||||
}
|
||||
|
||||
func allHandler(ctx *Context) {
|
||||
ctx.Template("allHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "All Articles",
|
||||
BodyTitle: "All Articles",
|
||||
Admin: ctx.GetAdmin(),
|
||||
}
|
||||
all, err := engine.GetAll(!ctx.LoggedOn())
|
||||
if err != nil {
|
||||
ctx.Data.Msg = err.Error()
|
||||
}
|
||||
ctx.Data.Data = all
|
||||
ctx.Exec()
|
||||
}
|
||||
|
||||
func wikiNewHandler(ctx *Context) {
|
||||
ctx.Template("wikiNewHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "New Article",
|
||||
BodyTitle: "New Article",
|
||||
Admin: ctx.GetAdmin(),
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.NewToken()
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
title := ctx.Form("title")
|
||||
text := ctx.Form("text")
|
||||
public := ctx.Form("public")
|
||||
pub := false
|
||||
if public == "0" {
|
||||
pub = true
|
||||
}
|
||||
submit := ctx.Form("submit")
|
||||
art := Article{
|
||||
Title: title,
|
||||
MD: text,
|
||||
}
|
||||
ctx.Data.Data = art
|
||||
if !ctx.CheckToken() {
|
||||
ctx.NewToken()
|
||||
ctx.Data.Msg = "wrong csrf token"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
ctx.NewToken()
|
||||
m, err := regexp.MatchString("[^a-zA-Z0-9 _-]", title)
|
||||
if err != nil {
|
||||
ctx.Data.Msg = err.Error()
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
if m {
|
||||
ctx.Data.Msg = "title contains forbidden characters, allowed are [a-zA-Z0-9 _-]"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
err = engine.Exists(!ctx.LoggedOn(), title)
|
||||
if err == nil {
|
||||
ctx.Data.Msg = "article alerady exsist"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
if submit != "Submit" {
|
||||
ctx.Data.Msg = "wrong inputs"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
newTitle, err := engine.Index(title, text, ctx.GetUser(), pub)
|
||||
if err != nil {
|
||||
ctx.Data.Msg = err.Error()
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
ctx.Redirect("/wiki/"+newTitle, 302)
|
||||
}
|
||||
}
|
||||
|
||||
func wikiHandler(ctx *Context) {
|
||||
if ctx.T == nil {
|
||||
ctx.Template("wikiHandler")
|
||||
}
|
||||
ctx.Data = webData{
|
||||
Admin: ctx.GetAdmin(),
|
||||
}
|
||||
ctx.NewToken()
|
||||
title := ctx.Var("title")
|
||||
art, err := engine.Get(!ctx.LoggedOn(), title)
|
||||
if err != nil {
|
||||
ctx.Redirect("/404", 302)
|
||||
return
|
||||
}
|
||||
art.LinkTitle = title
|
||||
ctx.Data.Data = art
|
||||
ctx.Data.Title = art.Title
|
||||
ctx.Data.BodyTitle = art.Title
|
||||
ctx.Exec()
|
||||
}
|
||||
|
||||
func wikiMDHandler(f ctxHandler) ctxHandler {
|
||||
return func(ctx *Context) {
|
||||
ctx.Template("wikiMDHandler")
|
||||
f(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func wikiEditHandler(ctx *Context) {
|
||||
ctx.Template("wikiEditHandler")
|
||||
ctx.Data = webData{}
|
||||
title := ctx.Var("title")
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.NewToken()
|
||||
art, err := engine.Get(!ctx.LoggedOn(), title)
|
||||
if err != nil {
|
||||
ctx.Redirect("/404", 302)
|
||||
return
|
||||
}
|
||||
art.LinkTitle = title
|
||||
ctx.Data.Data = art
|
||||
ctx.Data.Title = art.Title
|
||||
ctx.Data.BodyTitle = art.Title
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
ftitle := ctx.Form("title")
|
||||
text := ctx.Form("text")
|
||||
public := ctx.Form("public")
|
||||
pub := false
|
||||
if public == "0" {
|
||||
pub = true
|
||||
}
|
||||
submit := ctx.Form("submit")
|
||||
ctx.Data.Data = Article{
|
||||
Title: ftitle,
|
||||
LinkTitle: title,
|
||||
MD: text,
|
||||
Public: pub,
|
||||
}
|
||||
ctx.Data.Title = ftitle
|
||||
ctx.Data.BodyTitle = ftitle
|
||||
if !ctx.CheckToken() {
|
||||
ctx.NewToken()
|
||||
ctx.Data.Msg = "wrong csrf token"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
ctx.NewToken()
|
||||
if submit != "Submit" {
|
||||
ctx.Data.Msg = "wrong inputs"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
err := engine.Update(title, text, ctx.GetUser(), pub)
|
||||
if err != nil {
|
||||
ctx.Data.Msg = err.Error()
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
ctx.Redirect("/wiki/"+title, 302)
|
||||
}
|
||||
}
|
||||
|
||||
func wikiDelHandler(ctx *Context) {
|
||||
title := ctx.Var("title")
|
||||
submit := ctx.Form("submit")
|
||||
if !ctx.CheckToken() {
|
||||
ctx.Redirect("/wiki/"+title, 302)
|
||||
return
|
||||
}
|
||||
if submit != "Delete" {
|
||||
ctx.Redirect("/wiki/"+title, 302)
|
||||
return
|
||||
}
|
||||
if err := engine.Delete(title); err != nil {
|
||||
log.Println("delete:", err)
|
||||
ctx.Redirect("/wiki/"+title, 302)
|
||||
}
|
||||
ctx.Redirect("/wiki/Index", 302)
|
||||
}
|
||||
|
||||
func usersHandler(ctx *Context) {
|
||||
if !ctx.LoggedOn() {
|
||||
ctx.Redirect("/404", 302)
|
||||
return
|
||||
}
|
||||
ctx.Template("usersHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Users",
|
||||
BodyTitle: "Users",
|
||||
User: ctx.GetUser(),
|
||||
}
|
||||
ctx.NewToken()
|
||||
us, err := db.GetUsers()
|
||||
if err != nil {
|
||||
ctx.Data.Msg = err.Error()
|
||||
}
|
||||
ctx.Data.Data = us
|
||||
ctx.Exec()
|
||||
}
|
||||
|
||||
func userNewHandler(ctx *Context) {
|
||||
ctx.Template("userNewHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "New User",
|
||||
BodyTitle: "New User",
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.NewToken()
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
user := ctx.Form("user")
|
||||
password := ctx.Form("password")
|
||||
repeat := ctx.Form("repeat")
|
||||
admin := ctx.Form("admin")
|
||||
adm := false
|
||||
if admin == "0" {
|
||||
adm = true
|
||||
}
|
||||
submit := ctx.Form("submit")
|
||||
if !ctx.CheckToken() {
|
||||
ctx.NewToken()
|
||||
ctx.Data.Msg = "wrong csrf token"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
if user == "" || password == "" || password != repeat || submit != "Submit" {
|
||||
ctx.NewToken()
|
||||
ctx.Data.Msg = "wrong inputs"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
log.Println(user, password, adm)
|
||||
if err := db.CreateUser(user, password, adm); err != nil {
|
||||
ctx.NewToken()
|
||||
ctx.Data.Msg = err.Error()
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
ctx.Redirect("/users", 302)
|
||||
}
|
||||
}
|
||||
|
||||
func userEditHandler(ctx *Context) {
|
||||
ctx.Template("userEditHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Edit User",
|
||||
BodyTitle: "Edit User",
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
u, err := db.GetUserWithoutPassword(ctx.Var("user"))
|
||||
if err != nil {
|
||||
ctx.Data.Msg = err.Error()
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
ctx.Data.Data = u
|
||||
ctx.NewToken()
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
user := ctx.Var("user")
|
||||
password := ctx.Form("password")
|
||||
repeat := ctx.Form("repeat")
|
||||
admin := ctx.Form("admin")
|
||||
adm := false
|
||||
if admin == "0" {
|
||||
adm = true
|
||||
}
|
||||
submit := ctx.Form("submit")
|
||||
if !ctx.CheckToken() {
|
||||
ctx.NewToken()
|
||||
ctx.Data.Msg = "wrong csrf token"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
if user == "" || password != repeat || submit != "Submit" {
|
||||
ctx.NewToken()
|
||||
ctx.Data.Msg = "wrong inputs"
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
if ctx.GetAdmin() {
|
||||
if err := db.AdminUpdateUser(user, password, adm); err != nil {
|
||||
ctx.NewToken()
|
||||
ctx.Data.Msg = err.Error()
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if user == ctx.GetUser() {
|
||||
if err := db.UpdateUserPassword(user, password); err != nil {
|
||||
ctx.NewToken()
|
||||
ctx.Data.Msg = err.Error()
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
ctx.Redirect("/wiki/Index", 302)
|
||||
return
|
||||
}
|
||||
}
|
||||
ctx.Redirect("/users", 302)
|
||||
}
|
||||
}
|
||||
|
||||
func userUnlockHandler(ctx *Context) {
|
||||
user := ctx.Var("user")
|
||||
if err := db.UnlockUser(user); err != nil {
|
||||
log.Println("unlock:", err)
|
||||
}
|
||||
ctx.Redirect("/users", 302)
|
||||
}
|
||||
|
||||
func userDelHandler(ctx *Context) {
|
||||
ctx.Template("userDelHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Delete User",
|
||||
BodyTitle: "Delete User",
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
user, err := db.GetUserWithoutPassword(ctx.Var("user"))
|
||||
if err != nil {
|
||||
ctx.Data.Msg = err.Error()
|
||||
ctx.Exec()
|
||||
return
|
||||
}
|
||||
ctx.Data.Data = user
|
||||
ctx.NewToken()
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if err := db.DeleteUser(ctx.Var("user")); err != nil {
|
||||
log.Println("delete:", err)
|
||||
}
|
||||
ctx.Redirect("/users", 302)
|
||||
}
|
||||
}
|
||||
|
||||
func logoutHandler(ctx *Context) {
|
||||
ctx.UnsetUdata()
|
||||
ctx.Redirect("/wiki/Index", 302)
|
||||
}
|
||||
23
logger.go
23
logger.go
|
|
@ -1,23 +0,0 @@
|
|||
// Copyright (C) 2017 Marius Schellenberger
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
logger "log"
|
||||
"os"
|
||||
)
|
||||
|
||||
var log *logger.Logger
|
||||
|
||||
func newLogger(file string) {
|
||||
if file == "" {
|
||||
log = logger.New(os.Stdout, "", logger.LstdFlags)
|
||||
return
|
||||
}
|
||||
f, err := os.OpenFile(file, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
logger.Fatal(err)
|
||||
}
|
||||
logger.SetOutput(f)
|
||||
log = logger.New(f, "", logger.LstdFlags)
|
||||
}
|
||||
157
main.go
157
main.go
|
|
@ -1,157 +0,0 @@
|
|||
// Copyright (C) 2017 Marius Schellenberger
|
||||
|
||||
//go:generate ./templates.sh
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
logger "log"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.giftfish.de/ston1th/godrop"
|
||||
"github.com/blevesearch/bleve"
|
||||
)
|
||||
|
||||
const (
|
||||
bleveStore = "."
|
||||
|
||||
defDataDir = "/var/lib/gowiki"
|
||||
defListen = "127.0.0.1:8090"
|
||||
defLog = "gowiki.log"
|
||||
|
||||
defUser = "admin"
|
||||
defPassword = "gowiki"
|
||||
|
||||
defRunUser = "wiki"
|
||||
defRunGroup = "wiki"
|
||||
|
||||
welcome = `# Welcome to GoWiki
|
||||
This is the [Index](/wiki/Index) page.
|
||||
You can customize it how you like.`
|
||||
indexName = "Index"
|
||||
)
|
||||
|
||||
var (
|
||||
datadir string
|
||||
listen string
|
||||
secureCookie bool
|
||||
user string
|
||||
group string
|
||||
logfile string
|
||||
|
||||
engine *Engine
|
||||
db *BoltStore
|
||||
|
||||
dump bool
|
||||
restore bool
|
||||
verbose bool
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.StringVar(&datadir, "d", defDataDir, "data directory")
|
||||
flag.StringVar(&user, "u", defRunUser, "drop privileges to user")
|
||||
flag.StringVar(&group, "g", defRunGroup, "drop privileges to group")
|
||||
flag.StringVar(&listen, "l", defListen, "listening <address>:<port>")
|
||||
flag.StringVar(&logfile, "log", defLog, "log file")
|
||||
flag.BoolVar(&secureCookie, "s", false, "enable secure cookie")
|
||||
flag.BoolVar(&dump, "dump", false, "create a json dump of all articles in the data directory (dump is written to stdout)")
|
||||
flag.BoolVar(&restore, "restore", false, "restore a json dump to the data directory (dump is read from stdin)")
|
||||
flag.BoolVar(&verbose, "v", false, "enable verbose dump/restore mode")
|
||||
flag.Parse()
|
||||
|
||||
err := os.Chdir(datadir)
|
||||
if err != nil {
|
||||
logger.Fatal("main: ", err)
|
||||
}
|
||||
|
||||
if dump {
|
||||
err = dumpWiki()
|
||||
if err != nil {
|
||||
logger.Fatal("dump: ", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if restore {
|
||||
err = restoreWiki()
|
||||
if err != nil {
|
||||
logger.Fatal("restore: ", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
cfg := godrop.Config{
|
||||
User: user,
|
||||
Group: group,
|
||||
}
|
||||
err = godrop.Drop(cfg, func() (net.Listener, error) {
|
||||
return net.Listen("tcp", listen)
|
||||
})
|
||||
if err != nil {
|
||||
logger.Fatal("main: ", err)
|
||||
}
|
||||
newLogger(logfile)
|
||||
l, err := godrop.GetListener()
|
||||
if err != nil {
|
||||
log.Fatal("main: ", err)
|
||||
}
|
||||
|
||||
db, err = NewBoltStore("users")
|
||||
if err != nil {
|
||||
log.Fatal("main: ", err)
|
||||
}
|
||||
if !db.UserExists(defUser) {
|
||||
err = db.CreateUser(defUser, defPassword, true)
|
||||
if err != nil {
|
||||
log.Fatal("main: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := os.Stat(bleveStore); os.IsNotExist(err) {
|
||||
mapping := bleve.NewIndexMapping()
|
||||
index, err := bleve.New(bleveStore, mapping)
|
||||
if err != nil {
|
||||
log.Fatal("main: ", err)
|
||||
}
|
||||
engine = NewEngine(index)
|
||||
_, err = engine.Index(indexName, welcome, defUser, true)
|
||||
if err != nil {
|
||||
log.Fatal("main: ", err)
|
||||
}
|
||||
} else {
|
||||
index, err := bleve.Open(bleveStore)
|
||||
if err != nil {
|
||||
log.Fatal("main: ", err)
|
||||
}
|
||||
engine = NewEngine(index)
|
||||
}
|
||||
|
||||
go webserver(l)
|
||||
log.Println("gowiki started")
|
||||
sigs := make(chan os.Signal)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
sig := <-sigs
|
||||
log.Println("caught " + sig.String())
|
||||
go func() {
|
||||
time.Sleep(time.Second * 20)
|
||||
log.Fatal(errors.New("stop timed out: killing"))
|
||||
}()
|
||||
err = l.Close()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
err = db.Close()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
err = engine.Close()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
54
pkg/cache/cache.go
vendored
Normal file
54
pkg/cache/cache.go
vendored
Normal file
|
|
@ -0,0 +1,54 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
type CachedPage struct {
|
||||
Page *core.Page
|
||||
CacheTime time.Time
|
||||
}
|
||||
|
||||
type Cache struct {
|
||||
mu sync.RWMutex
|
||||
Cache map[string]*CachedPage
|
||||
}
|
||||
|
||||
func NewCache() *Cache {
|
||||
return &Cache{Cache: make(map[string]*CachedPage)}
|
||||
}
|
||||
|
||||
func (c *Cache) Add(title string, p *core.Page) {
|
||||
now := time.Now()
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
cp, ok := c.Cache[title]
|
||||
if ok && now.After(cp.CacheTime) {
|
||||
cp.Page = p
|
||||
cp.CacheTime = now
|
||||
c.Cache[title] = cp
|
||||
return
|
||||
}
|
||||
c.Cache[title] = &CachedPage{p, now}
|
||||
}
|
||||
|
||||
func (c *Cache) Get(title string) (p *core.Page) {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
if cp, ok := c.Cache[title]; ok {
|
||||
p = new(core.Page)
|
||||
*p = *cp.Page
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Cache) Delete(title string) {
|
||||
c.mu.Lock()
|
||||
delete(c.Cache, title)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
50
pkg/cache/cache_test.go
vendored
Normal file
50
pkg/cache/cache_test.go
vendored
Normal file
|
|
@ -0,0 +1,50 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package cache
|
||||
|
||||
import (
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestCache(t *testing.T) {
|
||||
title := "test/page"
|
||||
pages := []*core.Page{
|
||||
&core.Page{
|
||||
Title: title,
|
||||
Markdown: "page v1",
|
||||
},
|
||||
&core.Page{
|
||||
Title: title,
|
||||
Markdown: "page v2",
|
||||
},
|
||||
}
|
||||
c := NewCache()
|
||||
t.Run("Add", func(t *testing.T) {
|
||||
c.Add(title, pages[0])
|
||||
})
|
||||
t.Run("Get", func(t *testing.T) {
|
||||
p := c.Get(title)
|
||||
if p.Markdown != pages[0].Markdown {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
t.Run("Add", func(t *testing.T) {
|
||||
c.Add(title, pages[1])
|
||||
})
|
||||
t.Run("Get", func(t *testing.T) {
|
||||
p := c.Get(title)
|
||||
if p.Markdown == pages[0].Markdown {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
t.Run("Delete", func(t *testing.T) {
|
||||
c.Delete(title)
|
||||
})
|
||||
t.Run("Get", func(t *testing.T) {
|
||||
p := c.Get(title)
|
||||
if p != nil {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
}
|
||||
281
pkg/cmd/cmd.go
Normal file
281
pkg/cmd/cmd.go
Normal file
|
|
@ -0,0 +1,281 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package cmd
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
stdlog "log"
|
||||
"net"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"git.giftfish.de/ston1th/godrop/v2"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/db"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/log"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/server"
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
const (
|
||||
defDataDir = "/var/lib/gowiki"
|
||||
defListen = "127.0.0.1:8080"
|
||||
defLogFile = "gowiki.log"
|
||||
|
||||
defRunUser = "_wiki"
|
||||
defRunGroup = "_wiki"
|
||||
)
|
||||
|
||||
var (
|
||||
config core.Config
|
||||
dumpFile string
|
||||
configFile string
|
||||
)
|
||||
|
||||
func initServer(cfg *core.Config) (err error) {
|
||||
if err = godrop.PledgePromises("stdio rpath wpath cpath inet fattr flock unveil"); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
l, err := net.Listen("tcp", cfg.ListenAddr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
log.InitLogger(cfg)
|
||||
|
||||
srv := server.NewHTTPServer(cfg, l)
|
||||
err = srv.Start()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
log.Println("gowiki " + cfg.Version + " started")
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
sig := <-sigs
|
||||
log.Println("signal: " + sig.String())
|
||||
go func() {
|
||||
time.Sleep(time.Second * 20)
|
||||
log.Fatal("stop timed out: killing")
|
||||
}()
|
||||
err = srv.Stop()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
log.Println("gowiki stopped")
|
||||
return
|
||||
}
|
||||
|
||||
func loadConfig() error {
|
||||
if configFile == "" {
|
||||
return nil
|
||||
}
|
||||
file, err := os.Open(configFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = json.NewDecoder(file).Decode(&config)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return file.Close()
|
||||
}
|
||||
|
||||
func Run(version string) {
|
||||
app := cli.NewApp()
|
||||
app.Name = "gowiki"
|
||||
app.Usage = "a simple wiki engine"
|
||||
app.Version = version
|
||||
app.Commands = []cli.Command{
|
||||
{
|
||||
Name: "server",
|
||||
Usage: "start gowiki server",
|
||||
Flags: defaultFlags(serverFlags()),
|
||||
Action: func(c *cli.Context) error {
|
||||
if err := loadConfig(); err != nil {
|
||||
stdlog.Fatal("server: ", err)
|
||||
}
|
||||
config.Version = app.Version
|
||||
if err := initServer(&config); err != nil {
|
||||
stdlog.Fatal("server: ", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "dump",
|
||||
Usage: "create a json dump of all articles in the data directory (gowiki needs to be stopped)",
|
||||
Flags: defaultFlags(dumpRestoreFlags()),
|
||||
Action: func(c *cli.Context) error {
|
||||
var (
|
||||
file = os.Stdout
|
||||
err error
|
||||
)
|
||||
if err = loadConfig(); err != nil {
|
||||
stdlog.Fatal("dump: ", err)
|
||||
}
|
||||
log.Stderr()
|
||||
if dumpFile != "-" {
|
||||
file, err = os.OpenFile(dumpFile, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
|
||||
if err != nil {
|
||||
stdlog.Fatal("dump: ", err)
|
||||
}
|
||||
}
|
||||
DB, err := db.NewPlain(&config)
|
||||
if err != nil {
|
||||
stdlog.Fatal("dump: ", err)
|
||||
}
|
||||
if err = DB.Dump(file); err != nil {
|
||||
stdlog.Fatal("dump: ", err)
|
||||
}
|
||||
file.Close()
|
||||
err = DB.Close()
|
||||
if err != nil {
|
||||
stdlog.Fatal("dump: ", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "restore",
|
||||
Usage: "restore a json dump to the data directory (gowiki needs to be stopped)",
|
||||
Flags: defaultFlags(dumpRestoreFlags()),
|
||||
Action: func(c *cli.Context) error {
|
||||
var (
|
||||
file = os.Stdin
|
||||
err error
|
||||
)
|
||||
if err = loadConfig(); err != nil {
|
||||
stdlog.Fatal("restore: ", err)
|
||||
}
|
||||
if dumpFile != "-" {
|
||||
file, err = os.Open(dumpFile)
|
||||
if err != nil {
|
||||
stdlog.Fatal("restore: ", err)
|
||||
}
|
||||
}
|
||||
DB, err := db.NewPlain(&config)
|
||||
if err != nil {
|
||||
stdlog.Fatal("restore: ", err)
|
||||
}
|
||||
if err = DB.Restore(file); err != nil {
|
||||
stdlog.Fatal("restore: ", err)
|
||||
}
|
||||
file.Close()
|
||||
err = DB.Close()
|
||||
if err != nil {
|
||||
stdlog.Fatal("restore: ", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
{
|
||||
Name: "resetadmin",
|
||||
Usage: "reset the admin users password to the default (gowiki needs to be stopped)",
|
||||
Flags: defaultFlags(nil),
|
||||
Action: func(c *cli.Context) error {
|
||||
var err error
|
||||
if err = loadConfig(); err != nil {
|
||||
stdlog.Fatal("resetadmin: ", err)
|
||||
}
|
||||
DB, err := db.NewPlain(&config)
|
||||
if err != nil {
|
||||
stdlog.Fatal("resetadmin: ", err)
|
||||
}
|
||||
err = DB.ResetAdmin()
|
||||
if err != nil {
|
||||
stdlog.Fatal("resetadmin: ", err)
|
||||
}
|
||||
err = DB.Close()
|
||||
if err != nil {
|
||||
stdlog.Fatal("resetadmin: ", err)
|
||||
}
|
||||
return nil
|
||||
},
|
||||
},
|
||||
}
|
||||
app.Run(os.Args)
|
||||
}
|
||||
|
||||
func serverFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "user, u",
|
||||
Value: defRunUser,
|
||||
Usage: "drop privileges to user",
|
||||
Destination: &config.User,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "group, g",
|
||||
Value: defRunGroup,
|
||||
Usage: "drop privileges to group",
|
||||
Destination: &config.Group,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "listen, l",
|
||||
Value: defListen,
|
||||
Usage: "listening <address>:<port>",
|
||||
Destination: &config.ListenAddr,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "log",
|
||||
Value: defLogFile,
|
||||
Usage: "log file (use -log=- for stdout, this only works in combination with -f)",
|
||||
Destination: &config.LogFile,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "secret",
|
||||
Usage: "static hmac secret (at least 64 chars, used for JWT signing)",
|
||||
Destination: &config.Secret,
|
||||
},
|
||||
cli.BoolTFlag{
|
||||
Name: "admin, a",
|
||||
Usage: "use -a=false to disable the admin user",
|
||||
Destination: &config.Admin,
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "foreground, f",
|
||||
Usage: "do not fork into the background",
|
||||
Destination: &config.Foreground,
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "secure, s",
|
||||
Usage: "enable secure cookie",
|
||||
Destination: &config.SecureCookie,
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "debug",
|
||||
Usage: "enable debugging",
|
||||
Destination: &config.Debug,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func dumpRestoreFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "file, f",
|
||||
Value: "-",
|
||||
Usage: "file to read/write dump (default stdin/stdout)",
|
||||
Destination: &dumpFile,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func defaultFlags(f []cli.Flag) []cli.Flag {
|
||||
return append([]cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "config, c",
|
||||
Usage: "config file",
|
||||
Destination: &configFile,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "data, d",
|
||||
Value: defDataDir,
|
||||
Usage: "data directory",
|
||||
Destination: &config.DataDir,
|
||||
},
|
||||
}, f...)
|
||||
}
|
||||
26
pkg/core/const.go
Normal file
26
pkg/core/const.go
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package core
|
||||
|
||||
const (
|
||||
IndexPage = "Index"
|
||||
WikiSection = "wiki"
|
||||
|
||||
IndexURI = WikiSection + "/" + IndexPage
|
||||
|
||||
RootURI = "/"
|
||||
WikiURI = "/wiki"
|
||||
LoginURI = "/login"
|
||||
LogoutURI = "/logout"
|
||||
TotpURI = "/totp"
|
||||
SectionsURI = "/sections"
|
||||
SectionURI = "/section"
|
||||
SearchURI = "/search"
|
||||
StatsURI = "/stats"
|
||||
NewURI = "/new"
|
||||
BlacklistURI = "/blacklist"
|
||||
UsersURI = "/users"
|
||||
UserURI = "/user"
|
||||
ViewURI = "/view"
|
||||
StaticURI = "/static"
|
||||
)
|
||||
13
pkg/core/helper.go
Normal file
13
pkg/core/helper.go
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package core
|
||||
|
||||
// Contains checks if slice a contains string str
|
||||
func Contains(str string, a []string) (int, bool) {
|
||||
for i, s := range a {
|
||||
if s == str {
|
||||
return i, true
|
||||
}
|
||||
}
|
||||
return -1, false
|
||||
}
|
||||
41
pkg/core/permission.go
Normal file
41
pkg/core/permission.go
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package core
|
||||
|
||||
type Permission int
|
||||
|
||||
const (
|
||||
Invalid Permission = iota
|
||||
Public
|
||||
Private
|
||||
)
|
||||
|
||||
func ParsePermString(p string) Permission {
|
||||
switch p {
|
||||
case "1":
|
||||
return Public
|
||||
case "2":
|
||||
return Private
|
||||
}
|
||||
return Invalid
|
||||
}
|
||||
|
||||
func ReadPerm(username string, p Permission, s Section) bool {
|
||||
switch p {
|
||||
case Public:
|
||||
return true
|
||||
case Private:
|
||||
_, c := Contains(username, s.Members)
|
||||
return c
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func WritePerm(username string, p Permission, s Section) bool {
|
||||
switch p {
|
||||
case Public, Private:
|
||||
_, c := Contains(username, s.Members)
|
||||
return c
|
||||
}
|
||||
return false
|
||||
}
|
||||
77
pkg/core/permission_test.go
Normal file
77
pkg/core/permission_test.go
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package core
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestParsePermString(t *testing.T) {
|
||||
data := map[string]Permission{
|
||||
"": Invalid,
|
||||
"1": Public,
|
||||
"2": Private,
|
||||
"123": Invalid,
|
||||
"test": Invalid,
|
||||
}
|
||||
for k, v := range data {
|
||||
t.Run(k, func(t *testing.T) {
|
||||
if ParsePermString(k) != v {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReadPerm(t *testing.T) {
|
||||
data := []struct {
|
||||
Username string
|
||||
Perm Permission
|
||||
Section Section
|
||||
OK bool
|
||||
}{
|
||||
{"", Invalid, Section{Members: []string{"admin"}}, false},
|
||||
{"", Public, Section{Members: []string{"admin"}}, true},
|
||||
{"user", Public, Section{Members: []string{"admin"}}, true},
|
||||
{"", Private, Section{Members: []string{"admin"}}, false},
|
||||
{"user", Private, Section{Members: []string{"admin"}}, false},
|
||||
{"", Private, Section{Members: []string{"user"}}, false},
|
||||
{"user", Private, Section{Members: []string{"user"}}, true},
|
||||
{"user", Private, Section{Members: []string{"user", "admin"}}, true},
|
||||
{"admin", Private, Section{Members: []string{"user"}}, false},
|
||||
}
|
||||
for i, v := range data {
|
||||
t.Run(fmt.Sprintf("Read:%d", i), func(t *testing.T) {
|
||||
if ReadPerm(v.Username, v.Perm, v.Section) != v.OK {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWritePerm(t *testing.T) {
|
||||
data := []struct {
|
||||
Username string
|
||||
Perm Permission
|
||||
Section Section
|
||||
OK bool
|
||||
}{
|
||||
{"", Invalid, Section{Members: []string{"user"}}, false},
|
||||
{"", Public, Section{Members: []string{"user"}}, false},
|
||||
{"user", Public, Section{Members: []string{"user"}}, true},
|
||||
{"user", Public, Section{Members: []string{"admin"}}, false},
|
||||
{"user", Private, Section{Members: []string{"user"}}, true},
|
||||
{"", Private, Section{Members: []string{"user"}}, false},
|
||||
{"", Private, Section{Members: []string{"user", "admin"}}, false},
|
||||
{"user", Private, Section{Members: []string{"user", "admin"}}, true},
|
||||
{"admin", Private, Section{Members: []string{"user", "admin"}}, true},
|
||||
}
|
||||
for i, v := range data {
|
||||
t.Run(fmt.Sprintf("Write:%d", i), func(t *testing.T) {
|
||||
if WritePerm(v.Username, v.Perm, v.Section) != v.OK {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
94
pkg/core/types.go
Normal file
94
pkg/core/types.go
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package core
|
||||
|
||||
import "html/template"
|
||||
|
||||
type Page struct {
|
||||
Title string `json:"title"`
|
||||
StoreTitle string `json:"store_title"`
|
||||
Markdown string `json:"markdown"`
|
||||
HTML template.HTML `json:"html"`
|
||||
Created string `json:"created"`
|
||||
Updated string `json:"updated"`
|
||||
Perm Permission `json:"perm"`
|
||||
}
|
||||
|
||||
type Pages []Page
|
||||
|
||||
func (p Pages) Len() int { return len(p) }
|
||||
func (p Pages) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
||||
func (p Pages) Less(i, j int) bool { return p[i].StoreTitle < p[j].StoreTitle }
|
||||
|
||||
type UpdatedPages []Page
|
||||
|
||||
func (p UpdatedPages) Len() int { return len(p) }
|
||||
func (p UpdatedPages) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
|
||||
func (p UpdatedPages) Less(i, j int) bool { return p[i].Updated > p[j].Updated }
|
||||
|
||||
type Section struct {
|
||||
Name string
|
||||
User bool
|
||||
Members []string
|
||||
}
|
||||
|
||||
type Sections []Section
|
||||
|
||||
func (s Sections) Len() int { return len(s) }
|
||||
func (s Sections) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
func (s Sections) Less(i, j int) bool { return s[i].Name < s[j].Name }
|
||||
|
||||
type SectionPage struct {
|
||||
Section string
|
||||
Pages UpdatedPages
|
||||
}
|
||||
|
||||
type SectionPages []SectionPage
|
||||
|
||||
func (s SectionPages) Len() int { return len(s) }
|
||||
func (s SectionPages) Swap(i, j int) { s[i], s[j] = s[j], s[i] }
|
||||
func (s SectionPages) Less(i, j int) bool { return s[i].Section < s[j].Section }
|
||||
|
||||
type User struct {
|
||||
Username string
|
||||
Password string
|
||||
Created string
|
||||
Secret string
|
||||
Admin bool
|
||||
Locked int
|
||||
}
|
||||
|
||||
type Users []*User
|
||||
|
||||
func (u Users) Len() int { return len(u) }
|
||||
func (u Users) Swap(i, j int) { u[i], u[j] = u[j], u[i] }
|
||||
func (u Users) Less(i, j int) bool { return u[i].Username < u[j].Username }
|
||||
|
||||
type Result struct {
|
||||
Title string
|
||||
Section string
|
||||
StoreTitle string
|
||||
HTML template.HTML
|
||||
}
|
||||
|
||||
type Stats struct {
|
||||
Goroutines int
|
||||
Alloc string
|
||||
Sys string
|
||||
Docs uint64
|
||||
GoVersion string
|
||||
}
|
||||
|
||||
type Config struct {
|
||||
DataDir string `json:"data_dir,omitempty"`
|
||||
User string `json:"user,omitempty"`
|
||||
Group string `json:"group,omitempty"`
|
||||
ListenAddr string `json:"listen_addr,omitempty"`
|
||||
LogFile string `json:"log_file,omitempty"`
|
||||
Version string `json:"-"`
|
||||
Secret string `json:"secret,omitempty"`
|
||||
Admin bool `json:"admin,omitempty"`
|
||||
Foreground bool `json:"foreground,omitempty"`
|
||||
SecureCookie bool `json:"secure_cookie,omitempty"`
|
||||
Debug bool `json:"debug,omitempty"`
|
||||
}
|
||||
33
pkg/db/blacklist.go
Normal file
33
pkg/db/blacklist.go
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package db
|
||||
|
||||
import "git.giftfish.de/ston1th/jwt/v3"
|
||||
|
||||
const blacklistPrefix = "blacklist/"
|
||||
|
||||
func (db *DB) Add(sig string, exp int64) error {
|
||||
return db.store.Set(blacklistPrefix+sig, exp)
|
||||
}
|
||||
|
||||
func (db *DB) Remove(sig string) error {
|
||||
return db.store.Delete(blacklistPrefix + sig)
|
||||
}
|
||||
|
||||
func (db *DB) Check(sig string) (ok bool) {
|
||||
return db.store.Get(blacklistPrefix+sig, nil) == nil
|
||||
}
|
||||
|
||||
func (db *DB) Map() (list jwt.BlacklistMap, err error) {
|
||||
list = make(jwt.BlacklistMap)
|
||||
err = db.store.ForEachPrefix(blacklistPrefix, func(k string, v []byte) error {
|
||||
var exp int64
|
||||
err := db.store.Unmarshal(v, &exp)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
list[k] = exp
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
122
pkg/db/db.go
Normal file
122
pkg/db/db.go
Normal file
|
|
@ -0,0 +1,122 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"git.giftfish.de/ston1th/godrop/v2"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/cache"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/index"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/render"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/store"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
const (
|
||||
defUser = "admin"
|
||||
defPassword = "gowiki"
|
||||
storeFile = "store.db"
|
||||
bcryptCost = 13
|
||||
welcome = `# Welcome to GoWiki
|
||||
This is the [Index](/wiki/Index) page.
|
||||
You can customize it how you like.`
|
||||
)
|
||||
|
||||
const blevePath = "bleve"
|
||||
|
||||
type DB struct {
|
||||
admin bool
|
||||
store store.Store
|
||||
cache *cache.Cache
|
||||
Index *index.Index
|
||||
indexPath string
|
||||
}
|
||||
|
||||
func NewPlain(cfg *core.Config) (db *DB, err error) {
|
||||
db = &DB{admin: cfg.Admin, cache: cache.NewCache()}
|
||||
dbFile := filepath.Join(cfg.DataDir, storeFile)
|
||||
err = godrop.Unveil(dbFile, "rwc")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
db.store, err = store.NewBoltStore(dbFile, nil)
|
||||
return
|
||||
}
|
||||
|
||||
func New(cfg *core.Config) (db *DB, err error) {
|
||||
db, err = NewPlain(cfg)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
db.indexPath = filepath.Join(cfg.DataDir, blevePath)
|
||||
db.Index, err = index.New(db.indexPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if db.UserExists(defUser) != nil {
|
||||
err = db.CreateUser(defUser, defPassword, true)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
err = db.UnlockUser(defUser)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if db.SectionExists(core.WikiSection) != nil {
|
||||
err = db.CreateSection(core.WikiSection, []string{defUser}, false)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
_, err = db.CreatePage(core.IndexPage, core.WikiSection, welcome, defUser, core.Public)
|
||||
if err != errPageExists && err != nil {
|
||||
return
|
||||
}
|
||||
err = db.RunMigrations()
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) Dump(w io.Writer) error {
|
||||
return db.store.Dump(w)
|
||||
}
|
||||
|
||||
func (db *DB) Restore(r io.Reader) (err error) {
|
||||
err = db.Index.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = os.RemoveAll(db.indexPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
db.Index, err = index.New(db.indexPath)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = db.store.Restore(r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = db.store.ForEachPrefix(pagePrefix, func(k string, _ []byte) error {
|
||||
p := new(core.Page)
|
||||
db.store.Get(pagePrefix+k, p)
|
||||
db.Index.Add(p.Title, p.StoreTitle, render.Render(p))
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) Close() (err error) {
|
||||
err = db.store.Close()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if db.Index != nil {
|
||||
return db.Index.Close()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
55
pkg/db/helper.go
Normal file
55
pkg/db/helper.go
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
"time"
|
||||
)
|
||||
|
||||
func init() {
|
||||
reservedNames = make([]string, len(reservedURIs))
|
||||
for i, v := range reservedURIs {
|
||||
reservedNames[i] = v[1:]
|
||||
}
|
||||
}
|
||||
|
||||
var reservedURIs = []string{
|
||||
core.LoginURI,
|
||||
core.LogoutURI,
|
||||
core.TotpURI,
|
||||
core.SectionsURI,
|
||||
core.SectionURI,
|
||||
core.SearchURI,
|
||||
core.StatsURI,
|
||||
core.NewURI,
|
||||
core.BlacklistURI,
|
||||
core.UsersURI,
|
||||
core.UserURI,
|
||||
core.ViewURI,
|
||||
core.StaticURI,
|
||||
}
|
||||
|
||||
var reservedNames []string
|
||||
|
||||
func validatePassword(pw string) (b []byte) {
|
||||
b = []byte(pw)
|
||||
if len(pw) <= 56 {
|
||||
return
|
||||
}
|
||||
hash := sha256.New()
|
||||
hash.Write(b)
|
||||
b = hash.Sum(nil)
|
||||
return
|
||||
}
|
||||
|
||||
const timeFmt = "2006-01-02 15:04:05"
|
||||
|
||||
func now() string {
|
||||
return time.Now().Format(timeFmt)
|
||||
}
|
||||
|
||||
func created(username string) string {
|
||||
return username + " " + now()
|
||||
}
|
||||
13
pkg/db/lockout.go
Normal file
13
pkg/db/lockout.go
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package db
|
||||
|
||||
const lockoutPrefix = "lockout/"
|
||||
|
||||
func (db *DB) lockout(username, created string) error {
|
||||
return db.store.Set(lockoutPrefix+username+created, nil)
|
||||
}
|
||||
|
||||
func (db *DB) LockedOut(username, created string) bool {
|
||||
return db.store.Get(lockoutPrefix+username+created, nil) == nil
|
||||
}
|
||||
115
pkg/db/migration.go
Normal file
115
pkg/db/migration.go
Normal file
|
|
@ -0,0 +1,115 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/log"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/render"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/store"
|
||||
)
|
||||
|
||||
const (
|
||||
versionKey = "version/version"
|
||||
currentVersion = 2
|
||||
)
|
||||
|
||||
type migrator func(*DB, int) error
|
||||
|
||||
var migrators = []migrator{
|
||||
func(_ *DB, _ int) error {
|
||||
return nil
|
||||
},
|
||||
func(db *DB, version int) (err error) {
|
||||
// recreate all pages with new format
|
||||
var pages []string
|
||||
db.store.ForEachPrefix(pagePrefix, func(k string, _ []byte) error {
|
||||
pages = append(pages, k)
|
||||
return nil
|
||||
})
|
||||
var p core.Page
|
||||
for _, v := range pages {
|
||||
k := pagePrefix + v
|
||||
err := db.store.Get(k, &p)
|
||||
if err != nil {
|
||||
log.Printf("db: migrator[%d]: pageGet %s: %s", version, k, err)
|
||||
continue
|
||||
}
|
||||
if p.Perm == 3 {
|
||||
p.Perm = core.Private
|
||||
}
|
||||
err = db.store.Set(k, &p)
|
||||
if err != nil {
|
||||
log.Printf("db: migrator[%d]: pageSet %s: %s", version, k, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
// create user sections
|
||||
users, err := db.GetUsers()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, u := range users {
|
||||
err = db.CreateSection(u.Username, []string{u.Username}, true)
|
||||
if err != nil {
|
||||
log.Printf("db: migrator[%d]: userSection %s: %s", version, u.Username, err)
|
||||
}
|
||||
}
|
||||
return
|
||||
},
|
||||
func(db *DB, version int) (err error) {
|
||||
// re-render all pages
|
||||
var pages []string
|
||||
db.store.ForEachPrefix(pagePrefix, func(k string, _ []byte) error {
|
||||
pages = append(pages, k)
|
||||
return nil
|
||||
})
|
||||
var p core.Page
|
||||
for _, v := range pages {
|
||||
k := pagePrefix + v
|
||||
err := db.store.Get(k, &p)
|
||||
if err != nil {
|
||||
log.Printf("db: migrator[%d]: pageGet %s: %s", version, k, err)
|
||||
continue
|
||||
}
|
||||
render.Render(&p)
|
||||
err = db.store.Set(k, &p)
|
||||
if err != nil {
|
||||
log.Printf("db: migrator[%d]: pageSet %s: %s", version, k, err)
|
||||
continue
|
||||
}
|
||||
}
|
||||
return
|
||||
},
|
||||
}
|
||||
|
||||
func (db *DB) RunMigrations() (err error) {
|
||||
var version int
|
||||
err = db.store.Get(versionKey, &version)
|
||||
if err == store.ErrKeyNotFound {
|
||||
err = db.store.Set(versionKey, currentVersion)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = db.store.Get(versionKey, &version)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
log.Printf("db: database version %d", version)
|
||||
for version < currentVersion {
|
||||
v := version + 1
|
||||
log.Printf("db: running migration version %d", v)
|
||||
err = migrators[v](db, v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = db.store.Set(versionKey, v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
version = v
|
||||
log.Printf("db: database version %d", version)
|
||||
}
|
||||
return
|
||||
}
|
||||
241
pkg/db/page.go
Normal file
241
pkg/db/page.go
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/render"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/store"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const pagePrefix = "page/"
|
||||
|
||||
var (
|
||||
errPageNotFound = store.ErrKeyNotFound
|
||||
errPageExists = errors.New("db: page already exists")
|
||||
errPermissionDenied = errors.New("db: permission denied")
|
||||
errInvalidPermission = errors.New("db: invalid permission")
|
||||
errPrivateWikiPage = errors.New("db: private wiki pages are not allowed")
|
||||
errDeleteIndexPage = errors.New("db: index page can not be deleted")
|
||||
)
|
||||
|
||||
func (db *DB) GetAllSections(username string, admin bool) (sections core.SectionPages) {
|
||||
m := make(map[string]core.UpdatedPages)
|
||||
db.store.ForEachPrefix(pagePrefix, func(k string, _ []byte) error {
|
||||
if k == core.IndexURI {
|
||||
return nil
|
||||
}
|
||||
p, err := db.getPage(k, username)
|
||||
if err == nil {
|
||||
i := strings.Index(k, "/")
|
||||
if i == -1 {
|
||||
return nil
|
||||
}
|
||||
s := k[:i]
|
||||
if _, ok := m[s]; !ok {
|
||||
m[s] = core.UpdatedPages{}
|
||||
}
|
||||
m[s] = append(m[s], *p)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if admin {
|
||||
secs, err := db.GetSections()
|
||||
if err == nil {
|
||||
for _, s := range secs {
|
||||
if _, ok := m[s.Name]; !ok {
|
||||
m[s.Name] = core.UpdatedPages{}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
for s, p := range m {
|
||||
sort.Sort(p)
|
||||
l := len(p)
|
||||
if l > 5 {
|
||||
l = 5
|
||||
}
|
||||
sections = append(sections, core.SectionPage{
|
||||
Section: s,
|
||||
Pages: p[:l],
|
||||
})
|
||||
}
|
||||
sort.Sort(sections)
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) GetAllSectionPages(section, username string, admin bool) (pages core.Pages, err error) {
|
||||
err = db.SectionExists(section)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
db.store.ForEachPrefix(pagePrefix, func(k string, _ []byte) error {
|
||||
if !strings.HasPrefix(k, section+"/") {
|
||||
return nil
|
||||
}
|
||||
if k == core.IndexURI {
|
||||
return nil
|
||||
}
|
||||
p, err := db.getPage(k, username)
|
||||
if err == nil {
|
||||
pages = append(pages, *p)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
if len(pages) == 0 && !admin {
|
||||
err = errPageNotFound
|
||||
return
|
||||
}
|
||||
sort.Sort(pages)
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) ValidatePage(st, username string) bool {
|
||||
p := db.cache.Get(st)
|
||||
if p == nil {
|
||||
p, err := db.getPage(st, username)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
db.cache.Add(st, p)
|
||||
return true
|
||||
}
|
||||
return db.ReadPerm(st, username, p.Perm)
|
||||
}
|
||||
|
||||
func (db *DB) GetPage(section, title, username string) (p *core.Page, err error) {
|
||||
st := render.StoreTitle(section, title)
|
||||
p = db.cache.Get(st)
|
||||
if p == nil {
|
||||
p, err = db.getPage(st, username)
|
||||
db.cache.Add(st, p)
|
||||
return
|
||||
}
|
||||
if !db.ReadPerm(section, username, p.Perm) {
|
||||
err = errPageNotFound
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) getPage(title, username string) (p *core.Page, err error) {
|
||||
p = new(core.Page)
|
||||
err = db.store.Get(pagePrefix+title, p)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !db.ReadPerm(title, username, p.Perm) {
|
||||
err = errPageNotFound
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) CreatePage(title, section, markdown, username string, p core.Permission) (page *core.Page, err error) {
|
||||
if p == core.Invalid {
|
||||
err = errInvalidPermission
|
||||
return
|
||||
}
|
||||
if section == core.WikiSection && p == core.Private {
|
||||
err = errPrivateWikiPage
|
||||
return
|
||||
}
|
||||
st := render.StoreTitle(section, render.Title(title))
|
||||
_, err = db.getPage(st, username)
|
||||
if err != store.ErrKeyNotFound {
|
||||
if err == nil {
|
||||
err = errPageExists
|
||||
}
|
||||
return
|
||||
}
|
||||
s, err := db.GetSection(section)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if _, ok := core.Contains(username, s.Members); !ok {
|
||||
err = errPermissionDenied
|
||||
return
|
||||
}
|
||||
|
||||
page = &core.Page{
|
||||
Title: title,
|
||||
StoreTitle: st,
|
||||
Markdown: markdown,
|
||||
Created: created(username),
|
||||
Perm: p,
|
||||
}
|
||||
search := render.Render(page)
|
||||
|
||||
err = db.store.Set(pagePrefix+st, page)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = db.Index.Add(title, st, search)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
db.cache.Add(st, page)
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) UpdatePage(title, section, markdown, username string, p core.Permission) (err error) {
|
||||
if p == core.Invalid {
|
||||
err = errInvalidPermission
|
||||
return
|
||||
}
|
||||
if section == core.WikiSection && p == core.Private {
|
||||
err = errPrivateWikiPage
|
||||
return
|
||||
}
|
||||
st := render.StoreTitle(section, title)
|
||||
page, err := db.getPage(st, username)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if !db.WritePerm(section, username, page.Perm) {
|
||||
return errPermissionDenied
|
||||
}
|
||||
|
||||
page.Markdown = markdown
|
||||
page.Updated = created(username)
|
||||
page.Perm = p
|
||||
search := render.Render(page)
|
||||
|
||||
err = db.store.Set(pagePrefix+st, page)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
err = db.Index.Add(page.Title, st, search)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
db.cache.Add(st, page)
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) deletePage(st string) (err error) {
|
||||
if err = db.store.Delete(pagePrefix + st); err != nil {
|
||||
return
|
||||
}
|
||||
db.cache.Delete(st)
|
||||
return db.Index.Delete(st)
|
||||
}
|
||||
|
||||
func (db *DB) DeletePage(title, section, username string) (err error) {
|
||||
var page *core.Page
|
||||
if section == core.WikiSection && title == core.IndexPage {
|
||||
err = errDeleteIndexPage
|
||||
return
|
||||
}
|
||||
st := render.StoreTitle(section, title)
|
||||
if page, err = db.getPage(st, username); err != nil {
|
||||
return
|
||||
}
|
||||
if !db.WritePerm(section, username, page.Perm) {
|
||||
return errPermissionDenied
|
||||
}
|
||||
return db.deletePage(st)
|
||||
}
|
||||
29
pkg/db/permission.go
Normal file
29
pkg/db/permission.go
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/log"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/render"
|
||||
)
|
||||
|
||||
func (db *DB) ReadPerm(st, username string, p core.Permission) bool {
|
||||
section, _ := render.SplitStoreTitle(st)
|
||||
s, err := db.GetSection(section)
|
||||
if err != nil {
|
||||
log.Debugf("db: read: %s", err)
|
||||
return false
|
||||
}
|
||||
return core.ReadPerm(username, p, s)
|
||||
}
|
||||
|
||||
func (db *DB) WritePerm(st, username string, p core.Permission) bool {
|
||||
section, _ := render.SplitStoreTitle(st)
|
||||
s, err := db.GetSection(section)
|
||||
if err != nil {
|
||||
log.Debugf("db: read: %s", err)
|
||||
return false
|
||||
}
|
||||
return core.WritePerm(username, p, s)
|
||||
}
|
||||
118
pkg/db/section.go
Normal file
118
pkg/db/section.go
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/log"
|
||||
"sort"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const sectionPrefix = "section/"
|
||||
|
||||
var (
|
||||
errSectionExists = errors.New("db: section already exist")
|
||||
)
|
||||
|
||||
func (db *DB) GetSections() (secs core.Sections, err error) {
|
||||
err = db.store.ForEachPrefix(sectionPrefix, func(k string, _ []byte) error {
|
||||
s, err := db.GetSection(k)
|
||||
if err == nil {
|
||||
secs = append(secs, s)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
sort.Sort(secs)
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) GetUserSections(username string) (sections map[string]bool, err error) {
|
||||
sections = make(map[string]bool)
|
||||
secs, err := db.GetSections()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, s := range secs {
|
||||
if _, ok := core.Contains(username, s.Members); ok {
|
||||
sections[s.Name] = false
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) GetSection(section string) (s core.Section, err error) {
|
||||
err = db.store.Get(sectionPrefix+section, &s)
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) SectionExists(section string) error {
|
||||
return db.store.Get(sectionPrefix+section, nil)
|
||||
}
|
||||
|
||||
func (db *DB) CreateSection(section string, members []string, user bool) error {
|
||||
section = userRe.ReplaceAllString(section, "")
|
||||
if _, ok := core.Contains(section, reservedNames); ok {
|
||||
return fmt.Errorf("%s: %s", errNameReserved, section)
|
||||
}
|
||||
if err := db.SectionExists(section); err == nil {
|
||||
return errSectionExists
|
||||
}
|
||||
return db.UpdateSection(section, members, user)
|
||||
}
|
||||
|
||||
func (db *DB) UpdateSection(section string, members []string, user bool) error {
|
||||
section = userRe.ReplaceAllString(section, "")
|
||||
if _, ok := core.Contains(section, reservedNames); ok {
|
||||
return fmt.Errorf("%s: %s", errNameReserved, section)
|
||||
}
|
||||
var m []string
|
||||
if user {
|
||||
m = append(m, section)
|
||||
} else {
|
||||
for _, v := range members {
|
||||
if v == "" {
|
||||
continue
|
||||
}
|
||||
if err := db.UserExists(v); err == nil {
|
||||
m = append(m, v)
|
||||
}
|
||||
}
|
||||
}
|
||||
return db.store.Set(sectionPrefix+section, &core.Section{
|
||||
Name: section,
|
||||
User: user,
|
||||
Members: m,
|
||||
})
|
||||
}
|
||||
|
||||
func (db *DB) DeleteSection(section string) error {
|
||||
if section == core.WikiSection {
|
||||
return errors.New("section '" + core.WikiSection + "' cannot be deleted")
|
||||
}
|
||||
err := db.SectionExists(section)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = db.store.Delete(sectionPrefix + section)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
section += "/"
|
||||
var pages []string
|
||||
db.store.ForEachPrefix(pagePrefix, func(k string, _ []byte) error {
|
||||
if strings.HasPrefix(k, section) {
|
||||
pages = append(pages, k)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
for _, p := range pages {
|
||||
err := db.deletePage(p)
|
||||
if err != nil {
|
||||
log.Debugf("DeleteSection %s, %s", section, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
230
pkg/db/user.go
Normal file
230
pkg/db/user.go
Normal file
|
|
@ -0,0 +1,230 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/otp"
|
||||
"git.giftfish.de/ston1th/jwt/v3"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"regexp"
|
||||
"sort"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
const userPrefix = "user/"
|
||||
|
||||
var (
|
||||
errUserLocked = errors.New("db: user locked")
|
||||
errUserLogin = errors.New("db: wrong login")
|
||||
errUserPin = errors.New("db: wrong pin")
|
||||
errUserExists = errors.New("db: user already exist")
|
||||
errNameReserved = errors.New("db: name is reserved")
|
||||
|
||||
userRe = regexp.MustCompile("[^a-zA-Z0-9]+")
|
||||
)
|
||||
|
||||
func (db *DB) GetUsers() (users core.Users, err error) {
|
||||
err = db.store.ForEachPrefix(userPrefix, func(k string, _ []byte) error {
|
||||
u, err := db.GetUser(k)
|
||||
u.Password = ""
|
||||
if err == nil {
|
||||
users = append(users, u)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
sort.Sort(users)
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) GetUserWithoutPassword(username string) (u *core.User, err error) {
|
||||
u, err = db.GetUser(username)
|
||||
u.Password = ""
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) GetUserSecret(username string) (secret string, err error) {
|
||||
var user *core.User
|
||||
user, err = db.GetUser(username)
|
||||
secret = user.Secret
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) GetUser(username string) (u *core.User, err error) {
|
||||
u = new(core.User)
|
||||
err = db.store.Get(userPrefix+username, u)
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) UserExists(username string) error {
|
||||
return db.store.Get(userPrefix+username, nil)
|
||||
}
|
||||
|
||||
func (db *DB) CreateUser(username, password string, admin bool) error {
|
||||
username = userRe.ReplaceAllString(username, "")
|
||||
if _, ok := core.Contains(username, reservedNames); ok {
|
||||
return fmt.Errorf("%s: %s", errNameReserved, username)
|
||||
}
|
||||
if err := db.UserExists(username); err == nil {
|
||||
return errUserExists
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword(validatePassword(password), bcryptCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = db.CreateSection(username, nil, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.store.Set(userPrefix+username, &core.User{
|
||||
Username: username,
|
||||
Password: string(hash),
|
||||
Created: strconv.Itoa(int(jwt.Now())),
|
||||
Admin: admin,
|
||||
})
|
||||
}
|
||||
|
||||
func (db *DB) Login(username, password string) (u *core.User, err error) {
|
||||
if username == defUser && !db.admin {
|
||||
err = errors.New("db: user disabled")
|
||||
return
|
||||
}
|
||||
u, err = db.GetUser(username)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if u.Locked == 3 {
|
||||
err = errUserLocked
|
||||
return
|
||||
}
|
||||
if err = bcrypt.CompareHashAndPassword([]byte(u.Password), validatePassword(password)); err != nil {
|
||||
u.Locked++
|
||||
err = db.store.Set(userPrefix+username, u)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = errUserLogin
|
||||
return
|
||||
}
|
||||
if u.Locked > 0 {
|
||||
u.Locked = 0
|
||||
err = db.store.Set(userPrefix+username, u)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) Totp(username, pin string) (u *core.User, err error) {
|
||||
u, err = db.GetUser(username)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if u.Locked == 3 {
|
||||
err = errUserLocked
|
||||
return
|
||||
}
|
||||
if !otp.Validate(pin, u.Secret) {
|
||||
u.Locked++
|
||||
err = db.store.Set(userPrefix+username, u)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = errUserPin
|
||||
return
|
||||
}
|
||||
if u.Locked > 0 {
|
||||
u.Locked = 0
|
||||
err = db.store.Set(userPrefix+username, u)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) UnlockUser(username string) (err error) {
|
||||
var u *core.User
|
||||
u, err = db.GetUser(username)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if u.Locked > 0 {
|
||||
u.Locked = 0
|
||||
err = db.store.Set(userPrefix+username, u)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) UpdateUserSecret(username, secret string) error {
|
||||
u, err := db.GetUser(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Secret = secret
|
||||
return db.store.Set(userPrefix+username, u)
|
||||
}
|
||||
|
||||
func (db *DB) UpdateUserPassword(username, password string) error {
|
||||
if password == "" {
|
||||
return errors.New("empty password")
|
||||
}
|
||||
u, err := db.GetUser(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
hash, err := bcrypt.GenerateFromPassword(validatePassword(password), bcryptCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Password = string(hash)
|
||||
return db.store.Set(userPrefix+username, u)
|
||||
}
|
||||
|
||||
func (db *DB) ResetAdmin() error {
|
||||
return db.UpdateUserPassword(defUser, defPassword)
|
||||
}
|
||||
|
||||
func (db *DB) AdminUpdateUser(username, password string, admin bool) error {
|
||||
if username == defUser && !admin {
|
||||
return errors.New("user '" + defUser + "' cannot lose admin privileges")
|
||||
}
|
||||
u, err := db.GetUser(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Admin = admin
|
||||
if password != "" {
|
||||
hash, err := bcrypt.GenerateFromPassword(validatePassword(password), bcryptCost)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Password = string(hash)
|
||||
}
|
||||
return db.store.Set(userPrefix+username, u)
|
||||
}
|
||||
|
||||
func (db *DB) DeleteUser(username string) error {
|
||||
if username == defUser {
|
||||
return errors.New("user '" + defUser + "' cannot be deleted")
|
||||
}
|
||||
u, err := db.GetUser(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = db.store.Delete(userPrefix + username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = db.lockout(username, u.Created)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.DeleteSection(username)
|
||||
}
|
||||
130
pkg/index/index.go
Normal file
130
pkg/index/index.go
Normal file
|
|
@ -0,0 +1,130 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package index
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"git.giftfish.de/ston1th/godrop/v2"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/render"
|
||||
"github.com/blevesearch/bleve"
|
||||
"github.com/blevesearch/bleve/mapping"
|
||||
"html/template"
|
||||
"os"
|
||||
)
|
||||
|
||||
const (
|
||||
maxSearchResult = 101
|
||||
docType = "page"
|
||||
titleField = "title"
|
||||
storeTitleField = "store_title"
|
||||
searchField = "search"
|
||||
)
|
||||
|
||||
type indexPage struct {
|
||||
Title string `json:"title"`
|
||||
StoreTitle string `json:"store_title"`
|
||||
Search string `json:"search"`
|
||||
}
|
||||
|
||||
func (*indexPage) Type() string {
|
||||
return docType
|
||||
}
|
||||
|
||||
type Index struct {
|
||||
i bleve.Index
|
||||
}
|
||||
|
||||
func New(path string) (i *Index, err error) {
|
||||
var bi bleve.Index
|
||||
err = godrop.Unveil(path, "rwc")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if _, err = os.Stat(path); os.IsNotExist(err) {
|
||||
err = os.Mkdir(path, 0o700)
|
||||
if err != nil {
|
||||
err = errors.New("index: " + err.Error())
|
||||
return
|
||||
}
|
||||
err = godrop.Unveil(path, "rwc")
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var m *mapping.IndexMappingImpl
|
||||
m, err = createMapping()
|
||||
if err != nil {
|
||||
err = errors.New("mapping: " + err.Error())
|
||||
return
|
||||
}
|
||||
bi, err = bleve.New(path, m)
|
||||
if err != nil {
|
||||
err = errors.New("index: " + err.Error())
|
||||
return
|
||||
}
|
||||
} else {
|
||||
bi, err = bleve.Open(path)
|
||||
if err != nil {
|
||||
err = errors.New("index: " + err.Error())
|
||||
return
|
||||
}
|
||||
}
|
||||
i = &Index{i: bi}
|
||||
return
|
||||
}
|
||||
|
||||
func (i *Index) DocCount() (c uint64) {
|
||||
c, _ = i.i.DocCount()
|
||||
return
|
||||
}
|
||||
|
||||
func (i *Index) Close() error {
|
||||
return i.i.Close()
|
||||
}
|
||||
|
||||
func (i *Index) Add(title, storeTitle, search string) error {
|
||||
doc, _ := i.i.Document(storeTitle)
|
||||
if doc != nil {
|
||||
err := i.Delete(storeTitle)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return i.i.Index(storeTitle, &indexPage{title, storeTitle, search})
|
||||
}
|
||||
|
||||
func (i *Index) Delete(storeTitle string) error {
|
||||
return i.i.Delete(storeTitle)
|
||||
}
|
||||
|
||||
func (i *Index) Search(search string) (results []core.Result, err error) {
|
||||
req := bleve.NewSearchRequest(bleve.NewQueryStringQuery(search))
|
||||
req.Highlight = bleve.NewHighlightWithStyle("html")
|
||||
req.Size = maxSearchResult
|
||||
res, err := i.i.Search(req)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
for _, hit := range res.Hits {
|
||||
if hit == nil {
|
||||
continue
|
||||
}
|
||||
r := core.Result{StoreTitle: hit.ID}
|
||||
r.Section, r.Title = render.SplitStoreTitle(hit.ID)
|
||||
r.Title = render.Untitle(r.Title)
|
||||
d, err := i.i.Document(hit.ID)
|
||||
if d == nil || err != nil {
|
||||
continue
|
||||
}
|
||||
for fragField, frags := range hit.Fragments {
|
||||
if fragField == titleField || fragField == searchField {
|
||||
for _, f := range frags {
|
||||
r.HTML += template.HTML(f)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
results = append(results, r)
|
||||
}
|
||||
return
|
||||
}
|
||||
56
pkg/index/mapping.go
Normal file
56
pkg/index/mapping.go
Normal file
|
|
@ -0,0 +1,56 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package index
|
||||
|
||||
import (
|
||||
"github.com/blevesearch/bleve"
|
||||
"github.com/blevesearch/bleve/analysis/analyzer/custom"
|
||||
"github.com/blevesearch/bleve/analysis/token/edgengram"
|
||||
"github.com/blevesearch/bleve/analysis/token/lowercase"
|
||||
"github.com/blevesearch/bleve/analysis/tokenizer/unicode"
|
||||
"github.com/blevesearch/bleve/mapping"
|
||||
)
|
||||
|
||||
const (
|
||||
bigram = "bigram_tokenfilter"
|
||||
ngram = "fulltext_ngram"
|
||||
)
|
||||
|
||||
func createMapping() (m *mapping.IndexMappingImpl, err error) {
|
||||
m = bleve.NewIndexMapping()
|
||||
|
||||
err = m.AddCustomTokenFilter(bigram, map[string]interface{}{
|
||||
"type": edgengram.Name,
|
||||
"side": edgengram.FRONT,
|
||||
"min": 3.0,
|
||||
"max": 25.0,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = m.AddCustomAnalyzer(ngram, map[string]interface{}{
|
||||
"type": custom.Name,
|
||||
"tokenizer": unicode.Name,
|
||||
"token_filters": []string{
|
||||
lowercase.Name,
|
||||
bigram,
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
doc := bleve.NewDocumentMapping()
|
||||
ti := bleve.NewTextFieldMapping()
|
||||
ti.Analyzer = ngram
|
||||
doc.AddFieldMappingsAt(titleField, ti)
|
||||
st := bleve.NewTextFieldMapping()
|
||||
st.Analyzer = ngram
|
||||
doc.AddFieldMappingsAt(storeTitleField, st)
|
||||
s := bleve.NewTextFieldMapping()
|
||||
s.Analyzer = ngram
|
||||
doc.AddFieldMappingsAt(searchField, s)
|
||||
|
||||
m.AddDocumentMapping(docType, doc)
|
||||
return
|
||||
}
|
||||
63
pkg/log/log.go
Normal file
63
pkg/log/log.go
Normal file
|
|
@ -0,0 +1,63 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package log
|
||||
|
||||
import (
|
||||
"git.giftfish.de/ston1th/godrop/v2"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
stdlog "log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
)
|
||||
|
||||
var (
|
||||
log = stdlog.New(os.Stdout, "", stdlog.LstdFlags)
|
||||
debug = false
|
||||
)
|
||||
|
||||
func Stderr() {
|
||||
log = stdlog.New(os.Stderr, "", stdlog.LstdFlags)
|
||||
}
|
||||
|
||||
func InitLogger(cfg *core.Config) {
|
||||
debug = cfg.Debug
|
||||
if cfg.LogFile == "-" {
|
||||
return
|
||||
}
|
||||
logfile := filepath.Join(cfg.DataDir, cfg.LogFile)
|
||||
err := godrop.Unveil(logfile, "rwc")
|
||||
if err != nil {
|
||||
stdlog.Fatal(err)
|
||||
return
|
||||
}
|
||||
f, err := os.OpenFile(logfile, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0o600)
|
||||
if err != nil {
|
||||
stdlog.Fatal(err)
|
||||
}
|
||||
stdlog.SetOutput(f)
|
||||
log = stdlog.New(f, "", stdlog.LstdFlags)
|
||||
}
|
||||
|
||||
func Println(v ...interface{}) {
|
||||
log.Println(v...)
|
||||
}
|
||||
|
||||
func Printf(fmt string, v ...interface{}) {
|
||||
log.Printf(fmt, v...)
|
||||
}
|
||||
|
||||
func Fatal(v ...interface{}) {
|
||||
log.Fatal(v...)
|
||||
}
|
||||
|
||||
func Debug(v ...interface{}) {
|
||||
if debug {
|
||||
log.Println(append([]interface{}{"debug:"}, v...)...)
|
||||
}
|
||||
}
|
||||
|
||||
func Debugf(fmt string, v ...interface{}) {
|
||||
if debug {
|
||||
log.Printf("debug: "+fmt, v...)
|
||||
}
|
||||
}
|
||||
76
pkg/otp/otp.go
Normal file
76
pkg/otp/otp.go
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package otp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"github.com/pquerna/otp"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"html/template"
|
||||
"image/png"
|
||||
)
|
||||
|
||||
const size = 200
|
||||
|
||||
func base64Image(key *otp.Key) (data template.URL, err error) {
|
||||
var buf bytes.Buffer
|
||||
img, err := key.Image(size, size)
|
||||
if err != nil {
|
||||
err = errors.New("otp: " + err.Error())
|
||||
return
|
||||
}
|
||||
err = png.Encode(&buf, img)
|
||||
if err != nil {
|
||||
err = errors.New("otp: " + err.Error())
|
||||
return
|
||||
}
|
||||
data = template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(buf.Bytes()))
|
||||
return
|
||||
}
|
||||
|
||||
type Request struct {
|
||||
Username string
|
||||
Image template.URL
|
||||
Secret string
|
||||
URL string
|
||||
}
|
||||
|
||||
func New(username string) (req Request, err error) {
|
||||
key, err := totp.Generate(totp.GenerateOpts{
|
||||
Issuer: "gowiki",
|
||||
AccountName: username,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
data, err := base64Image(key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return Request{
|
||||
Username: username,
|
||||
Image: data,
|
||||
Secret: key.Secret(),
|
||||
URL: key.URL(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func Validate(pin, secret string) bool {
|
||||
if secret == "" {
|
||||
return true
|
||||
}
|
||||
return totp.Validate(pin, secret)
|
||||
}
|
||||
|
||||
func ImageSecretFromURL(url string) (data template.URL, secret string, err error) {
|
||||
key, err := otp.NewKeyFromURL(url)
|
||||
if err != nil {
|
||||
err = errors.New("otp: " + err.Error())
|
||||
return
|
||||
}
|
||||
secret = key.Secret()
|
||||
data, err = base64Image(key)
|
||||
return
|
||||
}
|
||||
25
pkg/otp/otp_test.go
Normal file
25
pkg/otp/otp_test.go
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package otp
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestNew(t *testing.T) {
|
||||
_, err := New("admin")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestImageSecretFromURL(t *testing.T) {
|
||||
r, _ := New("admin")
|
||||
_, secret, err := ImageSecretFromURL(r.URL)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
if secret != r.Secret {
|
||||
t.Error("secret is not equal")
|
||||
}
|
||||
}
|
||||
87
pkg/render/render.go
Normal file
87
pkg/render/render.go
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package render
|
||||
|
||||
import (
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
"github.com/blevesearch/bleve/analysis"
|
||||
htmlchar "github.com/blevesearch/bleve/analysis/char/html"
|
||||
"github.com/russross/blackfriday"
|
||||
"html/template"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const delim = "+"
|
||||
|
||||
var filter = makeFilter()
|
||||
|
||||
func makeFilter() analysis.CharFilter {
|
||||
f, _ := htmlchar.CharFilterConstructor(nil, nil)
|
||||
return f
|
||||
}
|
||||
|
||||
var (
|
||||
titleRe = regexp.MustCompile("[^a-zA-Z0-9 -]+")
|
||||
whiteSpace = regexp.MustCompile(`\s+`)
|
||||
)
|
||||
|
||||
const (
|
||||
commonHtmlFlags = 0 |
|
||||
blackfriday.HTML_USE_XHTML |
|
||||
blackfriday.HTML_USE_SMARTYPANTS |
|
||||
blackfriday.HTML_SMARTYPANTS_FRACTIONS |
|
||||
blackfriday.HTML_SMARTYPANTS_DASHES |
|
||||
blackfriday.HTML_SMARTYPANTS_LATEX_DASHES |
|
||||
blackfriday.HTML_TOC |
|
||||
blackfriday.HTML_HREF_TARGET_BLANK |
|
||||
blackfriday.HTML_NOREFERRER_LINKS |
|
||||
blackfriday.HTML_NOFOLLOW_LINKS |
|
||||
blackfriday.HTML_NOOPENER_LINKS
|
||||
|
||||
commonExtensions = 0 |
|
||||
blackfriday.EXTENSION_NO_INTRA_EMPHASIS |
|
||||
blackfriday.EXTENSION_TABLES |
|
||||
blackfriday.EXTENSION_FENCED_CODE |
|
||||
blackfriday.EXTENSION_AUTOLINK |
|
||||
blackfriday.EXTENSION_STRIKETHROUGH |
|
||||
blackfriday.EXTENSION_SPACE_HEADERS |
|
||||
blackfriday.EXTENSION_HEADER_IDS |
|
||||
blackfriday.EXTENSION_BACKSLASH_LINE_BREAK |
|
||||
blackfriday.EXTENSION_DEFINITION_LISTS |
|
||||
blackfriday.EXTENSION_AUTO_HEADER_IDS
|
||||
)
|
||||
|
||||
func Render(p *core.Page) string {
|
||||
renderer := blackfriday.HtmlRenderer(commonHtmlFlags, "", "")
|
||||
rend := blackfriday.MarkdownOptions([]byte(p.Markdown), renderer,
|
||||
blackfriday.Options{Extensions: commonExtensions},
|
||||
)
|
||||
html := string(rend)
|
||||
html = strings.ReplaceAll(html, "<table>", `<table class="table">`)
|
||||
p.HTML = template.HTML(html)
|
||||
return whiteSpace.ReplaceAllString(string(filter.Filter(rend)), " ")
|
||||
}
|
||||
|
||||
func Title(title string) string {
|
||||
return whiteSpace.ReplaceAllString(titleRe.ReplaceAllString(title, ""), delim)
|
||||
}
|
||||
|
||||
func Untitle(title string) string {
|
||||
return strings.ReplaceAll(title, delim, " ")
|
||||
}
|
||||
|
||||
func StoreTitle(section, title string) string {
|
||||
return section + "/" + title
|
||||
}
|
||||
|
||||
func SplitStoreTitle(st string) (section, title string) {
|
||||
a := strings.Split(st, "/")
|
||||
switch len(a) {
|
||||
case 1:
|
||||
return a[0], ""
|
||||
case 2:
|
||||
return a[0], a[1]
|
||||
}
|
||||
return
|
||||
}
|
||||
35
pkg/render/render_test.go
Normal file
35
pkg/render/render_test.go
Normal file
|
|
@ -0,0 +1,35 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package render
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTitleFunctions(t *testing.T) {
|
||||
section := "section"
|
||||
title := "title"
|
||||
combined := "section/title"
|
||||
t.Run("StoreTitle", func(t *testing.T) {
|
||||
if StoreTitle(section, title) != combined {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
t.Run("SplitStoreTitle", func(t *testing.T) {
|
||||
se, ti := SplitStoreTitle(combined)
|
||||
if se != section || ti != title {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
t.Run("SplitStoreTitleSingle", func(t *testing.T) {
|
||||
se, ti := SplitStoreTitle(section)
|
||||
if se != section || ti != "" {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
t.Run("Title", func(t *testing.T) {
|
||||
if Title("9ßw$%4h4t-8v74 287(G /SV&3[45v}=94#+?)") != "9w4h4t-8v74+287G+SV345v94" {
|
||||
t.Fail()
|
||||
}
|
||||
})
|
||||
}
|
||||
305
pkg/server/context.go
Normal file
305
pkg/server/context.go
Normal file
|
|
@ -0,0 +1,305 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/log"
|
||||
"git.giftfish.de/ston1th/jwt/v3"
|
||||
"github.com/gorilla/mux"
|
||||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
userClaim = "user"
|
||||
adminClaim = "admin"
|
||||
userCreatedClaim = "user_created"
|
||||
pageCreatedClaim = "page_created"
|
||||
sharedClaim = "shared"
|
||||
sectionClaim = "section"
|
||||
titleClaim = "title"
|
||||
totpClaim = "totp"
|
||||
rememberClaim = "remember"
|
||||
refererClaim = "referer"
|
||||
|
||||
empty = "(empty)"
|
||||
)
|
||||
|
||||
func newContext(w http.ResponseWriter, r *http.Request, s *HTTPServer) *Context {
|
||||
h := w.Header()
|
||||
h.Set("X-Frame-Options", "DENY")
|
||||
h.Set("X-Content-Type-Options", "nosniff")
|
||||
h.Set("X-XSS-Protection", "1; mode=block")
|
||||
h.Set("Content-Security-Policy", "default-src 'none';style-src 'self';img-src 'self' https: data:;connect-src 'self';frame-ancestors 'none'")
|
||||
h.Set("Referrer-Policy", "same-origin")
|
||||
return &Context{
|
||||
Request: r,
|
||||
Response: w,
|
||||
Srv: s,
|
||||
Time: time.Now(),
|
||||
}
|
||||
}
|
||||
|
||||
type Context struct {
|
||||
Request *http.Request
|
||||
Response http.ResponseWriter
|
||||
Srv *HTTPServer
|
||||
T *template.Template
|
||||
Status int
|
||||
Err error
|
||||
Time time.Time
|
||||
|
||||
Token jwt.Token
|
||||
|
||||
Data webData
|
||||
}
|
||||
|
||||
type webData struct {
|
||||
Version string
|
||||
Time string
|
||||
|
||||
Title string
|
||||
BodyTitle string
|
||||
|
||||
Admin bool
|
||||
Login bool
|
||||
|
||||
Key string
|
||||
|
||||
User string
|
||||
Token string
|
||||
Msg string
|
||||
Search string
|
||||
|
||||
Data interface{}
|
||||
}
|
||||
|
||||
type loginData struct {
|
||||
User string
|
||||
Referer string
|
||||
}
|
||||
|
||||
type sectionData struct {
|
||||
Name string
|
||||
Members map[string]bool
|
||||
}
|
||||
|
||||
type pageData struct {
|
||||
Page core.Page
|
||||
Sections map[string]bool
|
||||
}
|
||||
|
||||
func (c *Context) Exec() {
|
||||
defer c.log()
|
||||
if c.T == nil {
|
||||
c.Status = http.StatusInternalServerError
|
||||
c.Err = errors.New("template is nil")
|
||||
return
|
||||
}
|
||||
c.Data.Token = newXsrf(c.Token.RawSig())
|
||||
|
||||
c.Data.Version = c.Srv.Config.Version
|
||||
if c.Data.BodyTitle == "" {
|
||||
c.Data.BodyTitle = c.Data.Title
|
||||
}
|
||||
c.Data.Admin = c.Admin()
|
||||
c.Data.User = c.User()
|
||||
c.Data.Login = c.LoggedOn()
|
||||
|
||||
t := strconv.Itoa(int(time.Since(c.Time).Nanoseconds()/1e6)) + "ms"
|
||||
if t == "0ms" {
|
||||
t = strconv.Itoa(int(time.Since(c.Time).Nanoseconds()/1e5)) + "µs"
|
||||
}
|
||||
c.Data.Time = t
|
||||
|
||||
if c.Data.Msg != "" {
|
||||
c.Status = http.StatusBadRequest
|
||||
}
|
||||
|
||||
c.Err = c.T.Execute(c.Response, c.Data)
|
||||
if c.Err != nil {
|
||||
c.Status = http.StatusInternalServerError
|
||||
}
|
||||
if c.Status == 0 {
|
||||
c.Status = http.StatusOK
|
||||
}
|
||||
}
|
||||
|
||||
func (c *Context) log() {
|
||||
if c.Err != nil {
|
||||
log.Printf("%s %s %s %d error: %s\n", c.Request.RemoteAddr, c.Request.Method, c.Request.URL, c.Status, c.Err)
|
||||
return
|
||||
}
|
||||
log.Printf("%s %s %s %d\n", c.Request.RemoteAddr, c.Request.Method, c.Request.URL, c.Status)
|
||||
}
|
||||
|
||||
func (c *Context) Error(i interface{}) {
|
||||
switch v := i.(type) {
|
||||
case string:
|
||||
c.Data.Msg = v
|
||||
case error:
|
||||
c.Data.Msg = v.Error()
|
||||
}
|
||||
c.Exec()
|
||||
}
|
||||
|
||||
func (c *Context) NotFound() {
|
||||
c.Template("notFoundHandler")
|
||||
c.Data = webData{Title: "404"}
|
||||
c.Status = http.StatusNotFound
|
||||
c.Response.WriteHeader(http.StatusNotFound)
|
||||
c.Exec()
|
||||
}
|
||||
|
||||
func (c *Context) Forbidden() {
|
||||
c.Template("forbiddenHandler")
|
||||
c.Data = webData{Title: "403"}
|
||||
c.Status = http.StatusForbidden
|
||||
c.Response.WriteHeader(http.StatusForbidden)
|
||||
c.Exec()
|
||||
}
|
||||
|
||||
func (c *Context) Template(name string) {
|
||||
c.T = c.Srv.templ[name]
|
||||
}
|
||||
|
||||
func (c *Context) Write(buf []byte) (err error) {
|
||||
_, err = c.Response.Write(buf)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Context) SetHeader(name, value string) {
|
||||
c.Response.Header().Set(name, value)
|
||||
}
|
||||
|
||||
func (c *Context) Redirect(uri string, code int) {
|
||||
if len(uri) > 0 && uri[0] != '/' {
|
||||
uri = "/" + uri
|
||||
}
|
||||
log.Printf("%s %s %s %d %s\n", c.Request.RemoteAddr, c.Request.Method, c.Request.URL, code, uri)
|
||||
http.Redirect(c.Response, c.Request, uri, code)
|
||||
}
|
||||
|
||||
func (c *Context) Method() string {
|
||||
return c.Request.Method
|
||||
}
|
||||
|
||||
func (c *Context) Path() string {
|
||||
return c.Request.URL.Path
|
||||
}
|
||||
|
||||
func (c *Context) RefererURI() string {
|
||||
u, err := url.Parse(c.Request.Header.Get("Referer"))
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
return u.RequestURI()
|
||||
}
|
||||
|
||||
func (c *Context) FormSlice(name string) []string {
|
||||
// does nothing if called twice
|
||||
c.Request.ParseForm()
|
||||
return c.Request.PostForm[name]
|
||||
}
|
||||
|
||||
func (c *Context) Form(name string) string {
|
||||
return c.Request.PostFormValue(name)
|
||||
}
|
||||
|
||||
func (c *Context) Var(name string) (ret string) {
|
||||
ret = mux.Vars(c.Request)[name]
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Context) Forwarded() (ret string) {
|
||||
ret = c.Request.Header.Get("X-Forwarded-For")
|
||||
if ret == "" {
|
||||
ret = empty
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// CheckXsrf validates the xsrf token
|
||||
func (c *Context) CheckXsrf() (ok bool) {
|
||||
ok = checkXsrf(c.Form("token"), c.Token.RawSig())
|
||||
if !ok {
|
||||
c.Error("wrong csrf token")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Context) LoggedOn() (ok bool) {
|
||||
_, ok = c.Token.Claims.Get(userClaim)
|
||||
return
|
||||
}
|
||||
|
||||
func (c *Context) LogSetCookie(msg string, err error) {
|
||||
log.Printf("ctx: %s %s\n", msg, err)
|
||||
c.SetCookie(nil, 0)
|
||||
}
|
||||
|
||||
func (c *Context) Login(u *core.User, remember string) {
|
||||
var d time.Duration
|
||||
if remember != "" {
|
||||
d = day * 7
|
||||
}
|
||||
c.SetCookie(jwt.Claims{userClaim: u.Username, userCreatedClaim: u.Created, adminClaim: u.Admin}, d)
|
||||
}
|
||||
|
||||
func (c *Context) SetCookie(claims jwt.Claims, d time.Duration) {
|
||||
c.setCookie(jwt.NewToken(claims, nil), d)
|
||||
}
|
||||
|
||||
func (c *Context) setCookie(t *jwt.Token, d time.Duration) {
|
||||
if d == 0 {
|
||||
d = jwt.DefaultExpiry
|
||||
}
|
||||
t.Claims[jwt.ExpClaim] = jwt.NewExp(d)
|
||||
if err := c.Srv.JWT.Sign(t); err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
c.Token = *t
|
||||
cookie := &http.Cookie{
|
||||
Name: cookieName,
|
||||
Value: c.Token.String(),
|
||||
Path: "/",
|
||||
MaxAge: int(d.Seconds()),
|
||||
Secure: c.Srv.Config.SecureCookie,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteStrictMode,
|
||||
}
|
||||
if cookie.MaxAge > 0 {
|
||||
cookie.Expires = time.Now().Add(d)
|
||||
} else if cookie.MaxAge < 0 {
|
||||
cookie.Expires = time.Unix(1, 0)
|
||||
}
|
||||
http.SetCookie(c.Response, cookie)
|
||||
}
|
||||
|
||||
func (c *Context) User() string {
|
||||
return c.Token.Claims.GetString(userClaim)
|
||||
}
|
||||
|
||||
func (c *Context) Totp() string {
|
||||
return c.Token.Claims.GetString(totpClaim)
|
||||
}
|
||||
|
||||
func (c *Context) Remember() string {
|
||||
return c.Token.Claims.GetString(rememberClaim)
|
||||
}
|
||||
|
||||
func (c *Context) Referer() string {
|
||||
return c.Token.Claims.GetString(refererClaim)
|
||||
}
|
||||
|
||||
func (c *Context) Admin() bool {
|
||||
return c.Token.Claims.GetBool(adminClaim)
|
||||
}
|
||||
|
||||
type ctxHandler func(*Context)
|
||||
890
pkg/server/handler.go
Normal file
890
pkg/server/handler.go
Normal file
|
|
@ -0,0 +1,890 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/log"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/otp"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/render"
|
||||
"git.giftfish.de/ston1th/jwt/v3"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type notFoundHandler struct {
|
||||
s *HTTPServer
|
||||
}
|
||||
|
||||
func (nf *notFoundHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
newContext(w, r, nf.s).NotFound()
|
||||
}
|
||||
|
||||
func jwtHandler(h ctxHandler) ctxHandler {
|
||||
return func(ctx *Context) {
|
||||
if c, err := ctx.Request.Cookie(cookieName); err == nil {
|
||||
t, err := jwt.DecodeToken(c.Value)
|
||||
if err != nil {
|
||||
ctx.LogSetCookie("DecodeToken:", err)
|
||||
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||
return
|
||||
}
|
||||
if err = ctx.Srv.JWT.Verify(t); err != nil {
|
||||
ctx.LogSetCookie("VerifyToken:", err)
|
||||
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||
return
|
||||
}
|
||||
if t.Claims.GetString(totpClaim) != "" {
|
||||
path := ctx.Path()
|
||||
if path == core.TotpURI || path == core.LoginURI || path == core.LogoutURI {
|
||||
ctx.Token = *t
|
||||
h(ctx)
|
||||
return
|
||||
}
|
||||
ctx.Redirect(core.TotpURI, http.StatusFound)
|
||||
return
|
||||
|
||||
}
|
||||
if !ctx.Srv.DB.LockedOut(t.Claims.GetString(userClaim), t.Claims.GetString(userCreatedClaim)) {
|
||||
ctx.Token = *t
|
||||
h(ctx)
|
||||
return
|
||||
}
|
||||
if err = ctx.Srv.JWT.Invalidate(t); err != nil {
|
||||
log.Println("Invalidate:", err)
|
||||
}
|
||||
}
|
||||
ctx.SetCookie(nil, 0)
|
||||
h(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func totpAuthHandler(h ctxHandler) ctxHandler {
|
||||
return func(ctx *Context) {
|
||||
if ctx.Totp() != "" {
|
||||
h(ctx)
|
||||
return
|
||||
}
|
||||
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func adminAuthHandler(h ctxHandler) ctxHandler {
|
||||
return func(ctx *Context) {
|
||||
if ctx.Admin() {
|
||||
h(ctx)
|
||||
return
|
||||
}
|
||||
ctx.Forbidden()
|
||||
}
|
||||
}
|
||||
|
||||
func userAuthHandler(h ctxHandler) ctxHandler {
|
||||
return func(ctx *Context) {
|
||||
if ctx.Admin() {
|
||||
h(ctx)
|
||||
return
|
||||
}
|
||||
if ctx.User() == ctx.Var("user") {
|
||||
h(ctx)
|
||||
return
|
||||
}
|
||||
ctx.Forbidden()
|
||||
}
|
||||
}
|
||||
|
||||
func authHandler(h ctxHandler) ctxHandler {
|
||||
return func(ctx *Context) {
|
||||
if ctx.LoggedOn() {
|
||||
h(ctx)
|
||||
return
|
||||
}
|
||||
ctx.Forbidden()
|
||||
}
|
||||
}
|
||||
|
||||
func indexHandler(ctx *Context) {
|
||||
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||
}
|
||||
|
||||
func loginHandler(ctx *Context) {
|
||||
if ctx.LoggedOn() {
|
||||
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||
return
|
||||
}
|
||||
ctx.Template("loginHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Login",
|
||||
BodyTitle: "Login",
|
||||
}
|
||||
var data loginData
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
data.Referer = ctx.RefererURI()
|
||||
ctx.Data.Data = data
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
data.User = ctx.Form("user")
|
||||
data.Referer = ctx.Form("referer")
|
||||
ctx.Data.Data = data
|
||||
if data.Referer == "" {
|
||||
data.Referer = core.IndexURI
|
||||
}
|
||||
password := ctx.Form("password")
|
||||
if data.User == "" || password == "" {
|
||||
ctx.Error("empty username or password")
|
||||
return
|
||||
}
|
||||
u, err := ctx.Srv.DB.Login(data.User, password)
|
||||
if err != nil {
|
||||
log.Printf("login: %s from: %s %s", data.User, ctx.Forwarded(), err)
|
||||
ctx.Error("bad username or password")
|
||||
return
|
||||
}
|
||||
if u.Secret == "" {
|
||||
log.Println("login:", u.Username)
|
||||
ctx.Login(u, ctx.Form("remember"))
|
||||
ctx.Redirect(data.Referer, http.StatusFound)
|
||||
return
|
||||
}
|
||||
ctx.SetCookie(jwt.Claims{
|
||||
totpClaim: u.Username,
|
||||
rememberClaim: ctx.Form("remember"),
|
||||
refererClaim: data.Referer,
|
||||
}, time.Minute)
|
||||
ctx.Redirect(core.TotpURI, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func loginTotpHandler(ctx *Context) {
|
||||
if ctx.LoggedOn() {
|
||||
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||
return
|
||||
}
|
||||
ctx.Template("loginTotpHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "TOTP Verification",
|
||||
BodyTitle: "TOTP Veriftcation",
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
pin := ctx.Form("pin")
|
||||
if pin == "" {
|
||||
ctx.Error("empty pin")
|
||||
return
|
||||
}
|
||||
user := ctx.Totp()
|
||||
u, err := ctx.Srv.DB.Totp(user, pin)
|
||||
if err != nil {
|
||||
log.Printf("totp: %s from: %s %s", user, ctx.Forwarded(), err)
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
ref := ctx.Referer()
|
||||
log.Println("login:", u.Username)
|
||||
ctx.Login(u, ctx.Remember())
|
||||
ctx.Redirect(ref, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func searchHandler(ctx *Context) {
|
||||
ctx.Template("searchHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Search",
|
||||
BodyTitle: "Search",
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
search := ctx.Form("search")
|
||||
|
||||
res, err := ctx.Srv.DB.Index.Search(search)
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
user := ctx.User()
|
||||
r := make([]core.Result, len(res))
|
||||
i := 0
|
||||
for _, v := range res {
|
||||
if ctx.Srv.DB.ValidatePage(v.StoreTitle, user) {
|
||||
r[i] = v
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Data.Data = r[:i]
|
||||
ctx.Data.Search = search
|
||||
ctx.Exec()
|
||||
}
|
||||
}
|
||||
|
||||
func sectionsHandler(ctx *Context) {
|
||||
ctx.Template("sectionsHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "All Sections",
|
||||
BodyTitle: "All Sections",
|
||||
Admin: ctx.Admin(),
|
||||
}
|
||||
ctx.Data.Data = ctx.Srv.DB.GetAllSections(ctx.User(), ctx.Admin())
|
||||
ctx.Exec()
|
||||
}
|
||||
|
||||
func sectionHandler(ctx *Context) {
|
||||
ctx.Template("sectionHandler")
|
||||
section := ctx.Var("section")
|
||||
ctx.Data = webData{
|
||||
Title: section,
|
||||
BodyTitle: section,
|
||||
Admin: ctx.Admin(),
|
||||
}
|
||||
var err error
|
||||
ctx.Data.Data, err = ctx.Srv.DB.GetAllSectionPages(section, ctx.User(), ctx.Admin())
|
||||
if err != nil {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
ctx.Exec()
|
||||
}
|
||||
|
||||
func sectionNewHandler(ctx *Context) {
|
||||
ctx.Template("sectionNewHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "New Section",
|
||||
BodyTitle: "New Section",
|
||||
}
|
||||
users, err := ctx.Srv.DB.GetUsers()
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
sd := sectionData{Members: make(map[string]bool)}
|
||||
for _, u := range users {
|
||||
sd.Members[u.Username] = false
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.Data.Data = sd
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
section := ctx.Form("section")
|
||||
members := ctx.FormSlice("members")
|
||||
for _, m := range members {
|
||||
sd.Members[m] = true
|
||||
}
|
||||
ctx.Data.Data = sd
|
||||
if section == "" {
|
||||
ctx.Error("empty section")
|
||||
return
|
||||
}
|
||||
err = ctx.Srv.DB.CreateSection(section, members, false)
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
log.Printf("create: section %s created by %s\n", section, ctx.User())
|
||||
ctx.Redirect(core.SectionsURI, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func sectionEditHandler(ctx *Context) {
|
||||
ctx.Template("sectionEditHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Edit Section",
|
||||
BodyTitle: "Edit Section",
|
||||
}
|
||||
users, err := ctx.Srv.DB.GetUsers()
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
sd := sectionData{Members: make(map[string]bool)}
|
||||
for _, u := range users {
|
||||
sd.Members[u.Username] = false
|
||||
}
|
||||
s, err := ctx.Srv.DB.GetSection(ctx.Var("section"))
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
sd.Name = s.Name
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
for _, m := range s.Members {
|
||||
sd.Members[m] = true
|
||||
}
|
||||
ctx.Data.Data = sd
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
members := ctx.FormSlice("members")
|
||||
for _, m := range members {
|
||||
sd.Members[m] = true
|
||||
}
|
||||
ctx.Data.Data = sd
|
||||
err = ctx.Srv.DB.UpdateSection(s.Name, members, s.User)
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
log.Printf("update: section %s updated by %s\n", s.Name, ctx.User())
|
||||
ctx.Redirect(core.SectionsURI, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func sectionDelHandler(ctx *Context) {
|
||||
ctx.Template("sectionDelHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Delete Section",
|
||||
BodyTitle: "Delete Section",
|
||||
}
|
||||
s, err := ctx.Srv.DB.GetSection(ctx.Var("section"))
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
if s.User {
|
||||
ctx.Error("user section can not be deleted")
|
||||
return
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.Data.Data = s.Name
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
err := ctx.Srv.DB.DeleteSection(s.Name)
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
log.Printf("delete: section %s deleted by %s\n", s.Name, ctx.User())
|
||||
ctx.Redirect(core.SectionsURI, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func pageNewHandler(ctx *Context) {
|
||||
ctx.Template("pageNewHandler")
|
||||
ctx.Data = webData{Title: "New Page"}
|
||||
user := ctx.User()
|
||||
secs, err := ctx.Srv.DB.GetUserSections(user)
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
secs[user] = true
|
||||
ctx.Data.Data = pageData{core.Page{Perm: core.Public}, secs}
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
section := ctx.Form("section")
|
||||
title := ctx.Form("title")
|
||||
markdown := ctx.Form("markdown")
|
||||
perm := core.ParsePermString(ctx.Form("perm"))
|
||||
p := core.Page{
|
||||
Title: title,
|
||||
Markdown: markdown,
|
||||
Perm: perm,
|
||||
}
|
||||
secs[section] = true
|
||||
ctx.Data.Data = pageData{p, secs}
|
||||
if title == "" {
|
||||
ctx.Error("empty title")
|
||||
return
|
||||
}
|
||||
if perm == core.Invalid {
|
||||
ctx.Error("invalid permission")
|
||||
return
|
||||
}
|
||||
|
||||
page, err := ctx.Srv.DB.CreatePage(title, section, markdown, user, perm)
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
ctx.Redirect(page.StoreTitle, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func pageHandler(ctx *Context) {
|
||||
var pageCreated string
|
||||
ctx.Data = webData{
|
||||
Admin: ctx.Admin(),
|
||||
}
|
||||
section := ctx.Var("section")
|
||||
title := ctx.Var("title")
|
||||
user := ctx.User()
|
||||
key := ctx.Var("key")
|
||||
if key != "" {
|
||||
t, err := jwt.DecodeToken(key)
|
||||
if err != nil {
|
||||
log.Println("share: DecodeToken:", err)
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
if err = ctx.Srv.JWT.Verify(t); err != nil {
|
||||
log.Println("share: VerifyToken:", err)
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
user = t.Claims.GetString(sharedClaim)
|
||||
if ctx.Srv.DB.LockedOut(user, t.Claims.GetString(userCreatedClaim)) {
|
||||
if err = ctx.Srv.JWT.Invalidate(t); err != nil {
|
||||
log.Println("share: Invalidate:", err)
|
||||
}
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
section = t.Claims.GetString(sectionClaim)
|
||||
title = t.Claims.GetString(titleClaim)
|
||||
pageCreated = t.Claims.GetString(pageCreatedClaim)
|
||||
ctx.Data.Key = key
|
||||
if ctx.T == nil {
|
||||
ctx.Template("pageViewHandler")
|
||||
}
|
||||
} else if ctx.T == nil {
|
||||
ctx.Template("pageHandler")
|
||||
}
|
||||
page, err := ctx.Srv.DB.GetPage(section, title, user)
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
if key != "" && page.Created != pageCreated {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
ctx.Data.Data = page
|
||||
ctx.Data.Title = section + " | " + page.Title
|
||||
ctx.Data.BodyTitle = section
|
||||
ctx.Exec()
|
||||
}
|
||||
|
||||
func pageMDHandler(f ctxHandler) ctxHandler {
|
||||
return func(ctx *Context) {
|
||||
ctx.Template("pageMDHandler")
|
||||
f(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
func pageShareHandler(ctx *Context) {
|
||||
ctx.Template("pageShareHandler")
|
||||
ctx.Data = webData{}
|
||||
section := ctx.Var("section")
|
||||
title := ctx.Var("title")
|
||||
user := ctx.User()
|
||||
page, err := ctx.Srv.DB.GetPage(section, title, user)
|
||||
if err != nil {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
if page.Perm != core.Private {
|
||||
ctx.Error("only private pages can be shared")
|
||||
return
|
||||
}
|
||||
ctx.Data.Data = page.StoreTitle
|
||||
ctx.Data.Title = page.Title
|
||||
ctx.Data.BodyTitle = page.Title
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
|
||||
d, err := duration(ctx.Form("duration"), ctx.Form("mode"))
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
t := jwt.NewToken(map[string]interface{}{
|
||||
sharedClaim: user,
|
||||
userCreatedClaim: ctx.Token.Claims.GetString(userCreatedClaim),
|
||||
pageCreatedClaim: page.Created,
|
||||
sectionClaim: section,
|
||||
titleClaim: title,
|
||||
jwt.ExpClaim: jwt.NewExp(d),
|
||||
}, nil)
|
||||
|
||||
if err = ctx.Srv.JWT.Sign(t); err != nil {
|
||||
log.Println("share: SignToken:", err)
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
ctx.Data.Key = t.String()
|
||||
ctx.Exec()
|
||||
}
|
||||
}
|
||||
|
||||
func pageBlacklistHandler(ctx *Context) {
|
||||
ctx.Template("pageBlacklistHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Blacklist",
|
||||
BodyTitle: "Blacklist",
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
|
||||
t, err := jwt.DecodeToken(ctx.Form("share"))
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
err = ctx.Srv.JWT.Verify(t)
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
user := t.Claims.GetString(sharedClaim)
|
||||
if user == "" {
|
||||
ctx.Error("blacklisting denied: no share token")
|
||||
return
|
||||
}
|
||||
if ctx.User() != user {
|
||||
ctx.Error("blacklisting denied: token was issued by " + user)
|
||||
return
|
||||
}
|
||||
err = ctx.Srv.JWT.Invalidate(t)
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
ctx.Exec()
|
||||
}
|
||||
}
|
||||
|
||||
func pageEditHandler(ctx *Context) {
|
||||
ctx.Template("pageEditHandler")
|
||||
ctx.Data = webData{}
|
||||
section := ctx.Var("section")
|
||||
title := ctx.Var("title")
|
||||
page, err := ctx.Srv.DB.GetPage(section, title, ctx.User())
|
||||
if err != nil {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
ctx.Data.Title = page.Title
|
||||
ctx.Data.BodyTitle = page.Title
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.Data.Data = page
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
page.Markdown = ctx.Form("markdown")
|
||||
page.Perm = core.ParsePermString(ctx.Form("perm"))
|
||||
ctx.Data.Data = page
|
||||
|
||||
err = ctx.Srv.DB.UpdatePage(title, section, page.Markdown, ctx.User(), page.Perm)
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
ctx.Redirect(page.StoreTitle, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func pageDelHandler(ctx *Context) {
|
||||
section := ctx.Var("section")
|
||||
title := ctx.Var("title")
|
||||
ctx.Template("pageDelHandler")
|
||||
ctx.Data.Data = render.StoreTitle(section, title)
|
||||
ctx.Data.Title = title
|
||||
ctx.Data.BodyTitle = title
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
if err := ctx.Srv.DB.DeletePage(title, section, ctx.User()); err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func usersHandler(ctx *Context) {
|
||||
if !ctx.LoggedOn() {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
ctx.Template("usersHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Users",
|
||||
BodyTitle: "Users",
|
||||
User: ctx.User(),
|
||||
}
|
||||
us, err := ctx.Srv.DB.GetUsers()
|
||||
if err != nil {
|
||||
ctx.Data.Msg = err.Error()
|
||||
}
|
||||
ctx.Data.Data = us
|
||||
ctx.Exec()
|
||||
}
|
||||
|
||||
func userNewHandler(ctx *Context) {
|
||||
ctx.Template("userNewHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "New User",
|
||||
BodyTitle: "New User",
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
user := ctx.Form("user")
|
||||
password := ctx.Form("password")
|
||||
repeat := ctx.Form("repeat")
|
||||
admin := ctx.Form("admin")
|
||||
adm := false
|
||||
if admin == "0" {
|
||||
adm = true
|
||||
}
|
||||
|
||||
if user == "" || password == "" {
|
||||
ctx.Error("empty user or password")
|
||||
return
|
||||
}
|
||||
if password != repeat {
|
||||
ctx.Error("passwords do not match")
|
||||
return
|
||||
}
|
||||
if err := ctx.Srv.DB.CreateUser(user, password, adm); err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
log.Printf("create: user %s created by %s\n", user, ctx.User())
|
||||
ctx.Redirect(core.UsersURI, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func userEditHandler(ctx *Context) {
|
||||
ctx.Template("userEditHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Edit User",
|
||||
BodyTitle: "Edit User",
|
||||
}
|
||||
u, err := ctx.Srv.DB.GetUserWithoutPassword(ctx.Var("user"))
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
ctx.Data.Data = u
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
user := ctx.Var("user")
|
||||
password := ctx.Form("password")
|
||||
repeat := ctx.Form("repeat")
|
||||
admin := ctx.Form("admin")
|
||||
adm := false
|
||||
if admin == "0" {
|
||||
adm = true
|
||||
}
|
||||
|
||||
if password != repeat {
|
||||
ctx.Error("passwords do not match")
|
||||
return
|
||||
}
|
||||
if ctx.Admin() {
|
||||
if err := ctx.Srv.DB.AdminUpdateUser(user, password, adm); err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
log.Printf("update: user %s updated by %s\n", user, ctx.User())
|
||||
ctx.Redirect(core.UsersURI, http.StatusFound)
|
||||
return
|
||||
}
|
||||
if err := ctx.Srv.DB.UpdateUserPassword(user, password); err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
log.Printf("update: %s updated by %s\n", user, ctx.User())
|
||||
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func userTotpHandler(ctx *Context) {
|
||||
ctx.Template("userTotpHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "TOTP",
|
||||
BodyTitle: "TOTP",
|
||||
}
|
||||
req := otp.Request{Username: ctx.Var("user")}
|
||||
u, err := ctx.Srv.DB.GetUser(req.Username)
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
if u.Secret == "" {
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
req, err = otp.New(req.Username)
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
case "POST":
|
||||
req.URL = ctx.Form("url")
|
||||
req.Image, req.Secret, err = otp.ImageSecretFromURL(req.URL)
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.Data.Data = req
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
pin := ctx.Form("pin")
|
||||
if pin == "" {
|
||||
ctx.Error("empty pin")
|
||||
return
|
||||
}
|
||||
ctx.Data.Data = req
|
||||
secret := req.Secret
|
||||
if u.Secret != "" {
|
||||
secret = u.Secret
|
||||
}
|
||||
valid := otp.Validate(pin, secret)
|
||||
user := ctx.User()
|
||||
if user != req.Username && ctx.Admin() {
|
||||
valid = true
|
||||
}
|
||||
|
||||
if !valid {
|
||||
ctx.Error("validation failed")
|
||||
return
|
||||
}
|
||||
if err := ctx.Srv.DB.UpdateUserSecret(req.Username, req.Secret); err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
if req.Secret == "" {
|
||||
log.Printf("totp: disabled by %s for %s\n", user, req.Username)
|
||||
} else {
|
||||
log.Printf("totp: enabled by %s for %s\n", user, req.Username)
|
||||
}
|
||||
ctx.Redirect(core.UserURI+"/edit/"+req.Username, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func userUnlockHandler(ctx *Context) {
|
||||
user := ctx.Var("user")
|
||||
if err := ctx.Srv.DB.UnlockUser(user); err != nil {
|
||||
log.Println("unlock:", err)
|
||||
}
|
||||
log.Printf("unlock: %s unlocked by %s\n", user, ctx.User())
|
||||
ctx.Redirect(core.UsersURI, http.StatusFound)
|
||||
}
|
||||
|
||||
func userDelHandler(ctx *Context) {
|
||||
ctx.Template("userDelHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Delete User",
|
||||
BodyTitle: "Delete User",
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
user, err := ctx.Srv.DB.GetUserWithoutPassword(ctx.Var("user"))
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
ctx.Data.Data = user
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
self := ctx.User()
|
||||
user := ctx.Var("user")
|
||||
if err := ctx.Srv.DB.DeleteUser(user); err != nil {
|
||||
log.Println("delete:", err)
|
||||
}
|
||||
log.Printf("delete: user %s deleted by %s\n", user, self)
|
||||
if user == self {
|
||||
ctx.Redirect(core.LogoutURI, http.StatusFound)
|
||||
return
|
||||
}
|
||||
ctx.Redirect(core.UsersURI, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func statsHandler(ctx *Context) {
|
||||
ctx.Template("statsHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Stats",
|
||||
BodyTitle: "Stats",
|
||||
}
|
||||
if ctx.Method() == "GET" {
|
||||
ctx.Data.Data = getStats(ctx.Srv.DB.Index)
|
||||
ctx.Exec()
|
||||
}
|
||||
}
|
||||
|
||||
func logoutHandler(ctx *Context) {
|
||||
var totp string
|
||||
user := ctx.User()
|
||||
if user == "" {
|
||||
user = ctx.Totp()
|
||||
if user != "" {
|
||||
totp = " (totp)"
|
||||
}
|
||||
}
|
||||
if user != "" {
|
||||
ctx.Srv.JWT.Invalidate(&ctx.Token)
|
||||
} else {
|
||||
user = empty
|
||||
}
|
||||
log.Printf("logout: %s%s", user, totp)
|
||||
ctx.SetCookie(nil, 0)
|
||||
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||
}
|
||||
66
pkg/server/helper.go
Normal file
66
pkg/server/helper.go
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
bleve "git.giftfish.de/ston1th/gowiki/pkg/index"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
day = time.Hour * 24
|
||||
min = time.Second * 30
|
||||
max = day * 90
|
||||
|
||||
div = 1048576
|
||||
)
|
||||
|
||||
func duration(duration, mode string) (d time.Duration, err error) {
|
||||
switch mode {
|
||||
case "1":
|
||||
d, err = time.ParseDuration(duration)
|
||||
if err != nil {
|
||||
err = errors.New("could not parse duration")
|
||||
return
|
||||
}
|
||||
case "2":
|
||||
var t int
|
||||
t, err = strconv.Atoi(duration)
|
||||
if err != nil {
|
||||
err = errors.New("could not parse duration")
|
||||
return
|
||||
}
|
||||
d = time.Duration(int(day) * t)
|
||||
default:
|
||||
err = errors.New("invalid mode")
|
||||
return
|
||||
}
|
||||
|
||||
if d < min {
|
||||
err = errors.New("minimum duration is 30 seconds")
|
||||
}
|
||||
if d > max {
|
||||
err = errors.New("maximum duration is 90 days")
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func fmtMem(i uint64) string {
|
||||
return strconv.FormatFloat(float64(i)/div, 'f', 2, 64)
|
||||
}
|
||||
|
||||
func getStats(i *bleve.Index) *core.Stats {
|
||||
mem := new(runtime.MemStats)
|
||||
runtime.ReadMemStats(mem)
|
||||
return &core.Stats{
|
||||
Goroutines: runtime.NumGoroutine(),
|
||||
Alloc: fmtMem(mem.Alloc),
|
||||
Sys: fmtMem(mem.Sys),
|
||||
Docs: i.DocCount(),
|
||||
GoVersion: runtime.Version(),
|
||||
}
|
||||
}
|
||||
187
pkg/server/routes.go
Normal file
187
pkg/server/routes.go
Normal file
|
|
@ -0,0 +1,187 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package server
|
||||
|
||||
import "git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
|
||||
type route struct {
|
||||
Path string
|
||||
Handler ctxHandler
|
||||
Methods []string
|
||||
}
|
||||
|
||||
var routes = []route{
|
||||
{
|
||||
core.RootURI,
|
||||
jwtHandler(
|
||||
indexHandler),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
core.SectionsURI,
|
||||
jwtHandler(
|
||||
sectionsHandler),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
core.SectionURI + "/new",
|
||||
jwtHandler(
|
||||
adminAuthHandler(
|
||||
sectionNewHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
core.SectionURI + "/edit/{section:[a-zA-Z0-9]+$}",
|
||||
jwtHandler(
|
||||
adminAuthHandler(
|
||||
sectionEditHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
core.SectionURI + "/del/{section:[a-zA-Z0-9]+$}",
|
||||
jwtHandler(
|
||||
adminAuthHandler(
|
||||
sectionDelHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
core.SearchURI,
|
||||
jwtHandler(
|
||||
searchHandler),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
core.NewURI,
|
||||
jwtHandler(
|
||||
authHandler(
|
||||
pageNewHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
core.LoginURI,
|
||||
jwtHandler(
|
||||
loginHandler),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
core.TotpURI,
|
||||
jwtHandler(
|
||||
totpAuthHandler(
|
||||
loginTotpHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
core.BlacklistURI,
|
||||
jwtHandler(
|
||||
authHandler(
|
||||
pageBlacklistHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
core.UsersURI,
|
||||
jwtHandler(
|
||||
adminAuthHandler(
|
||||
usersHandler)),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
core.UserURI + "/new",
|
||||
jwtHandler(
|
||||
adminAuthHandler(
|
||||
userNewHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
core.UserURI + "/edit/{user:[a-zA-Z0-9]+$}",
|
||||
jwtHandler(
|
||||
userAuthHandler(
|
||||
userEditHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
core.UserURI + "/del/{user:[a-zA-Z0-9]+$}",
|
||||
jwtHandler(
|
||||
userAuthHandler(
|
||||
userDelHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
core.UserURI + "/totp/{user:[a-zA-Z0-9]+$}",
|
||||
jwtHandler(
|
||||
userAuthHandler(
|
||||
userTotpHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
core.UserURI + "/unlock/{user:[a-zA-Z0-9]+$}",
|
||||
jwtHandler(
|
||||
adminAuthHandler(
|
||||
userUnlockHandler)),
|
||||
[]string{"POST"},
|
||||
},
|
||||
{
|
||||
core.StatsURI,
|
||||
jwtHandler(
|
||||
adminAuthHandler(
|
||||
statsHandler)),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
core.LogoutURI,
|
||||
jwtHandler(
|
||||
logoutHandler),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
core.ViewURI + "/{key}",
|
||||
jwtHandler(
|
||||
pageHandler),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
core.ViewURI + "/{key}/md",
|
||||
jwtHandler(
|
||||
pageMDHandler(
|
||||
pageHandler)),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/{section:[a-zA-Z0-9]+}",
|
||||
jwtHandler(
|
||||
sectionHandler),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/{section:[a-zA-Z0-9]+}/{title:[a-zA-Z0-9+-]+}",
|
||||
jwtHandler(
|
||||
pageHandler),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/{section:[a-zA-Z0-9]+}/{title:[a-zA-Z0-9+-]+}/md",
|
||||
jwtHandler(
|
||||
pageMDHandler(
|
||||
pageHandler)),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/{section:[a-zA-Z0-9]+}/{title:[a-zA-Z0-9+-]+}/share",
|
||||
jwtHandler(
|
||||
pageShareHandler),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/{section:[a-zA-Z0-9]+}/{title:[a-zA-Z0-9+-]+}/edit",
|
||||
jwtHandler(
|
||||
authHandler(
|
||||
pageEditHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/{section:[a-zA-Z0-9]+}/{title:[a-zA-Z0-9+-]+}/del",
|
||||
jwtHandler(
|
||||
authHandler(
|
||||
pageDelHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
}
|
||||
99
pkg/server/server.go
Normal file
99
pkg/server/server.go
Normal file
|
|
@ -0,0 +1,99 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"embed"
|
||||
"errors"
|
||||
"git.giftfish.de/ston1th/godrop/v2"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/db"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/log"
|
||||
"git.giftfish.de/ston1th/jwt/v3"
|
||||
"github.com/gorilla/mux"
|
||||
"html/template"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
cookieName = "gowiki_session"
|
||||
)
|
||||
|
||||
type HTTPServer struct {
|
||||
Config *core.Config
|
||||
|
||||
listener net.Listener
|
||||
srv *http.Server
|
||||
|
||||
DB *db.DB
|
||||
JWT *jwt.JWT
|
||||
|
||||
templ map[string]*template.Template
|
||||
res map[string][]byte
|
||||
}
|
||||
|
||||
func NewHTTPServer(cfg *core.Config, l net.Listener) (srv *HTTPServer) {
|
||||
srv = &HTTPServer{
|
||||
Config: cfg,
|
||||
listener: l,
|
||||
}
|
||||
srv.loadTemplates()
|
||||
srv.srv = &http.Server{
|
||||
Handler: srv.buildRoutes(),
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *HTTPServer) Start() (err error) {
|
||||
s.DB, err = db.New(s.Config)
|
||||
if err != nil {
|
||||
return errors.New("db: " + err.Error())
|
||||
}
|
||||
err = godrop.UnveilBlock()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
opts := []jwt.JWTOption{jwt.WithBlacklist(s.DB), jwt.WithNonce()}
|
||||
if s.Config.Secret != "" {
|
||||
opts = append(opts, jwt.WithSecret(bytes.NewBufferString(s.Config.Secret)))
|
||||
}
|
||||
s.JWT, err = jwt.New(opts...)
|
||||
if err != nil {
|
||||
return errors.New("server: " + err.Error())
|
||||
}
|
||||
go func() {
|
||||
log.Println(s.srv.Serve(s.listener))
|
||||
}()
|
||||
return
|
||||
}
|
||||
|
||||
func (s *HTTPServer) Stop() error {
|
||||
s.DB.Close()
|
||||
s.JWT.Stop()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
|
||||
defer cancel()
|
||||
return s.srv.Shutdown(ctx)
|
||||
}
|
||||
|
||||
//go:embed static/*
|
||||
var static embed.FS
|
||||
|
||||
func (s *HTTPServer) buildRoutes() http.Handler {
|
||||
m := mux.NewRouter()
|
||||
m.NotFoundHandler = ¬FoundHandler{s}
|
||||
m.Handle("/static/{file}", http.StripPrefix("/", http.FileServer(http.FS(static))))
|
||||
for _, v := range routes {
|
||||
m.HandleFunc(v.Path, s.contextWrapper(v.Handler)).Methods(v.Methods...)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
func (s *HTTPServer) contextWrapper(h ctxHandler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
h(newContext(w, r, s))
|
||||
}
|
||||
}
|
||||
12
pkg/server/static/bootstrap.css
vendored
Normal file
12
pkg/server/static/bootstrap.css
vendored
Normal file
File diff suppressed because one or more lines are too long
|
|
@ -22,7 +22,12 @@
|
|||
float: left !important;
|
||||
}
|
||||
}
|
||||
|
||||
body {
|
||||
padding-top: 60px;
|
||||
}
|
||||
.msg {
|
||||
margin-top: 30px;
|
||||
}
|
||||
a.dark {
|
||||
color: #222222;
|
||||
}
|
||||
|
|
@ -30,6 +35,16 @@ a.para {
|
|||
text-decoration: none;
|
||||
font-size: 18px;
|
||||
}
|
||||
nav {
|
||||
padding-top: 20px;
|
||||
}
|
||||
*[id]:before {
|
||||
display: block;
|
||||
content: " ";
|
||||
margin-top: -75px;
|
||||
height: 75px;
|
||||
visibility: hidden;
|
||||
}
|
||||
.alert-primary {
|
||||
background-color: #375a7f;
|
||||
border-color: #375a7f;
|
||||
|
|
@ -37,8 +52,6 @@ a.para {
|
|||
}
|
||||
h1,h2,h3,h4,h5,h6 {
|
||||
font-weight: bold;
|
||||
padding-top: 60px !important;
|
||||
margin-top: -50px !important;
|
||||
-webkit-background-clip: content-box !important;
|
||||
background-clip: content-box !important;
|
||||
}
|
||||
|
|
@ -54,10 +67,7 @@ h3 {
|
|||
h4 {
|
||||
font-size: 1.25em;
|
||||
}
|
||||
h5 {
|
||||
font-size: 1em;
|
||||
}
|
||||
h6 {
|
||||
h5,h6 {
|
||||
font-size: 1em;
|
||||
}
|
||||
.md-text {
|
||||
|
|
@ -77,14 +87,64 @@ pre {
|
|||
border: 1px solid #444444;
|
||||
border-radius: 2px;
|
||||
}
|
||||
pre.wrap {
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
code {
|
||||
padding: 3px 3px 1px 3px;
|
||||
padding: 3px 3px 1px 0px;
|
||||
font-size: 90%;
|
||||
color: #0cdba6;
|
||||
background-color: #444444;
|
||||
border-radius: 2px;
|
||||
}
|
||||
textarea,input {
|
||||
textarea,input,select {
|
||||
color: #e2e2e2 !important;
|
||||
background-color: #444444 !important;
|
||||
}
|
||||
select.sections {
|
||||
height: 120px !important;
|
||||
}
|
||||
input.menu {
|
||||
color: #e2e2e2 !important;
|
||||
background-color: #222222 !important;
|
||||
}
|
||||
.menu-search-bar {
|
||||
padding-top: 4px;
|
||||
}
|
||||
.menu-search-input {
|
||||
background-color: #2f2f2f !important;
|
||||
}
|
||||
html,body,.container,.content {
|
||||
height: 100% !important;
|
||||
}
|
||||
.container,.content {
|
||||
position: relative;
|
||||
}
|
||||
.proper-content {
|
||||
padding-top: 40px;
|
||||
}
|
||||
.section {
|
||||
margin-bottom: 20px;
|
||||
width: 250px;
|
||||
}
|
||||
.wrapper {
|
||||
min-height: 100%;
|
||||
height: auto !important;
|
||||
height: 100%;
|
||||
margin: 0 auto -60px;
|
||||
}
|
||||
.push {
|
||||
height: 60px;
|
||||
}
|
||||
.full-width {
|
||||
width: 100%;
|
||||
}
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
.btn-breadcrumb {
|
||||
position: absolute;
|
||||
right: 1rem;
|
||||
top: 38%;
|
||||
transform: translateY(-50%);
|
||||
}
|
||||
BIN
pkg/server/static/favicon.ico
Normal file
BIN
pkg/server/static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 5.3 KiB |
67
pkg/server/templates.go
Normal file
67
pkg/server/templates.go
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"errors"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/log"
|
||||
"html/template"
|
||||
"io/fs"
|
||||
)
|
||||
|
||||
//go:embed templates/*
|
||||
var templates embed.FS
|
||||
|
||||
func (s *HTTPServer) loadTemplates() {
|
||||
var fatal bool
|
||||
s.templ = make(map[string]*template.Template)
|
||||
s.res = make(map[string][]byte)
|
||||
tfs, err := fs.Sub(templates, "templates")
|
||||
if err != nil {
|
||||
log.Fatal("parse: ", err)
|
||||
}
|
||||
parse := func(html ...string) (temp *template.Template) {
|
||||
var err error
|
||||
temp, err = template.ParseFS(tfs, html...)
|
||||
if err != nil {
|
||||
log.Println("parse:", err)
|
||||
fatal = true
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// pages
|
||||
s.templ["notFoundHandler"] = parse("index.html", "menu.html", "notFound.html")
|
||||
s.templ["forbiddenHandler"] = parse("index.html", "menu.html", "forbidden.html")
|
||||
s.templ["loginHandler"] = parse("index.html", "menu.html", "login.html")
|
||||
s.templ["loginTotpHandler"] = parse("index.html", "menu.html", "loginTotp.html")
|
||||
s.templ["searchHandler"] = parse("index.html", "menu.html", "search.html")
|
||||
s.templ["pageHandler"] = parse("index.html", "menu.html", "page.html")
|
||||
s.templ["pageNewHandler"] = parse("index.html", "menu.html", "pageNew.html")
|
||||
s.templ["pageEditHandler"] = parse("index.html", "menu.html", "pageEdit.html")
|
||||
s.templ["pageViewHandler"] = parse("index.html", "menu.html", "pageView.html")
|
||||
s.templ["pageMDHandler"] = parse("index.html", "menu.html", "pageMD.html")
|
||||
s.templ["pageShareHandler"] = parse("index.html", "menu.html", "pageShare.html")
|
||||
s.templ["pageBlacklistHandler"] = parse("index.html", "menu.html", "pageBlacklist.html")
|
||||
s.templ["pageDelHandler"] = parse("index.html", "menu.html", "pageDel.html")
|
||||
// sections
|
||||
s.templ["sectionHandler"] = parse("index.html", "menu.html", "section.html")
|
||||
s.templ["sectionsHandler"] = parse("index.html", "menu.html", "sections.html")
|
||||
s.templ["sectionNewHandler"] = parse("index.html", "menu.html", "sectionNew.html")
|
||||
s.templ["sectionEditHandler"] = parse("index.html", "menu.html", "sectionEdit.html")
|
||||
s.templ["sectionDelHandler"] = parse("index.html", "menu.html", "sectionDel.html")
|
||||
// users
|
||||
s.templ["usersHandler"] = parse("index.html", "menu.html", "users.html")
|
||||
s.templ["userNewHandler"] = parse("index.html", "menu.html", "userNew.html")
|
||||
s.templ["userEditHandler"] = parse("index.html", "menu.html", "userEdit.html")
|
||||
s.templ["userDelHandler"] = parse("index.html", "menu.html", "userDel.html")
|
||||
// totp
|
||||
s.templ["userTotpHandler"] = parse("index.html", "menu.html", "userTotp.html")
|
||||
// stats
|
||||
s.templ["statsHandler"] = parse("index.html", "menu.html", "stats.html")
|
||||
|
||||
if fatal {
|
||||
log.Fatal("parse: ", errors.New("parsing templates failed"))
|
||||
}
|
||||
}
|
||||
17
pkg/server/templates/forbidden.html
Normal file
17
pkg/server/templates/forbidden.html
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{{define "body"}}
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-danger mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>403 - Forbidden</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5>Access to this page is restricted</h5>
|
||||
<a href="/" class="btn btn-sm btn-primary">Back</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
46
pkg/server/templates/index.html
Normal file
46
pkg/server/templates/index.html
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<title>GoWiki | {{.Title}}</title>
|
||||
<link rel="shortcut icon" type="image/x-icon" href="/static/favicon.ico" integrity="sha256-bgotk/IPq4Yvt6s8AQGPTM5ZjPjQ6rrBRa2EyxpnFSc="></link>
|
||||
<link rel="stylesheet" type="text/css" href="/static/bootstrap.css" media="screen" integrity="sha256-3YeveuySvBgp2GItUS8eV1R/9T1zcPMyvw22SyVf8qo="></link>
|
||||
<link rel="stylesheet" type="text/css" href="/static/custom.css" media="screen" integrity="sha256-IwnADqysqqVpTpQ4sbG3Bz7v8FM4lqMiozQPozsFBUQ="></link>
|
||||
<meta name="theme-color" content="#375a7f">
|
||||
<meta name="description" content="{{.Title}}">
|
||||
</head>
|
||||
<body id="top">
|
||||
<div class="navbar navbar-expand fixed-top navbar-dark bg-primary">
|
||||
<div class="container">
|
||||
<a href="/" class="navbar-brand">GoWiki</a>
|
||||
<div class="collapse navbar-collapse">
|
||||
{{template "menu" .}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
<div class="proper-content">
|
||||
{{if .Msg}}
|
||||
<div class="alert alert-danger msg">
|
||||
<h4>Error!</h4>
|
||||
<p>{{.Msg}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
{{template "body" .}}
|
||||
</div>
|
||||
<div class="push"></div>
|
||||
<footer>
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<ul class="list-unstyled">
|
||||
<li class="float-md-right"><a href="#top">Back to top</a></li>
|
||||
{{if .Login}}
|
||||
<li><a href="/blacklist">Blacklist</a></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
<p>© GoWiki {{.Version}} Request: <strong>{{.Time}}</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
34
pkg/server/templates/login.html
Normal file
34
pkg/server/templates/login.html
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
{{define "body"}}
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-primary mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>{{.BodyTitle}}</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="form-horizontal" action="/login" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<input type="hidden" name="referer" value="{{.Data.Referer}}">
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="user">Username</label>
|
||||
<input class="form-control input-sm" type="text" id="user" name="user" value="{{.Data.User}}" autofocus required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="password">Password</label>
|
||||
<input class="form-control input-sm" type="password" id="password" name="password" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="remember" name="remember">
|
||||
<label class="custom-control-label" for="remember">Remember Me</label>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary" type="submit">Login</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
24
pkg/server/templates/loginTotp.html
Normal file
24
pkg/server/templates/loginTotp.html
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
{{define "body"}}
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-primary mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>{{.BodyTitle}}</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="form-horizontal" action="/totp" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="pin">PIN</label>
|
||||
<input class="form-control input-sm" type="text" id="pin" name="pin" autocomplete="off" autofocus required>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary" type="submit">Verify</button>
|
||||
<a href="/logout" class="btn btn-sm btn-primary">Cancel</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
27
pkg/server/templates/menu.html
Normal file
27
pkg/server/templates/menu.html
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
{{define "menu"}}
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item"><a class="nav-link" href="/wiki/Index">Index</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/sections">Sections</a></li>
|
||||
{{if .Login}}
|
||||
<li class="nav-item"><a class="nav-link" href="/new">New Page</a></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
<form class="form-inline" method="post" action="/search">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<input type="text" class="form-control mr-sm-2 menu" placeholder="Search" name="search" value="{{.Search}}" autocomplete="off">
|
||||
<button class="btn btn-sm btn-info" type="submit">Search</button>
|
||||
</form>
|
||||
<ul class="nav navbar-nav ml-auto">
|
||||
{{if .Login}}
|
||||
{{if .Admin}}
|
||||
<li class="nav-item"><a class="nav-link" href="/users">Users</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/stats">Stats</a></li>
|
||||
{{else}}
|
||||
<li class="nav-item"><a class="nav-link" href="/user/edit/{{.User}}">Profile</a></li>
|
||||
{{end}}
|
||||
<li class="nav-item"><a class="nav-link" href="/logout">Logout</a></li>
|
||||
{{else}}
|
||||
<li class="nav-item"><a class="nav-link" href="/login">Login</a></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}
|
||||
17
pkg/server/templates/notFound.html
Normal file
17
pkg/server/templates/notFound.html
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
{{define "body"}}
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-danger mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>404 - Page Not Found</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<h5>The requested page could not be found</h5>
|
||||
<a href="/" class="btn btn-sm btn-primary">Back</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
40
pkg/server/templates/page.html
Normal file
40
pkg/server/templates/page.html
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
{{define "body"}}
|
||||
<div class="row">
|
||||
<div class="nav-container full-width relative">
|
||||
<ol class="breadcrumb full-width">
|
||||
<li class="breadcrumb-item"></li>
|
||||
<li class="breadcrumb-item"><a href="/{{.BodyTitle}}">{{.BodyTitle}}</a></li>
|
||||
<li class="breadcrumb-item active">{{.Data.Title}}</li>
|
||||
</ol>
|
||||
<div class="btn-group btn-breadcrumb">
|
||||
<a href="/{{.Data.StoreTitle}}/md" class="btn btn-sm btn-primary">Markdown</a>
|
||||
{{if .Login}}
|
||||
<a href="/{{.Data.StoreTitle}}/edit" class="btn btn-sm btn-primary">Edit</a>
|
||||
{{if eq .Data.Perm 2}}
|
||||
<a href="/{{.Data.StoreTitle}}/share" class="btn btn-sm btn-primary">Share</a>
|
||||
{{end}}
|
||||
{{if ne .Data.StoreTitle "wiki/Index"}}
|
||||
<a href="/{{.Data.StoreTitle}}/del" class="btn btn-sm btn-danger">Delete</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col">
|
||||
<strong>Created by:</strong> {{.Data.Created}}<br>
|
||||
{{if .Data.Updated}}
|
||||
<strong>Updated by:</strong> {{.Data.Updated}}<br>
|
||||
{{end}}
|
||||
<strong>Visibility:</strong>
|
||||
{{if eq .Data.Perm 1}}
|
||||
<span class="badge badge-primary">Public</span>
|
||||
{{end}}
|
||||
{{if eq .Data.Perm 2}}
|
||||
<span class="badge badge-danger">Private</span>
|
||||
{{end}}
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
{{.Data.HTML}}
|
||||
{{end}}
|
||||
23
pkg/server/templates/pageBlacklist.html
Normal file
23
pkg/server/templates/pageBlacklist.html
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{{define "body"}}
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-danger mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>{{.BodyTitle}}</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="form-horizontal" action="/blacklist" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="token">Token</label>
|
||||
<input class="form-control input-sm" type="text" id="token" name="share" autocomplete="off" autofocus required>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-danger" type="submit">Blacklist</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
20
pkg/server/templates/pageDel.html
Normal file
20
pkg/server/templates/pageDel.html
Normal file
|
|
@ -0,0 +1,20 @@
|
|||
{{define "body"}}
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-danger mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>Delete {{.Data}}</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="form-horizontal" action="/{{.Data}}/del" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
|
||||
<a href="/{{.Data}}" class="btn btn-sm btn-primary">Back</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
30
pkg/server/templates/pageEdit.html
Normal file
30
pkg/server/templates/pageEdit.html
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{{define "body"}}
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
<h2>{{.BodyTitle}}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<form class="form-horizontal" action="/{{.Data.StoreTitle}}/edit" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="title">Title</label>
|
||||
<input class="form-control input-sm" type="text" id="title" name="title" value="{{.Data.Title}}" spellcheck="false" disabled>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="markdown">Markdown</label>
|
||||
<textarea class="form-control input-sm md-text" id="markdown" name="markdown" rows="30" spellcheck="false">{{.Data.Markdown}}</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-radio">
|
||||
<input type="radio" id="perm1" name="perm" value="1" class="custom-control-input" {{if eq .Data.Perm 1}}checked=""{{end}}>
|
||||
<label class="custom-control-label" for="perm1">Public</label>
|
||||
</div>
|
||||
<div class="custom-control custom-radio">
|
||||
<input type="radio" id="perm2" name="perm" value="2" class="custom-control-input" {{if eq .Data.Perm 2}}checked=""{{end}}>
|
||||
<label class="custom-control-label" for="perm2">Private</label>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary" type="submit">Update</button>
|
||||
<a href="/{{.Data.StoreTitle}}" class="btn btn-sm btn-primary">Back</a>
|
||||
</form>
|
||||
{{end}}
|
||||
14
pkg/server/templates/pageMD.html
Normal file
14
pkg/server/templates/pageMD.html
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
{{define "body"}}
|
||||
<div class="row">
|
||||
<div class="nav-container full-width relative">
|
||||
<ol class="breadcrumb full-width">
|
||||
<li class="breadcrumb-item"></li>
|
||||
<li class="breadcrumb-item"><a href="/{{.BodyTitle}}">{{.BodyTitle}}</a></li>
|
||||
<li class="breadcrumb-item active"><a href="/{{if .Key}}view/{{.Key}}{{else}}{{.Data.StoreTitle}}{{end}}">{{.Data.Title}}</a></li>
|
||||
</ol>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<textarea class="form-control input-sm md-text" rows="30" spellcheck="false" autofocus>{{.Data.Markdown}}</textarea>
|
||||
</div>
|
||||
{{end}}
|
||||
41
pkg/server/templates/pageNew.html
Normal file
41
pkg/server/templates/pageNew.html
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
{{define "body"}}
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
<h2>{{.BodyTitle}}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<form class="form-horizontal" action="/new" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="section">Section</label>
|
||||
<select class="form-control input-sm" id="section" name="section" required>
|
||||
{{range $section, $selected := .Data.Sections}}
|
||||
{{if $selected}}
|
||||
<option value="{{$section}}" selected>{{$section}}</option>
|
||||
{{else}}
|
||||
<option value="{{$section}}">{{$section}}</option>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="title">Title</label>
|
||||
<input class="form-control input-sm" type="text" id="title" name="title" placeholder="Title" value="{{.Data.Page.Title}}" spellcheck="false" autocomplete="off" autofocus required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="markdown">Markdown</label>
|
||||
<textarea class="form-control input-sm md-text" id="markdown" name="markdown" rows="30" spellcheck="false">{{.Data.Page.Markdown}}</textarea>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-radio">
|
||||
<input type="radio" id="perm1" name="perm" value="1" class="custom-control-input" {{if eq .Data.Page.Perm 1}}checked=""{{end}}>
|
||||
<label class="custom-control-label" for="perm1">Public</label>
|
||||
</div>
|
||||
<div class="custom-control custom-radio">
|
||||
<input type="radio" id="perm2" name="perm" value="2" class="custom-control-input" {{if eq .Data.Page.Perm 2}}checked=""{{end}}>
|
||||
<label class="custom-control-label" for="perm2">Private</label>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary" type="submit">Create</button>
|
||||
</form>
|
||||
{{end}}
|
||||
43
pkg/server/templates/pageShare.html
Normal file
43
pkg/server/templates/pageShare.html
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
{{define "body"}}
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-primary mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>Share {{.Data}}</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{if .Key}}
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="key">Key</label>
|
||||
<input class="form-control input-sm" type="text" id="key" value="{{.Key}}" disabled>
|
||||
</div>
|
||||
<a href="/view/{{.Key}}" class="btn btn-sm btn-danger">Share Link</a>
|
||||
<a href="/{{.Data}}/share" class="btn btn-sm btn-primary">Back</a>
|
||||
{{else}}
|
||||
<form class="form-horizontal" action="/{{.Data}}/share" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="duration">Lifetime</label>
|
||||
<input class="form-control input-sm" type="text" id="duration" name="duration" autocomplete="off" autofocus required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-radio">
|
||||
<input type="radio" id="mode1" name="mode" value="1" class="custom-control-input" checked="">
|
||||
<label class="custom-control-label" for="mode1">Parse (15m, 10h)</label>
|
||||
</div>
|
||||
<div class="custom-control custom-radio">
|
||||
<input type="radio" id="mode2" name="mode" value="2" class="custom-control-input">
|
||||
<label class="custom-control-label" for="mode2">Day(s) (Numbers only)</label>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary" type="submit">Share</button>
|
||||
<a href="/{{.Data}}" class="btn btn-sm btn-primary">Back</a>
|
||||
</form>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
31
pkg/server/templates/pageView.html
Normal file
31
pkg/server/templates/pageView.html
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
{{define "body"}}
|
||||
<div class="row">
|
||||
<div class="nav-container full-width relative">
|
||||
<ol class="breadcrumb full-width">
|
||||
<li class="breadcrumb-item"></li>
|
||||
<li class="breadcrumb-item"><a href="/{{.BodyTitle}}">{{.BodyTitle}}</a></li>
|
||||
<li class="breadcrumb-item active">{{.Data.Title}}</li>
|
||||
</ol>
|
||||
<div class="btn-group btn-breadcrumb">
|
||||
<a href="/view/{{.Key}}/md" class="btn btn-sm btn-primary">Markdown</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col">
|
||||
<strong>Created by:</strong> {{.Data.Created}}<br>
|
||||
{{if .Data.Updated}}
|
||||
<strong>Updated by:</strong> {{.Data.Updated}}<br>
|
||||
{{end}}
|
||||
<strong>Visibility:</strong>
|
||||
{{if eq .Data.Perm 1}}
|
||||
<span class="badge badge-primary">Public</span>
|
||||
{{end}}
|
||||
{{if eq .Data.Perm 2}}
|
||||
<span class="badge badge-danger">Private</span>
|
||||
{{end}}
|
||||
<br>
|
||||
</div>
|
||||
</div>
|
||||
{{.Data.HTML}}
|
||||
{{end}}
|
||||
30
pkg/server/templates/search.html
Normal file
30
pkg/server/templates/search.html
Normal file
|
|
@ -0,0 +1,30 @@
|
|||
{{define "body"}}
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
<h2>{{.BodyTitle}}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col-xs-6">
|
||||
<form class="form-horizontal" action="/search" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<div class="form-group">
|
||||
<div class="input-group">
|
||||
<input type="text" class="form-control input-sm" placeholder="Search" name="search" value="{{.Search}}" autocomplete="off" autofocus>
|
||||
<div class="input-group-append">
|
||||
<button class="btn btn-sm btn-primary" type="submit">Search</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col">
|
||||
{{range $item := .Data}}
|
||||
<a href="/{{$item.StoreTitle}}"><b>{{$item.Section}} / {{$item.Title}}</b></a>
|
||||
<pre class="wrap">{{$item.HTML}}</pre><br>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
39
pkg/server/templates/section.html
Normal file
39
pkg/server/templates/section.html
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
{{define "body"}}
|
||||
<!--<div class="page-header">
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
<h2>{{.BodyTitle}}</h2>
|
||||
</div>
|
||||
<div class="col">
|
||||
{{if .Admin}}
|
||||
<div class="btn-group float-right">
|
||||
<a class="btn btn-sm btn-primary" href="/section/edit/{{.BodyTitle}}">Edit</a>
|
||||
<a class="btn btn-sm btn-danger" href="/section/del/{{.BodyTitle}}">Delete</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>-->
|
||||
<div class="row">
|
||||
<div class="nav-container full-width relative">
|
||||
<ol class="breadcrumb full-width">
|
||||
<li class="breadcrumb-item"></li>
|
||||
<li class="breadcrumb-item">{{.BodyTitle}}</li>
|
||||
</ol>
|
||||
{{if .Admin}}
|
||||
<div class="btn-group btn-breadcrumb">
|
||||
<a class="btn btn-sm btn-primary" href="/section/edit/{{.BodyTitle}}">Edit</a>
|
||||
<a class="btn btn-sm btn-danger" href="/section/del/{{.BodyTitle}}">Delete</a>
|
||||
</div>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{if .Data}}
|
||||
<div class="row">
|
||||
<ul type="circle">
|
||||
{{range $item := .Data}}
|
||||
<li><a href="/{{$item.StoreTitle}}"><b>{{$item.Title}}</b></a></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
23
pkg/server/templates/sectionDel.html
Normal file
23
pkg/server/templates/sectionDel.html
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{{define "body"}}
|
||||
{{if .Data}}
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-danger mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>Delete {{.Data}}?</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>You are about to delete section <a href="/{{.Data}}">{{.Data}}</a>.<br>This will also remove all pages of this section.</p>
|
||||
<form class="form-horizontal" action="/section/del/{{.Data}}" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
|
||||
<a href="/{{.Data}}" class="btn btn-sm btn-primary">Back</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
34
pkg/server/templates/sectionEdit.html
Normal file
34
pkg/server/templates/sectionEdit.html
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
{{define "body"}}
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-primary mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>{{.BodyTitle}}</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="form-horizontal" action="/section/edit/{{.Data.Name}}" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="section">Section</label>
|
||||
<input class="form-control input-sm" type="text" id="section" name="section" value="{{.Data.Name}}" disabled>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="members">Members</label>
|
||||
<select class="form-control input-sm sections" id="members" name="members" multiple required>
|
||||
{{range $user, $member := .Data.Members}}
|
||||
{{if $member}}
|
||||
<option value="{{$user}}" selected>{{$user}}</option>
|
||||
{{else}}
|
||||
<option value="{{$user}}">{{$user}}</option>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary" type="submit">Update</button>
|
||||
<a href="/{{.Data.Name}}" class="btn btn-sm btn-primary">Back</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
34
pkg/server/templates/sectionNew.html
Normal file
34
pkg/server/templates/sectionNew.html
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
{{define "body"}}
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-primary mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>{{.BodyTitle}}</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="form-horizontal" action="/section/new" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="section">Section</label>
|
||||
<input class="form-control input-sm" type="text" id="section" name="section" autocomplete="off" value="{{.Data.Name}}" autofocus required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="members">Members</label>
|
||||
<select class="form-control input-sm sections" id="members" name="members" multiple required>
|
||||
{{range $user, $member := .Data.Members}}
|
||||
{{if $member}}
|
||||
<option value="{{$user}}" selected>{{$user}}</option>
|
||||
{{else}}
|
||||
<option value="{{$user}}">{{$user}}</option>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</select>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary" type="submit">Create</button>
|
||||
<a href="/sections" class="btn btn-sm btn-primary">Back</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
37
pkg/server/templates/sections.html
Normal file
37
pkg/server/templates/sections.html
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
{{define "body"}}
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
<h2>{{.BodyTitle}}</h2>
|
||||
</div>
|
||||
</div>
|
||||
{{if .Admin}}
|
||||
<div class="row mb-3">
|
||||
<div class="col-xs-5">
|
||||
<a class="btn btn-sm btn-primary" href="/section/new"><b>New Section</b></a>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{if .Data}}
|
||||
<div class="row">
|
||||
{{range $index, $item := .Data}}
|
||||
<div class="col-xs-4 section">
|
||||
<div class="card border-primary mx-auto">
|
||||
<div class="card-header">
|
||||
<strong><a href="/{{$item.Section}}">{{$item.Section}}</a></strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
{{if $item.Pages}}
|
||||
{{range $page := $item.Pages}}
|
||||
<strong><a href="/{{$page.StoreTitle}}">{{$page.Title}}</a></strong><br>
|
||||
{{end}}
|
||||
{{else}}
|
||||
<strong>Empty section</strong>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="offset-1"></div>
|
||||
{{end}}
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
33
pkg/server/templates/stats.html
Normal file
33
pkg/server/templates/stats.html
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
{{define "body"}}
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
<h2>{{.BodyTitle}}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<table class="table table-stripped table-hover">
|
||||
<tbody>
|
||||
<tr>
|
||||
<td><b>Goroutines</b></td>
|
||||
<td>{{.Data.Goroutines}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Memory usage</b></td>
|
||||
<td>{{.Data.Alloc}} MiB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Memory allocated</b></td>
|
||||
<td>{{.Data.Sys}} MiB</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Documents</b></td>
|
||||
<td>{{.Data.Docs}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td><b>Go Version</b></td>
|
||||
<td>{{.Data.GoVersion}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{{end}}
|
||||
23
pkg/server/templates/userDel.html
Normal file
23
pkg/server/templates/userDel.html
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
{{define "body"}}
|
||||
{{if .Data}}
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-danger mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>Delete {{.Data.Username}}?</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<p>You are about to delete user <a href="/{{.Data.Username}}">{{.Data.Username}}</a>.<br>This will also remove all pages of this user.</p>
|
||||
<form class="form-horizontal" action="/user/del/{{.Data.Username}}" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
|
||||
<a href="/users" class="btn btn-sm btn-primary">Back</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
65
pkg/server/templates/userEdit.html
Normal file
65
pkg/server/templates/userEdit.html
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
{{define "body"}}
|
||||
{{if .Data}}
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-primary mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>{{.BodyTitle}}</strong>
|
||||
{{if .Data.Secret}}
|
||||
<a href="/user/totp/{{.Data.Username}}" class="btn btn-sm btn-danger float-right">Disable TOTP</a>
|
||||
{{else}}
|
||||
<a href="/user/totp/{{.Data.Username}}" class="btn btn-sm btn-success float-right">Enable TOTP</a>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="form-horizontal" action="/user/edit/{{.Data.Username}}" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="user">Username</label>
|
||||
<input class="form-control input-sm" type="text" id="user" name="user" value="{{.Data.Username}}" disabled>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="password">Password</label>
|
||||
<input class="form-control input-sm" type="password" id="password" name="password" autofocus>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="repeat">Repeat</label>
|
||||
<input class="form-control input-sm" type="password" id="repeat" name="repeat">
|
||||
</div>
|
||||
{{if .Admin}}
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-checkbox">
|
||||
{{if .Data.Admin}}
|
||||
<input type="checkbox" class="custom-control-input" id="admin" name="admin" value="0" checked>
|
||||
{{else}}
|
||||
<input type="checkbox" class="custom-control-input" id="admin" name="admin" value="0">
|
||||
{{end}}
|
||||
<label class="custom-control-label" for="admin">Admin Privileges</label>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<button class="btn btn-sm btn-primary" type="submit">Update</button>
|
||||
{{if .Admin}}
|
||||
<a href="/users" class="btn btn-sm btn-primary">Back</a>
|
||||
{{else}}
|
||||
<a href="/" class="btn btn-sm btn-primary">Back</a>
|
||||
{{end}}
|
||||
<a class="btn btn-sm btn-danger" href="/user/del/{{.Data.Username}}">Delete</a>
|
||||
</form>
|
||||
{{if .Admin}}
|
||||
{{if eq .Data.Locked 3}}
|
||||
<form class="form-horizontal" action="/user/unlock/{{.Data.Username}}" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="unlock">Locked</label>
|
||||
<button class="btn btn-sm btn-warning" type="submit" id="unlock">Unlock</button>
|
||||
</div>
|
||||
</form>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
36
pkg/server/templates/userNew.html
Normal file
36
pkg/server/templates/userNew.html
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
{{define "body"}}
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-primary mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>{{.BodyTitle}}</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="form-horizontal" action="/user/new" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="user">Username</label>
|
||||
<input class="form-control input-sm" type="text" id="user" name="user" autocomplete="off" autofocus required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="password">Password</label>
|
||||
<input class="form-control input-sm" type="password" id="password" name="password" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="repeat">Repeat</label>
|
||||
<input class="form-control input-sm" type="password" id="repeat" name="repeat" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="custom-control custom-checkbox">
|
||||
<input type="checkbox" class="custom-control-input" id="admin" name="admin" value="0">
|
||||
<label class="custom-control-label" for="admin">Admin Privileges</label>
|
||||
</div>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary" type="submit">Create</button>
|
||||
<a href="/users" class="btn btn-sm btn-primary">Back</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
38
pkg/server/templates/userTotp.html
Normal file
38
pkg/server/templates/userTotp.html
Normal file
|
|
@ -0,0 +1,38 @@
|
|||
{{define "body"}}
|
||||
{{if .Data}}
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-primary mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>{{.BodyTitle}} - {{.Data.Username}}</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="form-horizontal" action="/user/totp/{{.Data.Username}}" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
{{if .Data.Secret}}
|
||||
<input type="hidden" name="url" value="{{.Data.URL}}">
|
||||
<div class="form-group">
|
||||
<img src="{{.Data.Image}}" alt="{{.Data.Secret}}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="secret">Secret</label>
|
||||
<pre><code id="secret">{{.Data.Secret}}</code></pre>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="pin">PIN</label>
|
||||
<input class="form-control input-sm" type="text" id="pin" name="pin" autocomplete="off" autofocus required>
|
||||
</div>
|
||||
{{if .Data.Secret}}
|
||||
<button class="btn btn-sm btn-success" type="submit">Enable</button>
|
||||
{{else}}
|
||||
<button class="btn btn-sm btn-danger" type="submit">Disable</button>
|
||||
{{end}}
|
||||
<a href="/user/edit/{{.Data.Username}}" class="btn btn-sm btn-primary">Back</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
55
pkg/server/templates/users.html
Normal file
55
pkg/server/templates/users.html
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
{{define "body"}}
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
<h2>{{.BodyTitle}}</h2>
|
||||
</div>
|
||||
</div>
|
||||
{{if .Admin}}
|
||||
<div class="row mb-3">
|
||||
<div class="col-xs-5">
|
||||
<a class="btn btn-sm btn-primary" href="/user/new"><b>New User</b></a>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="row">
|
||||
<div class="col">
|
||||
{{if .Data}}
|
||||
<table class="table table-hover">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Username</th>
|
||||
<th>Flags</th>
|
||||
<th>Options</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{$user := .User}}
|
||||
{{$admin := .Admin}}
|
||||
{{range $item := .Data}}
|
||||
<tr>
|
||||
<td>{{$item.Username}}</td>
|
||||
<td>
|
||||
{{if $item.Secret}}<span class="badge badge-success">TOTP</span>{{end}}
|
||||
{{if eq $item.Locked 3}}<span class="badge badge-warning">Locked</span>{{end}}
|
||||
{{if $item.Admin}}<span class="badge badge-danger">Admin</span>{{end}}
|
||||
</td>
|
||||
<td>
|
||||
<div class="btn-group">
|
||||
{{if $admin}}
|
||||
<a class="btn btn-sm btn-primary" href="/user/edit/{{$item.Username}}">Edit</a>
|
||||
<a class="btn btn-sm btn-danger" href="/user/del/{{$item.Username}}">Delete</a>
|
||||
{{else}}
|
||||
{{if eq $item.Username $user}}
|
||||
<a class="btn btn-sm btn-primary" href="/user/edit/{{$item.Username}}">Edit</a>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
44
pkg/server/xsrf.go
Normal file
44
pkg/server/xsrf.go
Normal file
|
|
@ -0,0 +1,44 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/log"
|
||||
)
|
||||
|
||||
const (
|
||||
keySize = 16
|
||||
fullSize = keySize * 2
|
||||
)
|
||||
|
||||
func genKey(length int) (bytes []byte) {
|
||||
bytes = make([]byte, length)
|
||||
if _, err := rand.Reader.Read(bytes); err != nil {
|
||||
log.Println("genKey:", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func newXsrf(secret []byte) string {
|
||||
mac := hmac.New(sha256.New, secret)
|
||||
rnd := genKey(keySize)
|
||||
mac.Write(rnd)
|
||||
return base64.RawURLEncoding.EncodeToString(append(rnd, mac.Sum(nil)[:keySize]...))
|
||||
}
|
||||
|
||||
func checkXsrf(xsrf string, secret []byte) bool {
|
||||
t, err := base64.RawURLEncoding.DecodeString(xsrf)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if len(t) != fullSize {
|
||||
return false
|
||||
}
|
||||
mac := hmac.New(sha256.New, secret)
|
||||
mac.Write(t[:keySize])
|
||||
return hmac.Equal(t[keySize:], mac.Sum(nil)[:keySize])
|
||||
}
|
||||
31
pkg/server/xsrf_test.go
Normal file
31
pkg/server/xsrf_test.go
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package server
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestXsrf(t *testing.T) {
|
||||
sec := genKey(keySize)
|
||||
if len(sec) != keySize {
|
||||
t.Fatal("len(sec) != keySize")
|
||||
}
|
||||
xsrf := newXsrf(sec)
|
||||
if xsrf == "" {
|
||||
t.Fatal("xsrf is empty")
|
||||
}
|
||||
|
||||
if !checkXsrf(xsrf, sec) {
|
||||
t.Fatal("valid xsrf check failed")
|
||||
}
|
||||
|
||||
if checkXsrf("test", sec) {
|
||||
t.Fatal("invalid xsrf check succeeded")
|
||||
}
|
||||
|
||||
rnd := genKey(keySize)
|
||||
if checkXsrf(xsrf, rnd) {
|
||||
t.Fatal("invalid xsrf check succeeded")
|
||||
}
|
||||
}
|
||||
139
pkg/store/boltstore.go
Normal file
139
pkg/store/boltstore.go
Normal file
|
|
@ -0,0 +1,139 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package store
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
bolt "go.etcd.io/bbolt"
|
||||
"io"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultBoltBucket = "default"
|
||||
fileMode = 0o600
|
||||
)
|
||||
|
||||
type BoltStore struct {
|
||||
Marshaler
|
||||
db *bolt.DB
|
||||
}
|
||||
|
||||
func NewBoltStore(file string, m Marshaler) (Store, error) {
|
||||
if m == nil {
|
||||
m = NewGOB()
|
||||
}
|
||||
db, err := bolt.Open(file, fileMode, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := db.Update(func(tx *bolt.Tx) (err error) {
|
||||
_, err = tx.CreateBucketIfNotExists([]byte(defaultBoltBucket))
|
||||
return
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &BoltStore{m, db}, nil
|
||||
}
|
||||
|
||||
type dump struct {
|
||||
K string `json:"k"`
|
||||
V []byte `json:"v"`
|
||||
}
|
||||
|
||||
func (bs *BoltStore) Dump(w io.Writer) (err error) {
|
||||
if w == nil {
|
||||
return ErrWriterIsNil
|
||||
}
|
||||
var d []dump
|
||||
err = bs.ForEach(func(k string, v []byte) error {
|
||||
b := make([]byte, len(v))
|
||||
copy(b, v)
|
||||
d = append(d, dump{k, b})
|
||||
return nil
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return json.NewEncoder(w).Encode(d)
|
||||
}
|
||||
|
||||
func (bs *BoltStore) Restore(r io.Reader) (err error) {
|
||||
if r == nil {
|
||||
return ErrReaderIsNil
|
||||
}
|
||||
var d []dump
|
||||
err = json.NewDecoder(r).Decode(&d)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if err = bs.db.Update(func(tx *bolt.Tx) (err error) {
|
||||
if err = tx.DeleteBucket([]byte(defaultBoltBucket)); err != nil {
|
||||
return
|
||||
}
|
||||
_, err = tx.CreateBucket([]byte(defaultBoltBucket))
|
||||
return
|
||||
}); err != nil {
|
||||
return
|
||||
}
|
||||
return bs.db.Update(func(tx *bolt.Tx) (err error) {
|
||||
b := tx.Bucket([]byte(defaultBoltBucket))
|
||||
for _, v := range d {
|
||||
b.Put([]byte(v.K), v.V)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (bs *BoltStore) Get(key string, v interface{}) (err error) {
|
||||
err = bs.db.View(func(tx *bolt.Tx) error {
|
||||
b := tx.Bucket([]byte(defaultBoltBucket)).Get([]byte(key))
|
||||
if b == nil {
|
||||
return ErrKeyNotFound
|
||||
}
|
||||
if v != nil {
|
||||
return bs.Unmarshal(b, v)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
func (bs *BoltStore) Set(key string, v interface{}) error {
|
||||
return bs.db.Update(func(tx *bolt.Tx) (err error) {
|
||||
var b []byte
|
||||
if v != nil {
|
||||
b, err = bs.Marshal(v)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return tx.Bucket([]byte(defaultBoltBucket)).Put([]byte(key), b)
|
||||
})
|
||||
}
|
||||
|
||||
func (bs *BoltStore) ForEach(f func(string, []byte) error) error {
|
||||
return bs.db.View(func(tx *bolt.Tx) error {
|
||||
return tx.Bucket([]byte(defaultBoltBucket)).ForEach(func(k, v []byte) error {
|
||||
return f(string(k), v)
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
func (bs *BoltStore) ForEachPrefix(prefix string, f func(string, []byte) error) error {
|
||||
return bs.ForEach(func(k string, v []byte) error {
|
||||
if trim, ok := hasTrimPrefix(k, prefix); ok {
|
||||
return f(trim, v)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
}
|
||||
|
||||
func (bs *BoltStore) Delete(key string) error {
|
||||
return bs.db.Update(func(tx *bolt.Tx) error {
|
||||
return tx.Bucket([]byte(defaultBoltBucket)).Delete([]byte(key))
|
||||
})
|
||||
}
|
||||
|
||||
func (bs *BoltStore) Close() error {
|
||||
return bs.db.Close()
|
||||
}
|
||||
98
pkg/store/boltstore_test.go
Normal file
98
pkg/store/boltstore_test.go
Normal file
|
|
@ -0,0 +1,98 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package store
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type testData struct {
|
||||
Data string
|
||||
}
|
||||
|
||||
func TestBoltStore(t *testing.T) {
|
||||
data := []testData{
|
||||
{"123"},
|
||||
{"hello"},
|
||||
}
|
||||
dir, err := os.MkdirTemp("", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(dir)
|
||||
bs, err := NewBoltStore(filepath.Join(dir, "bolt.db"), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
err = bs.Set("key0", data[0])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
err = bs.Set("key1", data[1])
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
t.Run("GetKey0", func(t *testing.T) {
|
||||
var d testData
|
||||
err := bs.Get("key0", &d)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if d != data[0] {
|
||||
t.Error("d is not data[0]")
|
||||
}
|
||||
})
|
||||
t.Run("ForEach", func(t *testing.T) {
|
||||
err := bs.ForEach(func(k string, v []byte) error {
|
||||
if k == "key0" || k == "key1" {
|
||||
return nil
|
||||
}
|
||||
return errors.New("key0 or key1 not found")
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
t.Run("DumpRestore", func(t *testing.T) {
|
||||
buf := new(bytes.Buffer)
|
||||
err := bs.Dump(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if buf.Len() == 0 {
|
||||
t.Error("no dump written")
|
||||
}
|
||||
err = bs.Restore(buf)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
t.Run("DeleteKey1", func(t *testing.T) {
|
||||
err := bs.Delete("key1")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
t.Run("GetKey1", func(t *testing.T) {
|
||||
var d testData
|
||||
err := bs.Get("key1", &d)
|
||||
if err == nil {
|
||||
t.Error("key1 should be deleted")
|
||||
}
|
||||
if d == data[1] {
|
||||
t.Error("d is not data[1]")
|
||||
}
|
||||
})
|
||||
t.Run("Close", func(t *testing.T) {
|
||||
err := bs.Close()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
})
|
||||
}
|
||||
13
pkg/store/dump.go
Normal file
13
pkg/store/dump.go
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package store
|
||||
|
||||
import "io"
|
||||
|
||||
type Dumper interface {
|
||||
Dump(io.Writer) error
|
||||
}
|
||||
|
||||
type Restorer interface {
|
||||
Restore(io.Reader) error
|
||||
}
|
||||
25
pkg/store/gob.go
Normal file
25
pkg/store/gob.go
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package store
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/gob"
|
||||
)
|
||||
|
||||
type gobMarshaler struct{}
|
||||
|
||||
func NewGOB() Marshaler {
|
||||
return gobMarshaler{}
|
||||
}
|
||||
|
||||
func (gobMarshaler) Marshal(v interface{}) (b []byte, err error) {
|
||||
buf := new(bytes.Buffer)
|
||||
err = gob.NewEncoder(buf).Encode(v)
|
||||
b = buf.Bytes()
|
||||
return
|
||||
}
|
||||
|
||||
func (gobMarshaler) Unmarshal(data []byte, v interface{}) error {
|
||||
return gob.NewDecoder(bytes.NewBuffer(data)).Decode(v)
|
||||
}
|
||||
39
pkg/store/gob_test.go
Normal file
39
pkg/store/gob_test.go
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package store
|
||||
|
||||
import "testing"
|
||||
|
||||
var g = NewGOB()
|
||||
|
||||
func BenchmarkGOBMarshalPage(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
g.Marshal(p)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGOBUnmarshalPage(b *testing.B) {
|
||||
bytes, _ := g.Marshal(p)
|
||||
var pp Page
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
g.Unmarshal(bytes, &pp)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGOBMarshalUser(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
g.Marshal(u)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkGOBUnmarshalUser(b *testing.B) {
|
||||
bytes, _ := g.Marshal(u)
|
||||
var uu User
|
||||
b.ReportAllocs()
|
||||
for i := 0; i < b.N; i++ {
|
||||
g.Unmarshal(bytes, &uu)
|
||||
}
|
||||
}
|
||||
11
pkg/store/helper.go
Normal file
11
pkg/store/helper.go
Normal file
|
|
@ -0,0 +1,11 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package store
|
||||
|
||||
// Parts taken from strings.TrimPrefix
|
||||
func hasTrimPrefix(s, prefix string) (string, bool) {
|
||||
if len(s) >= len(prefix) && s[0:len(prefix)] == prefix {
|
||||
return s[len(prefix):], true
|
||||
}
|
||||
return s, false
|
||||
}
|
||||
8
pkg/store/marshal.go
Normal file
8
pkg/store/marshal.go
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package store
|
||||
|
||||
type Marshaler interface {
|
||||
Marshal(v interface{}) ([]byte, error)
|
||||
Unmarshal(data []byte, v interface{}) error
|
||||
}
|
||||
34
pkg/store/marshal_test.go
Normal file
34
pkg/store/marshal_test.go
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package store
|
||||
|
||||
import "time"
|
||||
|
||||
type Page struct {
|
||||
Text string
|
||||
Date time.Time
|
||||
Number int
|
||||
Test bool
|
||||
}
|
||||
|
||||
type User struct {
|
||||
Username string
|
||||
Password string
|
||||
Number int
|
||||
Test bool
|
||||
Today time.Time
|
||||
}
|
||||
|
||||
var p = &Page{
|
||||
"Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet. Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat, sed diam voluptua. At vero eos et accusam et justo duo dolores et ea rebum. Stet clita kasd gubergren, no sea takimata sanctus est Lorem ipsum dolor sit amet.",
|
||||
time.Now(),
|
||||
34534534,
|
||||
true,
|
||||
}
|
||||
var u = &User{
|
||||
"svdfnvdnfvon",
|
||||
"func BenchmarkNew(b *testing.B) {",
|
||||
8794355,
|
||||
false,
|
||||
time.Now(),
|
||||
}
|
||||
23
pkg/store/store.go
Normal file
23
pkg/store/store.go
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
// Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
package store
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
ErrKeyNotFound = errors.New("store: key not found")
|
||||
ErrWriterIsNil = errors.New("store: writer is nil")
|
||||
ErrReaderIsNil = errors.New("store: reader is nil")
|
||||
)
|
||||
|
||||
type Store interface {
|
||||
Marshaler
|
||||
Dumper
|
||||
Restorer
|
||||
Get(string, interface{}) error
|
||||
Set(string, interface{}) error
|
||||
ForEach(func(string, []byte) error) error
|
||||
ForEachPrefix(string, func(string, []byte) error) error
|
||||
Delete(string) error
|
||||
Close() error
|
||||
}
|
||||
107
render.go
107
render.go
|
|
@ -1,107 +0,0 @@
|
|||
// Copyright (C) 2017 Marius Schellenberger
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"html/template"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/blevesearch/bleve/analysis"
|
||||
htmlFilter "github.com/blevesearch/bleve/analysis/char_filters/html_char_filter"
|
||||
"github.com/russross/blackfriday"
|
||||
)
|
||||
|
||||
var filter = makeFilter()
|
||||
|
||||
func makeFilter() analysis.CharFilter {
|
||||
f, _ := htmlFilter.CharFilterConstructor(nil, nil)
|
||||
return f
|
||||
}
|
||||
|
||||
var (
|
||||
reHeader = regexp.MustCompile("(<h[1-6])>(.+)(</h[1-6]>)")
|
||||
reLink = regexp.MustCompile(`id="(.+)">`)
|
||||
whiteSpace = regexp.MustCompile(`\s+`)
|
||||
reIndex = regexp.MustCompile("<h([1-6])>(.+)</h[1-6]>")
|
||||
)
|
||||
|
||||
func render(text string) Article {
|
||||
rend := blackfriday.MarkdownCommon([]byte(text))
|
||||
html := string(rend)
|
||||
index := buildIndex(html)
|
||||
html = strings.Replace(html, "<table>", `<table class="table">`, -1)
|
||||
html = reHeader.ReplaceAllString(html, `${1} id="${2}">${2}${3}`)
|
||||
html = reLink.ReplaceAllStringFunc(html, makeLinkTitle)
|
||||
html = strings.Replace(html, "</h1>", `</h1><hr>`, -1)
|
||||
html = strings.Replace(html, "<a", `<a target="_blank"`, -1)
|
||||
return Article{
|
||||
Search: whiteSpace.ReplaceAllString(string(filter.Filter(rend)), " "),
|
||||
Index: template.HTML(index),
|
||||
Text: template.HTML(html),
|
||||
MD: text,
|
||||
}
|
||||
}
|
||||
|
||||
func makeLinkTitle(title string) string {
|
||||
return strings.Replace(title, " ", "+", -1)
|
||||
}
|
||||
|
||||
func buildIndex(html string) (ret string) {
|
||||
hn := reIndex.FindAllStringSubmatch(html, -1)
|
||||
for k, v := range hn {
|
||||
link := makeLinkTitle(v[2])
|
||||
ret += before(hn, k)
|
||||
ret += `<li><a href="#` + link + `">` + v[2] + "</a>"
|
||||
ret += after(hn, k)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func before(arr [][]string, i int) string {
|
||||
c, _ := strconv.Atoi(arr[i][1])
|
||||
if c == 1 {
|
||||
return `<ul type="circle">`
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func after(arr [][]string, i int) string {
|
||||
c, _ := strconv.Atoi(arr[i][1])
|
||||
if len(arr) <= i+1 {
|
||||
return closeTag(c-1) + "</ul>"
|
||||
}
|
||||
n, _ := strconv.Atoi(arr[i+1][1])
|
||||
if n == 1 {
|
||||
return closeTag(c-n) + "</ul>"
|
||||
} else if n < c {
|
||||
return closeTag(c - n)
|
||||
} else if n > c {
|
||||
return openTag(n - c)
|
||||
} else if n == c {
|
||||
return "</li>"
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func openTag(j int) (ret string) {
|
||||
for i := 0; i < j; i++ {
|
||||
ret += `<ul type="circle"><li>`
|
||||
}
|
||||
if j > 1 {
|
||||
ret = strings.Replace(ret, "circle", "none", j-1)
|
||||
}
|
||||
ret = ret[0 : len(ret)-4]
|
||||
return
|
||||
}
|
||||
|
||||
func closeTag(j int) (ret string) {
|
||||
for i := 0; i < j; i++ {
|
||||
ret += "</li></ul></li>"
|
||||
}
|
||||
if j > 1 {
|
||||
ret = strings.Replace(ret, "</li></li>", "</li>", -1)
|
||||
}
|
||||
return
|
||||
}
|
||||
135
routes.go
135
routes.go
|
|
@ -1,135 +0,0 @@
|
|||
// Copyright (C) 2017 Marius Schellenberger
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"github.com/gorilla/mux"
|
||||
)
|
||||
|
||||
type route struct {
|
||||
Path string
|
||||
Handler http.HandlerFunc
|
||||
Methods []string
|
||||
}
|
||||
|
||||
func buildRoutes() http.Handler {
|
||||
m := mux.NewRouter()
|
||||
m.NotFoundHandler = notFoundHandler{}
|
||||
for _, v := range routes {
|
||||
m.HandleFunc(v.Path, v.Handler).Methods(v.Methods...)
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
var routes = []route{
|
||||
{
|
||||
"/",
|
||||
contextWrapper(
|
||||
indexHandler),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/bootstrap.css",
|
||||
contextWrapper(
|
||||
staticHandler),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/custom.css",
|
||||
contextWrapper(
|
||||
staticHandler),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/login",
|
||||
contextWrapper(
|
||||
loginHandler),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/search",
|
||||
contextWrapper(
|
||||
searchHandler),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/new",
|
||||
contextWrapper(
|
||||
wikiNewHandler),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/all",
|
||||
contextWrapper(
|
||||
allHandler),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/wiki/{title:[a-zA-Z0-9+]+$}",
|
||||
contextWrapper(
|
||||
wikiHandler),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/wiki/{title:[a-zA-Z0-9+]+}/md",
|
||||
contextWrapper(
|
||||
wikiMDHandler(
|
||||
wikiHandler)),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/wiki/{title:[a-zA-Z0-9+]+}/edit",
|
||||
contextWrapper(
|
||||
wikiEditHandler),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/wiki/{title:[a-zA-Z0-9+]+}/del",
|
||||
contextWrapper(
|
||||
wikiDelHandler),
|
||||
[]string{"POST"},
|
||||
},
|
||||
{
|
||||
"/users",
|
||||
contextWrapper(
|
||||
adminAuthHandler(
|
||||
usersHandler)),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/user/new",
|
||||
contextWrapper(
|
||||
adminAuthHandler(
|
||||
userNewHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/user/del/{user:[a-z]+$}",
|
||||
contextWrapper(
|
||||
adminAuthHandler(
|
||||
userDelHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/user/unlock/{user:[a-z]+$}",
|
||||
contextWrapper(
|
||||
adminAuthHandler(
|
||||
userUnlockHandler)),
|
||||
[]string{"POST"},
|
||||
},
|
||||
{
|
||||
"/user/edit/{user:[a-z]+$}",
|
||||
contextWrapper(
|
||||
userAuthHandler(
|
||||
userEditHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/logout",
|
||||
contextWrapper(
|
||||
logoutHandler),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
}
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
#!/bin/sh
|
||||
### BEGIN INIT INFO
|
||||
# Provides: gowiki
|
||||
# Required-Start: $syslog $network
|
||||
# Required-Stop: $syslog
|
||||
# Default-Start: 2 3 4 5
|
||||
# Default-Stop: 0 1 6
|
||||
# Short-Description: GoWiki
|
||||
# Description: GoWiki
|
||||
### END INIT INFO
|
||||
|
||||
PATH=/sbin:/usr/sbin:/bin:/usr/bin
|
||||
DESC="wiki engine"
|
||||
NAME=gowiki
|
||||
DAEMON=/usr/bin/${NAME}
|
||||
DAEMON_OPTS="-s"
|
||||
PID=$(ps aux|grep "${DAEMON}"|awk '!/grep/{print $2}')
|
||||
|
||||
. /lib/lsb/init-functions
|
||||
|
||||
do_start() {
|
||||
${DAEMON} ${DAEMON_OPTS}
|
||||
log_end_msg ${?}
|
||||
}
|
||||
|
||||
do_stop() {
|
||||
kill ${PID}
|
||||
return "${?}"
|
||||
}
|
||||
|
||||
case "${1}" in
|
||||
start)
|
||||
log_action_msg "Starting ${DESC}" "${NAME}"
|
||||
do_start
|
||||
;;
|
||||
stop)
|
||||
log_daemon_msg "Stopping ${DESC}" "${NAME}"
|
||||
do_stop
|
||||
;;
|
||||
restart)
|
||||
log_daemon_msg "Restarting ${DESC}" "${NAME}"
|
||||
do_stop
|
||||
if [ ${?} -ne 0 ]; then
|
||||
log_end_msg 1
|
||||
fi
|
||||
do_start
|
||||
;;
|
||||
status)
|
||||
status_of_proc -p ${PID} "${DAEMON}" "${NAME}" && exit 0 || exit ${?}
|
||||
;;
|
||||
*)
|
||||
echo "Usage: service ${NAME} {start|stop|restart|status}"
|
||||
exit 3
|
||||
;;
|
||||
esac
|
||||
|
|
@ -1,4 +0,0 @@
|
|||
#!/bin/sh
|
||||
useradd wiki
|
||||
mkdir /var/{lib,log}/gowiki
|
||||
chown wiki: /var/{lib,log}/gowiki
|
||||
32
scripts/install_linux.sh
Executable file
32
scripts/install_linux.sh
Executable file
|
|
@ -0,0 +1,32 @@
|
|||
#!/bin/sh
|
||||
groupadd wiki
|
||||
useradd -g wiki wiki
|
||||
mkdir /var/lib/gowiki
|
||||
chown wiki: /var/lib/gowiki
|
||||
mkdir /etc/gowiki
|
||||
cat <<EOF> /etc/gowiki/gowiki.conf
|
||||
{
|
||||
"data_dir": "/var/lib/gowiki",
|
||||
"secret": "$(openssl rand -hex 32)"
|
||||
}
|
||||
EOF
|
||||
cat <<EOF> /etc/systemd/system/gowiki.service
|
||||
[Unit]
|
||||
Description=GoWiki
|
||||
After=syslog.target
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=forking
|
||||
ExecStart=/usr/local/bin/gowiki server -c /etc/gowiki/gowiki.conf
|
||||
Restart=always
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
systemctl enable gowiki
|
||||
cat <<EOF
|
||||
copy your gowiki binary to /usr/local/bin/gowiki
|
||||
|
||||
then run: systemctl start gowiki
|
||||
EOF
|
||||
41
scripts/install_openbsd.sh
Executable file
41
scripts/install_openbsd.sh
Executable file
|
|
@ -0,0 +1,41 @@
|
|||
#!/bin/sh
|
||||
|
||||
cat <<'EOF'> /etc/rc.d/gowiki
|
||||
#!/bin/ksh
|
||||
|
||||
daemon="/usr/local/bin/gowiki"
|
||||
daemon_user="_wiki"
|
||||
daemon_logger="daemon.info"
|
||||
|
||||
. /etc/rc.d/rc.subr
|
||||
|
||||
pexp=".*${daemon}.*"
|
||||
rc_bg=YES
|
||||
rc_reload=NO
|
||||
|
||||
rc_cmd $1
|
||||
EOF
|
||||
chmod 555 /etc/rc.d/gowiki
|
||||
rcctl enable gowiki
|
||||
rcctl set gowiki flags server -c /var/wiki/gowiki.conf
|
||||
|
||||
mkdir /var/wiki
|
||||
groupadd _wiki
|
||||
useradd -d /var/wiki -s /sbin/nologin -g _wiki _wiki
|
||||
chmod 750 /var/wiki
|
||||
chown _wiki:_wiki /var/wiki
|
||||
|
||||
cat <<EOF> /var/wiki/gowiki.conf
|
||||
{
|
||||
"data_dir": "/var/wiki",
|
||||
"secret": "$(openssl rand -hex 32)"
|
||||
}
|
||||
EOF
|
||||
chmod 640 /var/wiki/gowiki.conf
|
||||
chgrp _wiki /var/wiki/gowiki.conf
|
||||
|
||||
cat <<EOF
|
||||
copy your gowiki binary to /usr/local/bin/gowiki
|
||||
|
||||
then run: rcctl start gowiki
|
||||
EOF
|
||||
11
sri.sh
11
sri.sh
|
|
@ -1,9 +1,10 @@
|
|||
#!/bin/bash
|
||||
# Copyright (C) 2017 Marius Schellenberger
|
||||
# Copyright (C) 2021 Marius Schellenberger
|
||||
|
||||
HASH="sha256"
|
||||
DIR="templates"
|
||||
FILES="*.css *.js *.ico"
|
||||
DIR="pkg/server/static"
|
||||
FILES="*.css *.js"
|
||||
BINS="*.ico"
|
||||
echo "SRI (subresource integrity) hashes"
|
||||
cd ${DIR}
|
||||
for f in $(ls ${FILES} 2>/dev/null); do
|
||||
|
|
@ -12,3 +13,7 @@ for f in $(ls ${FILES} 2>/dev/null); do
|
|||
printf "%13s: " ${f}
|
||||
echo "${HASH}-$(cat ${f}|openssl dgst -${HASH} -binary|openssl enc -base64 -A)"
|
||||
done
|
||||
for f in $(ls ${BINS} 2>/dev/null); do
|
||||
printf "%13s: " ${f}
|
||||
echo "${HASH}-$(cat ${f}|openssl dgst -${HASH} -binary|openssl enc -base64 -A)"
|
||||
done
|
||||
|
|
|
|||
641
templates.go
641
templates.go
File diff suppressed because one or more lines are too long
94
templates.sh
94
templates.sh
|
|
@ -1,94 +0,0 @@
|
|||
#!/bin/bash
|
||||
# Copyright (C) 2017 Marius Schellenberger
|
||||
|
||||
# this file generates Go source code for the gowiki
|
||||
# it contains html and css and uses the contents
|
||||
# of the templates directory
|
||||
|
||||
templates_go="templates.go"
|
||||
echo "Generating templates.go"
|
||||
|
||||
if [ ! -d templates ]; then
|
||||
echo "error: templates directory not found" 1>&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
cat << EOF > ${templates_go}
|
||||
// Copyright (C) 2017 Marius Schellenberger
|
||||
|
||||
// This file is auto-generated by build/templates.sh
|
||||
// using the contents of the templates directory
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"html/template"
|
||||
)
|
||||
|
||||
EOF
|
||||
|
||||
echo "const (" >> ${templates_go}
|
||||
|
||||
for f in $(ls templates/*.html); do
|
||||
fname=${f/templates\//}
|
||||
fname=${fname/.html/}
|
||||
echo " ${fname} = \`$(cat ${f})\`" >> ${templates_go}
|
||||
done
|
||||
|
||||
for f in $(ls templates/*.css); do
|
||||
fname=${f/templates\//}
|
||||
fname=${fname/.css/CSS}
|
||||
echo " ${fname} = \`$(cat ${f})\`" >> ${templates_go}
|
||||
done
|
||||
|
||||
cat << EOF >> ${templates_go}
|
||||
)
|
||||
|
||||
var (
|
||||
templ map[string]*template.Template
|
||||
res map[string][]byte
|
||||
)
|
||||
|
||||
func loadTemplates() {
|
||||
var errf bool
|
||||
templ = make(map[string]*template.Template)
|
||||
res = make(map[string][]byte)
|
||||
parse := func(html ...string) (temp *template.Template) {
|
||||
var err error
|
||||
temp = template.New("")
|
||||
for _, s := range html {
|
||||
temp, err = temp.Parse(s)
|
||||
if err != nil {
|
||||
log.Println("parse:", err)
|
||||
errf = true
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// resources
|
||||
res["bootstrap.css"] = []byte(bootstrapCSS)
|
||||
res["custom.css"] = []byte(customCSS)
|
||||
|
||||
// pages.go
|
||||
templ["notFoundHandler"] = parse(index, menu, notFound)
|
||||
templ["loginHandler"] = parse(index, menu, login)
|
||||
templ["searchHandler"] = parse(index, menu, search)
|
||||
templ["allHandler"] = parse(index, menu, all)
|
||||
templ["wikiNewHandler"] = parse(index, menu, wikiNew)
|
||||
templ["wikiEditHandler"] = parse(index, menu, wikiEdit)
|
||||
templ["wikiHandler"] = parse(index, menu, wiki)
|
||||
templ["wikiMDHandler"] = parse(index, menu, wikiMD)
|
||||
// users.go
|
||||
templ["usersHandler"] = parse(index, menu, users)
|
||||
templ["userNewHandler"] = parse(index, menu, userNew)
|
||||
templ["userEditHandler"] = parse(index, menu, userEdit)
|
||||
templ["userDelHandler"] = parse(index, menu, userDel)
|
||||
|
||||
if errf {
|
||||
log.Fatal("parse: ", errors.New("parsing templates failed"))
|
||||
}
|
||||
}
|
||||
EOF
|
||||
|
|
@ -1,16 +0,0 @@
|
|||
{{define "body"}}
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
<h2>{{.BodyTitle}}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<ul type="circle">
|
||||
{{if .Data}}
|
||||
{{range $item := .Data}}
|
||||
<li><a href="/wiki/{{$item.LinkTitle}}"><b>{{$item.Title}}</b></a></li>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</ul>
|
||||
</div>
|
||||
{{end}}
|
||||
11
templates/bootstrap.css
vendored
11
templates/bootstrap.css
vendored
File diff suppressed because one or more lines are too long
|
|
@ -1,29 +0,0 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<title>GoWiki | {{.Title}}</title>
|
||||
<link rel="stylesheet" type="text/css" href="/bootstrap.css" mdeia="screen" integrity="sha256-KC5lAzGRWwscU0sTmXtd8ka5mt7Vk8a0L5JqOhwA28s="></link>
|
||||
<link rel="stylesheet" type="text/css" href="/custom.css" integrity="sha256-uAIARpL4VhLyhfuh5bS4YWjdcAvN+KjjTli9S8Ud8O4="></link>
|
||||
</head>
|
||||
<body style="padding-top:50px">
|
||||
<div class="navbar navbar-default navbar-fixed-top">
|
||||
<div class="container">
|
||||
<div class="navbar-header">
|
||||
<a href="/" class="navbar-brand">GoWiki</a>
|
||||
</div>
|
||||
<div class="navbar-collapse collapse">
|
||||
{{template "menu" .}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="container">
|
||||
{{if .Msg}}
|
||||
<div class="alert alert-danger" style="margin-top:20px">
|
||||
<h4>Error!</h4>
|
||||
<p>{{.Msg}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
{{template "body" .}}
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -1,37 +0,0 @@
|
|||
{{define "body"}}
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
<div class="col-xs-6 col-xs-offset-3">
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">{{.BodyTitle}}</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<form class="form-horizontal" action="/login" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<fieldset>
|
||||
<div class="form-group">
|
||||
<label class="col-lg-4 control-label">Username</label>
|
||||
<div class="col-lg-6">
|
||||
<input class="form-control input-sm" type="text" name="user" autofocus required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-lg-4 control-label">Password</label>
|
||||
<div class="col-lg-6">
|
||||
<input class="form-control input-sm" type="password" name="password" required>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="col-lg-6 col-lg-offset-4">
|
||||
<button class="btn btn-sm btn-primary" type="submit" name="submit" value="Login">Login</button>
|
||||
</div>
|
||||
</div>
|
||||
</fieldset>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
@ -1,49 +0,0 @@
|
|||
{{define "menu"}}
|
||||
{{if .Login}}
|
||||
<ul class="nav navbar-nav">
|
||||
<li><a href="/wiki/Index">
|
||||
Index
|
||||
</a></li>
|
||||
<li><a href="/search">
|
||||
Search
|
||||
</a></li>
|
||||
<li><a href="/all">
|
||||
All
|
||||
</a></li>
|
||||
<li><a href="/new">
|
||||
New
|
||||
</a></li>
|
||||
</ul>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
{{if .Admin}}
|
||||
<li><a href="/users">
|
||||
Users
|
||||
</a></li>
|
||||
{{else}}
|
||||
<li><a href="/user/edit/{{.User}}">
|
||||
Profile
|
||||
</a></li>
|
||||
{{end}}
|
||||
<li><a href="/logout">
|
||||
Logout
|
||||
</a></li>
|
||||
</ul>
|
||||
{{else}}
|
||||
<ul class="nav navbar-nav">
|
||||
<li><a href="/wiki/Index">
|
||||
Index
|
||||
</a></li>
|
||||
<li><a href="/search">
|
||||
Search
|
||||
</a></li>
|
||||
<li><a href="/all">
|
||||
All
|
||||
</a></li>
|
||||
</ul>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li><a href="/login">
|
||||
Login
|
||||
</a></li>
|
||||
</ul>
|
||||
{{end}}
|
||||
{{end}}
|
||||
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Add a link
Reference in a new issue