first working version

This commit is contained in:
ston1th 2019-04-28 18:16:40 +02:00
commit de359ab415
47 changed files with 1016 additions and 2012 deletions

View file

@ -1 +1 @@
v2.0 v1.0

BIN
docstore Executable file

Binary file not shown.

View file

@ -14,17 +14,18 @@ import (
"net" "net"
"os" "os"
"os/signal" "os/signal"
"path/filepath"
"syscall" "syscall"
"time" "time"
) )
const ( const (
defDataDir = "/var/lib/docstore" defRunDir = "/var/lib/docstore"
defListen = "127.0.0.1:8080" defListen = "127.0.0.1:8080"
defLogFile = "docstore.log" defLogFile = "docstore.log"
defRunUser = "wiki" defRunUser = "docstore"
defRunGroup = "wiki" defRunGroup = "docstore"
) )
var ( var (
@ -50,7 +51,7 @@ func initServer(cfg core.Config) (err error) {
if err != nil { if err != nil {
return return
} }
if err = os.Chdir(cfg.DataDir); err != nil { if err = os.Chdir(cfg.RunDir); err != nil {
return return
} }
@ -65,6 +66,11 @@ func initServer(cfg core.Config) (err error) {
log.InitLogger(cfg) log.InitLogger(cfg)
cfg.DataDir = filepath.Join(cfg.RunDir, core.DataDir)
err = godrop.Unveil(cfg.DataDir, "rwc")
if err != nil {
return
}
srv := server.NewHTTPServer(cfg, l) srv := server.NewHTTPServer(cfg, l)
err = srv.Start() err = srv.Start()
if err != nil { if err != nil {
@ -105,7 +111,7 @@ func loadConfig() error {
func Run(version string) { func Run(version string) {
app := cli.NewApp() app := cli.NewApp()
app.Name = "docstore" app.Name = "docstore"
app.Usage = "a simple wiki engine" app.Usage = "file server and indexing engine"
app.Version = version app.Version = version
app.Commands = []cli.Command{ app.Commands = []cli.Command{
{ {
@ -125,7 +131,7 @@ func Run(version string) {
}, },
{ {
Name: "dump", Name: "dump",
Usage: "create a json dump of all articles in the data directory (docstore needs to be stopped)", Usage: "create a json dump of all users (docstore needs to be stopped)",
Flags: defaultFlags(dumpRestoreFlags()), Flags: defaultFlags(dumpRestoreFlags()),
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
var ( var (
@ -158,7 +164,7 @@ func Run(version string) {
}, },
{ {
Name: "restore", Name: "restore",
Usage: "restore a json dump to the data directory (docstore needs to be stopped)", Usage: "restore a json dump (docstore needs to be stopped)",
Flags: defaultFlags(dumpRestoreFlags()), Flags: defaultFlags(dumpRestoreFlags()),
Action: func(c *cli.Context) error { Action: func(c *cli.Context) error {
var ( var (
@ -224,11 +230,6 @@ func serverFlags() []cli.Flag {
Usage: "static hmac secret (at least 64 chars, used for JWT signing)", Usage: "static hmac secret (at least 64 chars, used for JWT signing)",
Destination: &config.Secret, Destination: &config.Secret,
}, },
cli.BoolTFlag{
Name: "admin, a",
Usage: "use -a=false to disable the admin user",
Destination: &config.Admin,
},
cli.BoolFlag{ cli.BoolFlag{
Name: "foreground, f", Name: "foreground, f",
Usage: "do not fork into the background", Usage: "do not fork into the background",
@ -266,10 +267,10 @@ func defaultFlags(f []cli.Flag) []cli.Flag {
Destination: &configFile, Destination: &configFile,
}, },
cli.StringFlag{ cli.StringFlag{
Name: "data, d", Name: "run, r",
Value: defDataDir, Value: defRunDir,
Usage: "data directory", Usage: "run directory",
Destination: &config.DataDir, Destination: &config.RunDir,
}, },
}, f...) }, f...)
} }

View file

@ -8,6 +8,16 @@ var (
LoginURI = "/login" LoginURI = "/login"
LogoutURI = "/logout" LogoutURI = "/logout"
RawPrefix = "/raw"
IndexPrefix = "/index"
MovePrefix = "/move"
DeletePrefix = "/delete"
Favicon = "/favicon.ico"
BootstrapCSS = "/bootstrap.css"
CustomCSS = "/custom.css"
DataDir = "data"
AutoImport = "auto_import" AutoImport = "auto_import"
ManualImport = "manual_import" ManualImport = "manual_import"
Imported = "imported" Imported = "imported"

View file

@ -2,16 +2,6 @@ package core
import "time" import "time"
// 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
}
const ( const (
timeFmt = "2006-01-02 15:04:05" timeFmt = "2006-01-02 15:04:05"
fileFmt = "20060102_150405" fileFmt = "20060102_150405"

View file

@ -1,41 +0,0 @@
// Copyright (C) 2019 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
}

View file

@ -1,77 +0,0 @@
// Copyright (C) 2019 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()
}
})
}
}

View file

@ -1,6 +1,7 @@
package core package core
import ( import (
"errors"
"os/exec" "os/exec"
"time" "time"
) )
@ -24,7 +25,12 @@ func Timeout(cmd *exec.Cmd, t time.Duration) (output []byte, err error) {
} }
err = <-done err = <-done
case e := <-done: case e := <-done:
err = e exerr, ok := e.(*exec.ExitError)
if ok {
err = errors.New(e.Error() + "\n" + string(exerr.Stderr))
} else {
err = e
}
output = <-out output = <-out
} }
return return

View file

@ -4,52 +4,6 @@ package core
import "html/template" import "html/template"
type Page struct {
Title string `json:"title"`
StoreTitle string `json:"store_title"`
Markdown string `json:"markdown"`
TOC template.HTML `json:"toc"`
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 { type User struct {
Username string Username string
Password string Password string
@ -59,11 +13,25 @@ type User struct {
type Result struct { type Result struct {
ID string ID string
Path string
HTML template.HTML HTML template.HTML
} }
type Path struct {
Name string
Abs string
Indexed bool
}
type Paths []Path
func (p Paths) Len() int { return len(p) }
func (p Paths) Swap(i, j int) { p[i], p[j] = p[j], p[i] }
func (p Paths) Less(i, j int) bool { return p[i].Name < p[j].Name }
type Config struct { type Config struct {
DataDir string `json:"data_dir,omitempty"` RunDir string `json:"run_dir,omitempty"`
DataDir string `json:"-"`
User string `json:"user,omitempty"` User string `json:"user,omitempty"`
Group string `json:"group,omitempty"` Group string `json:"group,omitempty"`
ListenAddr string `json:"listen_addr,omitempty"` ListenAddr string `json:"listen_addr,omitempty"`

View file

@ -25,7 +25,7 @@ type DB struct {
func New(cfg core.Config) (db *DB, err error) { func New(cfg core.Config) (db *DB, err error) {
db = new(DB) db = new(DB)
dbFile := filepath.Join(cfg.DataDir, storeFile) dbFile := filepath.Join(cfg.RunDir, storeFile)
err = godrop.Unveil(dbFile, "rwc") err = godrop.Unveil(dbFile, "rwc")
if err != nil { if err != nil {
return return
@ -34,12 +34,12 @@ func New(cfg core.Config) (db *DB, err error) {
if err != nil { if err != nil {
return return
} }
db.Index, err = index.NewIndex(filepath.Join(cfg.DataDir, blevePath)) db.Index, err = index.NewIndex(filepath.Join(cfg.RunDir, blevePath))
if err != nil { if err != nil {
return return
} }
err = db.UnlockUser() err = db.UnlockUser()
if err != nil { if err != nil && err != store.ErrKeyNotFound {
return return
} }
err = db.RunMigrations() err = db.RunMigrations()

80
pkg/db/index.go Normal file
View file

@ -0,0 +1,80 @@
// Copyright (C) 2019 Marius Schellenberger
package db
import (
"git.giftfish.de/ston1th/docstore/pkg/core"
)
const (
idPrefix = "id/"
pathPrefix = "path/"
)
func (db *DB) GetPaths(res []core.Result) error {
for i, v := range res {
err := db.store.Get(pathPrefix+v.ID, &res[i].Path)
if err != nil {
return err
}
}
return nil
}
func (db *DB) GetIndexed(p core.Paths) {
for i, v := range p {
p[i].Indexed = db.IsIndexed(v.Abs)
}
}
func (db *DB) IsIndexed(path string) bool {
return db.store.Get(idPrefix+path, nil) == nil
}
func (db *DB) GetPath(id string) (path string, err error) {
err = db.store.Get(pathPrefix+id, &path)
return
}
func (db *DB) GetID(path string) (id string, err error) {
err = db.store.Get(idPrefix+path, &id)
return
}
func (db *DB) DeletePath(id string) error {
return db.store.Delete(pathPrefix + id)
}
func (db *DB) DeleteID(path string) error {
return db.store.Delete(idPrefix + path)
}
func (db *DB) NewFile(id, path string) (err error) {
err = db.store.Set(idPrefix+path, id)
if err != nil {
return
}
err = db.store.Set(pathPrefix+id, path)
return
}
func (db *DB) MoveFile(op, np string) error {
id, err := db.DeleteFile(op)
if err != nil {
return err
}
return db.NewFile(id, np)
}
func (db *DB) DeleteFile(path string) (id string, err error) {
id, err = db.GetID(path)
if err != nil {
return
}
err = db.DeleteID(path)
if err != nil {
return
}
err = db.DeletePath(id)
return
}

72
pkg/fs/file.go Normal file
View file

@ -0,0 +1,72 @@
package fs
import (
"errors"
"git.giftfish.de/ston1th/docstore/pkg/core"
"os"
"path/filepath"
"strings"
)
func (fs *Filesystem) GetFile(path string) (f *os.File, fi os.FileInfo, err error) {
path = strings.TrimPrefix(filepath.Clean(path), core.RawPrefix)
p := filepath.Join(fs.Base, path)
f, err = os.Open(p)
if err != nil {
return
}
fi, err = f.Stat()
if err != nil {
return
}
if !fi.Mode().IsRegular() {
err = os.ErrNotExist
}
return
}
func (fs *Filesystem) Mode(path string) (mode FileMode, err error) {
path = filepath.Clean(path)
p := filepath.Join(fs.Base, path)
fi, err := os.Stat(p)
if err != nil {
return
}
m := fi.Mode()
if m.IsDir() {
mode = IsDir
} else if m.IsRegular() {
mode = IsFile
}
return
}
func (fs *Filesystem) Move(path, dest, name string) (string, error) {
path = filepath.Clean(path)
if name == "" {
_, name = filepath.Split(path)
}
newfile := filepath.Clean(filepath.Join(dest, name))
return newfile, os.Rename(filepath.Join(fs.Base, path), filepath.Join(fs.Base, newfile))
}
func (fs *Filesystem) Delete(path string) (removed []string, err error) {
path = filepath.Clean(path)
fullpath := filepath.Join(fs.Base, path)
if fullpath == fs.Base {
return nil, errors.New("the root path can not be deleted")
}
fi, err := os.Stat(path)
if err != nil {
return
}
m := fi.Mode()
if m.IsDir() {
err = os.RemoveAll(fullpath)
removed = fs.RecursiveFiles(path)
} else if m.IsRegular() {
err = os.Remove(fullpath)
removed = []string{path}
}
return
}

120
pkg/fs/fs.go Normal file
View file

@ -0,0 +1,120 @@
package fs
import (
"errors"
"git.giftfish.de/ston1th/docstore/pkg/core"
"os"
"path/filepath"
"sort"
"strings"
)
type FileMode int
const (
Undefined FileMode = iota
IsDir
IsFile
)
// Directory represents a directory and its contents
type Directory struct {
Dirs core.Paths
Files core.Paths
}
type Filesystem struct {
Base string
}
func NewFilesystem(base string) (fs *Filesystem, err error) {
fi, err := os.Stat(base)
if err != nil {
return
}
if !fi.IsDir() {
err = errors.New("base dir '" + base + "' is not a directory")
}
fs = &Filesystem{base}
return
}
func (fs *Filesystem) ReadDir(path string) (d *Directory) {
path = filepath.Clean(path)
combined := filepath.Join(fs.Base, path)
f, err := os.Open(combined)
if err != nil {
return
}
names, err := f.Readdirnames(0)
if err != nil {
return
}
d = new(Directory)
for _, n := range names {
fi, err := os.Stat(filepath.Join(combined, n))
if err != nil {
continue
}
m := fi.Mode()
if m.IsDir() {
d.Dirs = append(d.Dirs, core.Path{Name: n, Abs: filepath.Join(path, n)})
continue
}
if m.IsRegular() {
d.Files = append(d.Files, core.Path{Name: n, Abs: filepath.Join(path, n)})
}
}
sort.Sort(d.Dirs)
sort.Sort(d.Files)
return
}
func (fs *Filesystem) Recursive() (p core.Paths) {
filepath.Walk(fs.Base, func(path string, info os.FileInfo, err error) error {
if err != nil {
return nil
}
if info.IsDir() {
a := strings.TrimPrefix(path, fs.Base)
if a == "" {
a = "/"
}
p = append(p, core.Path{Abs: a})
}
return nil
})
sort.Sort(p)
return
}
func (fs *Filesystem) RecursiveFiles(p string) (files []string) {
p = filepath.Clean(p)
fullpath := filepath.Join(fs.Base, p)
filepath.Walk(fullpath, func(path string, info os.FileInfo, err error) error {
if err != nil {
return nil
}
if info.Mode().IsRegular() {
files = append(files, strings.TrimPrefix(path, fullpath))
}
return nil
})
return
}
func Paths(path string) (p core.Paths) {
path = filepath.Clean(path)
s := strings.Split(path, string(filepath.Separator))
for i, v := range s[1:] {
if v == "" {
continue
}
if i == 0 {
p = append(p, core.Path{Name: v, Abs: "/" + v})
continue
}
p = append(p, core.Path{Name: v, Abs: filepath.Join(p[i-1].Abs, v)})
}
return
}

12
pkg/index/id.go Normal file
View file

@ -0,0 +1,12 @@
package index
import (
"crypto/rand"
"encoding/hex"
)
func newID() string {
b := make([]byte, 16)
rand.Read(b)
return hex.EncodeToString(b)
}

View file

@ -51,15 +51,17 @@ func (i *Index) Close() error {
return i.i.Close() return i.i.Close()
} }
func (i *Index) Add(id, text, docType string) error { func (i *Index) Add(text, docType string) (id string, err error) {
doc, _ := i.i.Document(id) id = newID()
if doc != nil { for {
err := i.Delete(id) doc, _ := i.i.Document(id)
if err != nil { if doc == nil {
return err break
} }
id = newID()
} }
return i.i.Index(id, indexDocument{text, core.Now(), docType}) err = i.i.Index(id, indexDocument{text, core.Now(), docType})
return
} }
func (i *Index) Delete(id string) error { func (i *Index) Delete(id string) error {

View file

@ -11,6 +11,7 @@ import (
"html/template" "html/template"
"net/http" "net/http"
"net/url" "net/url"
"reflect"
"time" "time"
) )
@ -21,59 +22,29 @@ const (
refererClaim = "referer" refererClaim = "referer"
) )
func newContext(w http.ResponseWriter, r *http.Request, s *HTTPServer) (ctx *Context) { func newContext(w http.ResponseWriter, r *http.Request, s *HTTPServer) *Context {
h := w.Header() h := w.Header()
h.Set("X-Frame-Options", "DENY") h.Set("X-Frame-Options", "sameorigin")
h.Set("X-Content-Type-Options", "nosniff") h.Set("X-Content-Type-Options", "nosniff")
h.Set("X-XSS-Protection", "1; mode=block") 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("Content-Security-Policy", "default-src 'none';object-src 'self';frame-src 'self';style-src 'self';img-src 'self' https: data:;connect-src 'self';frame-ancestors 'self'")
h.Set("Referrer-Policy", "same-origin") h.Set("Referrer-Policy", "same-origin")
ctx = &Context{ return &Context{
Request: r, Request: r,
Response: w, Response: w,
Srv: s, Srv: s,
Time: time.Now(), Time: time.Now(),
} }
path := ctx.Path()
if path == "/bootstrap.css" || path == "/custom.css" || path == "/favicon.ico" {
return
}
if c, err := r.Cookie(cookieName); err == nil {
t, err := jwt.DecodeToken(c.Value)
if err != nil {
ctx.LogSetCookie("DecodeToken:", err)
return
}
if err = s.JWT.Verify(t); err != nil {
ctx.LogSetCookie("VerifyToken:", err)
return
}
if t.Claims.GetString(totpClaim) != "" {
if path == core.TotpURI || path == core.LoginURI || path == core.LogoutURI {
ctx.Token = *t
return
}
ctx.Redirect(core.TotpURI, http.StatusFound)
return nil
}
if err = s.JWT.Invalidate(t); err != nil {
log.Println("Invalidate:", err)
}
}
ctx.SetCookie(nil, 0)
return
} }
type Context struct { type Context struct {
Request *http.Request Request *http.Request
Response http.ResponseWriter Response http.ResponseWriter
Srv *HTTPServer Srv *HTTPServer
T *template.Template T *template.Template
Status int HTTPStatus int
Err error Err error
Time time.Time Time time.Time
Token jwt.Token Token jwt.Token
@ -96,7 +67,9 @@ type webData struct {
Msg string Msg string
Search string Search string
Data interface{} Path string
Paths core.Paths
Data interface{}
} }
type loginData struct { type loginData struct {
@ -104,20 +77,10 @@ type loginData struct {
Referer 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() { func (c *Context) Exec() {
defer c.log() defer c.Log()
if c.T == nil { if c.T == nil {
c.Status = http.StatusInternalServerError c.Status(http.StatusInternalServerError)
c.Err = errors.New("template is nil") c.Err = errors.New("template is nil")
return return
} }
@ -132,24 +95,24 @@ func (c *Context) Exec() {
c.Data.Time = time.Since(c.Time).Nanoseconds() / 1e6 c.Data.Time = time.Since(c.Time).Nanoseconds() / 1e6
if c.Data.Msg != "" { if c.Data.Msg != "" {
c.Status = http.StatusBadRequest c.Status(http.StatusBadRequest)
} }
c.Err = c.T.Execute(c.Response, c.Data) c.Err = c.T.Execute(c.Response, c.Data)
if c.Err != nil { if c.Err != nil {
c.Status = http.StatusInternalServerError c.Status(http.StatusInternalServerError)
}
if c.Status == 0 {
c.Status = http.StatusOK
} }
} }
func (c *Context) log() { func (c *Context) Log() {
if c.HTTPStatus == 0 {
c.HTTPStatus = int(reflect.Indirect(reflect.ValueOf(c.Response)).FieldByName("status").Int())
}
if c.Err != nil { 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) log.Printf("%s %s %s %d error: %s\n", c.Request.RemoteAddr, c.Request.Method, c.Request.URL, c.HTTPStatus, c.Err)
return return
} }
log.Printf("%s %s %s %d\n", c.Request.RemoteAddr, c.Request.Method, c.Request.URL, c.Status) log.Printf("%s %s %s %d\n", c.Request.RemoteAddr, c.Request.Method, c.Request.URL, c.HTTPStatus)
} }
func (c *Context) Error(i interface{}) { func (c *Context) Error(i interface{}) {
@ -165,20 +128,19 @@ func (c *Context) Error(i interface{}) {
func (c *Context) NotFound() { func (c *Context) NotFound() {
c.Template("notFoundHandler") c.Template("notFoundHandler")
c.Data = webData{Title: "404"} c.Data = webData{Title: "404"}
c.Status = http.StatusNotFound c.Status(http.StatusNotFound)
c.Response.WriteHeader(http.StatusNotFound)
c.Exec() c.Exec()
} }
func (c *Context) Forbidden() { func (c *Context) Forbidden() {
c.Template("forbiddenHandler") c.Template("forbiddenHandler")
c.Data = webData{Title: "403"} c.Data = webData{Title: "403"}
c.Status = http.StatusForbidden c.Status(http.StatusForbidden)
c.Response.WriteHeader(http.StatusForbidden)
c.Exec() c.Exec()
} }
func (c *Context) SwitchHandler() { func (c *Context) SwitchHandler() {
log.Debug("server: switching to normal handler")
c.Srv.srv.Handler = c.Srv.handler c.Srv.srv.Handler = c.Srv.handler
c.Redirect(core.IndexURI, http.StatusFound) c.Redirect(core.IndexURI, http.StatusFound)
} }
@ -192,6 +154,11 @@ func (c *Context) Write(buf []byte) (err error) {
return return
} }
func (c *Context) Status(status int) {
c.HTTPStatus = status
c.Response.WriteHeader(status)
}
func (c *Context) SetHeader(name, value string) { func (c *Context) SetHeader(name, value string) {
c.Response.Header().Set(name, value) c.Response.Header().Set(name, value)
} }

View file

@ -4,13 +4,20 @@ package server
import ( import (
"git.giftfish.de/ston1th/docstore/pkg/core" "git.giftfish.de/ston1th/docstore/pkg/core"
"git.giftfish.de/ston1th/docstore/pkg/fs"
"git.giftfish.de/ston1th/docstore/pkg/log" "git.giftfish.de/ston1th/docstore/pkg/log"
"git.giftfish.de/ston1th/docstore/pkg/otp" "git.giftfish.de/ston1th/docstore/pkg/otp"
"git.giftfish.de/ston1th/jwt/v3" "git.giftfish.de/ston1th/jwt/v3"
"net/http" "net/http"
"path/filepath"
"strings"
"time" "time"
) )
//TODO add missing CSRF checks
const noSuchFile = "no such file or directory"
type notFoundHandler struct { type notFoundHandler struct {
s *HTTPServer s *HTTPServer
} }
@ -29,13 +36,66 @@ func totpAuthHandler(h ctxHandler) ctxHandler {
} }
} }
func jwtRegisterHandler(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)
return
}
if err = ctx.Srv.JWT.Verify(t); err != nil {
ctx.LogSetCookie("VerifyToken:", err)
return
}
ctx.Token = *t
h(ctx)
return
}
ctx.SetCookie(nil, 0)
h(ctx)
}
}
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)
return
}
if err = ctx.Srv.JWT.Verify(t); err != nil {
ctx.LogSetCookie("VerifyToken:", err)
return
}
path := ctx.Path()
if t.Claims.GetString(totpClaim) != "" {
if path == core.TotpURI || path == core.LoginURI || path == core.LogoutURI {
ctx.Token = *t
h(ctx)
return
}
ctx.Redirect(core.TotpURI, http.StatusFound)
return
}
ctx.Token = *t
h(ctx)
return
}
ctx.SetCookie(nil, 0)
h(ctx)
}
}
func authHandler(h ctxHandler) ctxHandler { func authHandler(h ctxHandler) ctxHandler {
return func(ctx *Context) { return func(ctx *Context) {
if ctx.LoggedOn() { if ctx.LoggedOn() {
h(ctx) h(ctx)
return return
} }
ctx.Forbidden() //ctx.Forbidden()
ctx.Redirect(core.LoginURI, http.StatusFound)
} }
} }
@ -44,11 +104,11 @@ func staticHandler(ctx *Context) {
ctx.SetHeader("Content-Type", "text/css; charset=utf-8") ctx.SetHeader("Content-Type", "text/css; charset=utf-8")
ctx.SetHeader("Expires", time.Now().UTC().Add(max).Format(http.TimeFormat)) ctx.SetHeader("Expires", time.Now().UTC().Add(max).Format(http.TimeFormat))
switch ctx.Path() { switch ctx.Path() {
case "/bootstrap.css": case core.BootstrapCSS:
err = ctx.Write(ctx.Srv.res["bootstrap.css"]) err = ctx.Write(ctx.Srv.res["bootstrap.css"])
case "/custom.css": case core.CustomCSS:
err = ctx.Write(ctx.Srv.res["custom.css"]) err = ctx.Write(ctx.Srv.res["custom.css"])
case "/favicon.ico": case core.Favicon:
ctx.SetHeader("Content-Type", "image/x-icon") ctx.SetHeader("Content-Type", "image/x-icon")
err = ctx.Write(ctx.Srv.res["favicon.ico"]) err = ctx.Write(ctx.Srv.res["favicon.ico"])
} }
@ -75,12 +135,14 @@ func registerHandler(ctx *Context) {
case "GET": case "GET":
ctx.Exec() ctx.Exec()
case "POST": case "POST":
if !ctx.CheckXsrf() {
return
}
user := ctx.Form("user") user := ctx.Form("user")
password := ctx.Form("password") password := ctx.Form("password")
repeat := ctx.Form("repeat") repeat := ctx.Form("repeat")
ctx.Data.Data = user
if !ctx.CheckXsrf() {
return
}
if user == "" || password == "" { if user == "" || password == "" {
ctx.Error("empty user or password") ctx.Error("empty user or password")
@ -98,8 +160,156 @@ func registerHandler(ctx *Context) {
} }
} }
func fileHandler(ctx *Context) {
defer ctx.Log()
f, fi, err := ctx.Srv.FS.GetFile(ctx.Path())
if err != nil {
ctx.Status(http.StatusNotFound)
err = ctx.Write([]byte(noSuchFile))
if err != nil {
log.Println("file:", err)
}
return
}
http.ServeContent(ctx.Response, ctx.Request, fi.Name(), fi.ModTime(), f)
}
func indexHandler(ctx *Context) { func indexHandler(ctx *Context) {
ctx.Redirect(core.IndexURI, http.StatusFound) path := strings.TrimPrefix(ctx.Path(), core.IndexPrefix)
switch ctx.Method() {
case "GET":
go func() {
file, txt, err := ctx.Srv.Tesseract.Scan(path)
if err != nil {
log.Printf("scan: %s: %s", path, err)
return
}
id, err := ctx.Srv.DB.Index.Add(txt, "pdf")
if err != nil {
log.Printf("index: %s: %s", path, err)
return
}
err = ctx.Srv.DB.NewFile(id, file)
if err != nil {
log.Printf("newFile: %s: %s", path, err)
}
}()
p, _ := filepath.Split(path)
ctx.Redirect(p, http.StatusFound)
}
}
func moveHandler(ctx *Context) {
path := strings.TrimPrefix(ctx.Path(), core.MovePrefix)
ctx.Data = webData{
Title: "Move File",
BodyTitle: "Move File",
}
ctx.Template("moveHandler")
switch ctx.Method() {
case "GET":
ctx.Data.Path = path
ctx.Data.Paths = ctx.Srv.FS.Recursive()
_, ctx.Data.Data = filepath.Split(path)
ctx.Exec()
case "POST":
name := ctx.Form("name")
dest := ctx.Form("destination")
newfile, err := ctx.Srv.FS.Move(path, dest, name)
if err != nil {
ctx.Error(err)
return
}
err = ctx.Srv.DB.MoveFile(path, newfile)
if err != nil {
ctx.Error(err)
return
}
ctx.Redirect(newfile, http.StatusFound)
}
}
func deleteHandler(ctx *Context) {
path := strings.TrimPrefix(ctx.Path(), core.DeletePrefix)
ctx.Template("deleteHandler")
switch ctx.Method() {
case "GET":
mode, err := ctx.Srv.FS.Mode(path)
if err != nil {
ctx.Error(err)
return
}
switch mode {
case fs.IsFile:
ctx.Data = webData{
Title: "Delete File",
BodyTitle: "Delete File",
}
case fs.IsDir:
ctx.Data = webData{
Title: "Delete Directory",
BodyTitle: "Delete Directory",
}
default:
ctx.Data = webData{
Title: "Delete",
BodyTitle: "Delete",
}
ctx.Error(noSuchFile)
return
}
ctx.Data.Path = path
ctx.Data.Data = mode
ctx.Exec()
case "POST":
files, err := ctx.Srv.FS.Delete(path)
if err != nil {
ctx.Error(err)
return
}
for _, f := range files {
id, err := ctx.Srv.DB.DeleteFile(f)
if err != nil {
log.Printf("db: delete: %s %s", f, err)
}
err = ctx.Srv.DB.Index.Delete(id)
if err != nil {
log.Printf("index: delete: %s %s", f, err)
}
}
ctx.Redirect(core.IndexURI, http.StatusFound)
}
}
func dirHandler(ctx *Context) {
ctx.Template("dirHandler")
ctx.Data = webData{
Title: "Directory Viewer",
BodyTitle: "Directory Viewer",
}
path := ctx.Path()
switch ctx.Method() {
case "GET":
mode, err := ctx.Srv.FS.Mode(path)
if err != nil {
ctx.Error(err)
return
}
switch mode {
case fs.IsDir:
dir := ctx.Srv.FS.ReadDir(path)
ctx.Srv.DB.GetIndexed(dir.Files)
ctx.Data.Data = dir
ctx.Data.Paths = fs.Paths(path)
ctx.Exec()
case fs.IsFile:
ctx.Template("fileHandler")
ctx.Data.Data = core.RawPrefix + path
ctx.Data.Paths = fs.Paths(path)
ctx.Exec()
}
case "POST":
}
} }
func loginHandler(ctx *Context) { func loginHandler(ctx *Context) {
@ -201,492 +411,23 @@ func searchHandler(ctx *Context) {
if !ctx.CheckXsrf() { if !ctx.CheckXsrf() {
return return
} }
search := ctx.Form("search") ctx.Data.Search = ctx.Form("search")
if ctx.Data.Search == "" {
res, err := ctx.Srv.DB.Index.Search(search) ctx.Error("empty search request")
return
}
res, err := ctx.Srv.DB.Index.Search(ctx.Data.Search)
if err != nil { if err != nil {
ctx.Error(err) ctx.Error(err)
return return
} }
user := ctx.User() err = ctx.Srv.DB.GetPaths(res)
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 { if err != nil {
ctx.Error(err) ctx.Error(err)
return return
} }
log.Printf("create: section %s created by %s\n", section, ctx.User()) ctx.Data.Data = res
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() 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 = page.Title
ctx.Data.BodyTitle = page.Title
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.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("/users", http.StatusFound)
} }
} }
@ -696,7 +437,7 @@ func userEditHandler(ctx *Context) {
Title: "Edit User", Title: "Edit User",
BodyTitle: "Edit User", BodyTitle: "Edit User",
} }
u, err := ctx.Srv.DB.GetUserWithoutPassword(ctx.Var("user")) u, err := ctx.Srv.DB.GetUserWithoutPassword()
if err != nil { if err != nil {
ctx.Error(err) ctx.Error(err)
return return
@ -709,33 +450,18 @@ func userEditHandler(ctx *Context) {
if !ctx.CheckXsrf() { if !ctx.CheckXsrf() {
return return
} }
user := ctx.Var("user")
password := ctx.Form("password") password := ctx.Form("password")
repeat := ctx.Form("repeat") repeat := ctx.Form("repeat")
admin := ctx.Form("admin")
adm := false
if admin == "0" {
adm = true
}
if password != repeat { if password != repeat {
ctx.Error("passwords do not match") ctx.Error("passwords do not match")
return return
} }
if ctx.Admin() { if err := ctx.Srv.DB.UpdateUserPassword(password); err != nil {
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("/users", http.StatusFound)
return
}
if err := ctx.Srv.DB.UpdateUserPassword(user, password); err != nil {
ctx.Error(err) ctx.Error(err)
return return
} }
log.Printf("update: %s updated by %s\n", user, ctx.User()) log.Printf("update: %s updated\n", ctx.User())
ctx.Redirect(core.IndexURI, http.StatusFound) ctx.Redirect(core.IndexURI, http.StatusFound)
} }
} }
@ -747,7 +473,7 @@ func userTotpHandler(ctx *Context) {
BodyTitle: "TOTP", BodyTitle: "TOTP",
} }
req := otp.Request{Username: ctx.Var("user")} req := otp.Request{Username: ctx.Var("user")}
u, err := ctx.Srv.DB.GetUser(req.Username) u, err := ctx.Srv.DB.GetUser()
if err != nil { if err != nil {
ctx.Error(err) ctx.Error(err)
return return
@ -787,68 +513,23 @@ func userTotpHandler(ctx *Context) {
if u.Secret != "" { if u.Secret != "" {
secret = u.Secret secret = u.Secret
} }
valid := otp.Validate(pin, secret)
user := ctx.User() user := ctx.User()
if user != req.Username && ctx.Admin() {
valid = true
}
if !valid { if !otp.Validate(pin, secret) {
ctx.Error("validation failed") ctx.Error("validation failed")
return return
} }
if err := ctx.Srv.DB.UpdateUserSecret(req.Username, req.Secret); err != nil {
if err := ctx.Srv.DB.UpdateUserSecret(req.Secret); err != nil {
ctx.Error(err) ctx.Error(err)
return return
} }
if req.Secret == "" { if req.Secret == "" {
log.Printf("totp: disabled by %s for %s\n", user, req.Username) log.Printf("totp: disabled for %s\n", user)
} else { } else {
log.Printf("totp: enabled by %s for %s\n", user, req.Username) log.Printf("totp: enabled for %s\n", user)
} }
ctx.Redirect("/user/edit/"+req.Username, http.StatusFound) ctx.Redirect("/user/edit", 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("/users", 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("/logout", http.StatusFound)
return
}
ctx.Redirect("/users", http.StatusFound)
} }
} }

View file

@ -2,6 +2,8 @@
package server package server
import "git.giftfish.de/ston1th/docstore/pkg/core"
type route struct { type route struct {
Path string Path string
Handler ctxHandler Handler ctxHandler
@ -10,90 +12,106 @@ type route struct {
var static = []route{ var static = []route{
{ {
"/favicon.ico", core.Favicon,
staticHandler, staticHandler,
[]string{"GET"}, []string{"GET"},
}, },
{ {
"/bootstrap.css", core.BootstrapCSS,
staticHandler, staticHandler,
[]string{"GET"}, []string{"GET"},
}, },
{ {
"/custom.css", core.CustomCSS,
staticHandler, staticHandler,
[]string{"GET"}, []string{"GET"},
}, },
} }
var register = append(static, route{ var registerRoutes = append(static, route{
"/", core.IndexURI,
registerHandler, jwtRegisterHandler(
registerHandler),
[]string{"GET", "POST"}, []string{"GET", "POST"},
}) })
var prefixRoutes = []route{
{
core.RawPrefix,
jwtHandler(
authHandler(
fileHandler)),
[]string{"GET"},
},
{
core.IndexPrefix,
jwtHandler(
authHandler(
indexHandler)),
[]string{"GET", "POST"},
},
{
core.MovePrefix,
jwtHandler(
authHandler(
moveHandler)),
[]string{"GET", "POST"},
},
{
core.DeletePrefix,
jwtHandler(
authHandler(
deleteHandler)),
[]string{"GET", "POST"},
},
{
core.IndexURI,
jwtHandler(
authHandler(
dirHandler)),
[]string{"GET", "POST", "PUT", "DELETE"},
},
}
var routes = append(static, []route{ var routes = append(static, []route{
{
"/",
authHandler(
dirHandler),
[]string{"GET"},
},
{
"/sections",
sectionsHandler,
[]string{"GET"},
},
{ {
"/search", "/search",
authHandler( jwtHandler(
searchHandler), authHandler(
searchHandler)),
[]string{"GET", "POST"}, []string{"GET", "POST"},
}, },
{ {
"/upload", core.LoginURI,
authHandler( jwtHandler(
searchHandler), loginHandler),
[]string{"GET", "POST"}, []string{"GET", "POST"},
}, },
{ {
"/login", core.TotpURI,
loginHandler, jwtHandler(
totpAuthHandler(
loginTotpHandler)),
[]string{"GET", "POST"}, []string{"GET", "POST"},
}, },
{ {
"/totp", "/user/edit",
totpAuthHandler( jwtHandler(
loginTotpHandler), authHandler(
userEditHandler)),
[]string{"GET", "POST"}, []string{"GET", "POST"},
}, },
{ {
"/user/edit/{user:[a-zA-Z0-9]+$}", "/user/totp",
authHandler( jwtHandler(
userEditHandler), authHandler(
userTotpHandler)),
[]string{"GET", "POST"}, []string{"GET", "POST"},
}, },
{ {
"/user/totp/{user:[a-zA-Z0-9]+$}", core.LogoutURI,
authHandler( jwtHandler(
userTotpHandler), logoutHandler),
[]string{"GET", "POST"}, []string{"GET", "POST"},
}, },
{
"/logout",
logoutHandler,
[]string{"GET", "POST"},
},
{
"/{dir}",
authHandler(
dirHandler),
[]string{"GET"},
},
{
"/{dir}/{file}",
authHandler(
fileHandler),
[]string{"GET"},
},
}...) }...)

View file

@ -8,7 +8,9 @@ import (
"errors" "errors"
"git.giftfish.de/ston1th/docstore/pkg/core" "git.giftfish.de/ston1th/docstore/pkg/core"
"git.giftfish.de/ston1th/docstore/pkg/db" "git.giftfish.de/ston1th/docstore/pkg/db"
"git.giftfish.de/ston1th/docstore/pkg/fs"
"git.giftfish.de/ston1th/docstore/pkg/log" "git.giftfish.de/ston1th/docstore/pkg/log"
"git.giftfish.de/ston1th/docstore/pkg/tesseract"
"git.giftfish.de/ston1th/godrop/v2" "git.giftfish.de/ston1th/godrop/v2"
"git.giftfish.de/ston1th/jwt/v3" "git.giftfish.de/ston1th/jwt/v3"
"github.com/gorilla/mux" "github.com/gorilla/mux"
@ -19,7 +21,7 @@ import (
) )
const ( const (
cookieName = "gosession" cookieName = "docstore_session"
) )
type HTTPServer struct { type HTTPServer struct {
@ -29,8 +31,10 @@ type HTTPServer struct {
srv *http.Server srv *http.Server
handler http.Handler handler http.Handler
DB *db.DB DB *db.DB
JWT *jwt.JWT JWT *jwt.JWT
Tesseract *tesseract.Tesseract
FS *fs.Filesystem
templ map[string]*template.Template templ map[string]*template.Template
res map[string][]byte res map[string][]byte
@ -40,14 +44,22 @@ func NewHTTPServer(cfg core.Config, l net.Listener) (srv *HTTPServer) {
srv = &HTTPServer{ srv = &HTTPServer{
Config: cfg, Config: cfg,
listener: l, listener: l,
handler: srv.buildRoutes(),
} }
srv.handler = srv.buildRoutes()
srv.loadTemplates() srv.loadTemplates()
srv.srv = &http.Server{} srv.srv = &http.Server{}
return return
} }
func (s *HTTPServer) Start() (err error) { func (s *HTTPServer) Start() (err error) {
s.FS, err = fs.NewFilesystem(s.Config.DataDir)
if err != nil {
return errors.New("fs: " + err.Error())
}
s.Tesseract, err = tesseract.NewTesseract(s.Config.DataDir, nil, time.Second*30)
if err != nil {
return errors.New("tesseract: " + err.Error())
}
s.DB, err = db.New(s.Config) s.DB, err = db.New(s.Config)
if err != nil { if err != nil {
return errors.New("db: " + err.Error()) return errors.New("db: " + err.Error())
@ -65,6 +77,7 @@ func (s *HTTPServer) Start() (err error) {
return errors.New("server: " + err.Error()) return errors.New("server: " + err.Error())
} }
if !s.DB.UserExists() { if !s.DB.UserExists() {
log.Debug("server: starting register handler")
s.srv.Handler = s.register() s.srv.Handler = s.register()
} else { } else {
s.srv.Handler = s.handler s.srv.Handler = s.handler
@ -84,29 +97,28 @@ func (s *HTTPServer) Stop() error {
} }
func (s *HTTPServer) register() http.Handler { func (s *HTTPServer) register() http.Handler {
m := mux.NewRouter() r := mux.NewRouter()
m.NotFoundHandler = &registerNotFoundHandler{s} r.NotFoundHandler = &registerNotFoundHandler{s}
for _, v := range register { for _, v := range registerRoutes {
m.HandleFunc(v.Path, s.contextWrapper(v.Handler)).Methods(v.Methods...) r.HandleFunc(v.Path, s.contextWrapper(v.Handler)).Methods(v.Methods...)
} }
return m return r
} }
func (s *HTTPServer) buildRoutes() http.Handler { func (s *HTTPServer) buildRoutes() http.Handler {
m := mux.NewRouter() r := mux.NewRouter()
m.NotFoundHandler = &notFoundHandler{s} r.NotFoundHandler = &notFoundHandler{s}
for _, v := range routes { for _, v := range routes {
m.HandleFunc(v.Path, s.contextWrapper(v.Handler)).Methods(v.Methods...) r.HandleFunc(v.Path, s.contextWrapper(v.Handler)).Methods(v.Methods...)
} }
return m for _, v := range prefixRoutes {
r.PathPrefix(v.Path).HandlerFunc(s.contextWrapper(v.Handler)).Methods(v.Methods...)
}
return r
} }
func (s *HTTPServer) contextWrapper(h ctxHandler) http.HandlerFunc { func (s *HTTPServer) contextWrapper(h ctxHandler) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) { return func(w http.ResponseWriter, r *http.Request) {
ctx := newContext(w, r, s) h(newContext(w, r, s))
if ctx == nil {
return
}
h(ctx)
} }
} }

View file

@ -13,6 +13,94 @@ import (
) )
const ( const (
delete = `{{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">
<p>You are about to delete
{{if eq .Data 1}}the directory{{end}}
{{if eq .Data 2}}the file{{end}}
<a href="{{.Path}}">{{.Path}}</a>.
{{if eq .Data 1}}<br>This will also remove all files and directories below.</p>{{end}}
<form class="form-horizontal" action="/delete{{.Path}}" method="post">
<input type="hidden" name="token" value="{{.Token}}">
<button class="btn btn-sm btn-primary" type="submit">Delete</button>
</form>
</div>
</div>
</div>
</div>
{{end}}`
dir = `{{define "body"}}
<div class="page-header">
<div class="row">
<h2>{{.BodyTitle}}</h2>
</div>
</div>
<div class="row">
<a href="/">root</a>&nbsp;/&nbsp;
{{range $item := .Paths}}
<a href="{{$item.Abs}}">{{$item.Name}}</a>&nbsp;/&nbsp;
{{end}}
</div>
<div class="row">
<table class="table table-stripped table-hover">
<thead>
<tr>
<th></th>
<th>Path</th>
<th>Options</th>
</tr>
</thead>
<tbody>
{{if .Data.Dirs}}
{{range $item := .Data.Dirs}}
<tr>
<td><input type="checkbox" name="cb" value="{{$item.Name}}"></td>
<td><a href="{{$item.Abs}}">{{$item.Name}}/</a></td>
<td>Move</td>
</tr>
{{end}}
{{end}}
{{if .Data.Files}}
{{range $item := .Data.Files}}
<tr>
<td><input type="checkbox" name="cb" value="{{$item.Name}}"></td>
<td><a href="{{$item.Abs}}">{{$item.Name}}</a></td>
<td><div class="btn-group">
{{if not $item.Indexed}}
<a href="/index{{$item.Abs}}" class="btn btn-sm btn-primary">Index</a>
{{end}}
<a href="/move{{$item.Abs}}" class="btn btn-sm btn-primary">Move</a>
<a href="/delete{{$item.Abs}}" class="btn btn-sm btn-primary">Delete</a>
</div></td>
</tr>
{{end}}
{{end}}
</tbody>
</table>
</div>
{{end}}`
file = `{{define "body"}}
<div class="page-header">
<div class="row">
<h2>{{.BodyTitle}}</h2>
</div>
</div>
<div class="row">
<a href="/">root</a>
{{range $i, $item := .Paths}}
&nbsp;/&nbsp;<a href="{{$item.Abs}}">{{$item.Name}}</a>
{{end}}
</div>
<div class="row">
<embed src="{{.Data}}" width="100%" height="700px" toolbar="1" statusbar="1" navpanes="1"></embed>
</div>
{{end}}`
forbidden = `{{define "body"}} forbidden = `{{define "body"}}
<div class="page-header"> <div class="page-header">
<div class="row"> <div class="row">
@ -33,7 +121,7 @@ const (
index = `<!DOCTYPE html> index = `<!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<title>GoWiki | {{.Title}}</title> <title>DocStore | {{.Title}}</title>
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" integrity="sha256-bgotk/IPq4Yvt6s8AQGPTM5ZjPjQ6rrBRa2EyxpnFSc="></link> <link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" integrity="sha256-bgotk/IPq4Yvt6s8AQGPTM5ZjPjQ6rrBRa2EyxpnFSc="></link>
<link rel="stylesheet" type="text/css" href="/bootstrap.css" media="screen" integrity="sha256-NeLmQ7cX66J4MdtgGlG3O/TgvsSyP1b9WbaQtxYZUbQ="></link> <link rel="stylesheet" type="text/css" href="/bootstrap.css" media="screen" integrity="sha256-NeLmQ7cX66J4MdtgGlG3O/TgvsSyP1b9WbaQtxYZUbQ="></link>
<link rel="stylesheet" type="text/css" href="/custom.css" media="screen" integrity="sha256-5/Vbh+c9BC042HeSbJXZZVIc1dD2b9cy6uH9LFAOSfo="></link> <link rel="stylesheet" type="text/css" href="/custom.css" media="screen" integrity="sha256-5/Vbh+c9BC042HeSbJXZZVIc1dD2b9cy6uH9LFAOSfo="></link>
@ -43,7 +131,7 @@ const (
<body id="top"> <body id="top">
<div class="navbar navbar-expand fixed-top navbar-dark bg-primary"> <div class="navbar navbar-expand fixed-top navbar-dark bg-primary">
<div class="container"> <div class="container">
<a href="/" class="navbar-brand">GoWiki</a> <a href="/" class="navbar-brand">DocStore</a>
<div class="collapse navbar-collapse"> <div class="collapse navbar-collapse">
{{template "menu" .}} {{template "menu" .}}
</div> </div>
@ -65,11 +153,8 @@ const (
<div class="col"> <div class="col">
<ul class="list-unstyled"> <ul class="list-unstyled">
<li class="float-md-right"><a href="#top">Back to top</a></li> <li class="float-md-right"><a href="#top">Back to top</a></li>
{{if .Login}}
<li><a href="/blacklist">Blacklist</a></li>
{{end}}
</ul> </ul>
<p>© GoWiki {{.Version}} Request: <strong>{{.Time}}ms</strong></p> <p>© DocStore {{.Version}} Request: <strong>{{.Time}}ms</strong></p>
</div> </div>
</div> </div>
</footer> </footer>
@ -136,11 +221,7 @@ const (
{{end}}` {{end}}`
menu = `{{define "menu"}} menu = `{{define "menu"}}
<ul class="navbar-nav"> <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="/">Root</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> </ul>
<form class="form-inline" method="post" action="/search"> <form class="form-inline" method="post" action="/search">
<input type="hidden" name="token" value="{{.Token}}"> <input type="hidden" name="token" value="{{.Token}}">
@ -149,16 +230,41 @@ const (
</form> </form>
<ul class="nav navbar-nav ml-auto"> <ul class="nav navbar-nav ml-auto">
{{if .Login}} {{if .Login}}
{{if .Admin}} <li class="nav-item"><a class="nav-link" href="/user/edit">Profile</a></li>
<li class="nav-item"><a class="nav-link" href="/users">Users</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> <li class="nav-item"><a class="nav-link" href="/logout">Logout</a></li>
{{else}} {{else}}
<li class="nav-item"><a class="nav-link" href="/login">Login</a></li> <li class="nav-item"><a class="nav-link" href="/login">Login</a></li>
{{end}} {{end}}
</ul> </ul>
{{end}}`
move = `{{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="/move{{.Path}}" method="post">
<input type="hidden" name="token" value="{{.Token}}">
<div class="form-group">
<label class="col-form-label" for="destination">Destination</label>
<select class="form-control input-sm" id="destination" name="destination" required>
{{range $item := .Paths}}
<option value="{{$item.Abs}}">{{$item.Abs}}</option>
{{end}}
</select>
</div>
<div class="form-group">
<label class="col-form-label" for="name">Name</label>
<input class="form-control input-sm" type="text" id="name" name="name" autocomplete="off" value="{{.Data}}">
</div>
<button class="btn btn-sm btn-primary" type="submit">Move</button>
</form>
</div>
</div>
</div>
</div>
{{end}}` {{end}}`
notFound = `{{define "body"}} notFound = `{{define "body"}}
<div class="page-header"> <div class="page-header">
@ -177,249 +283,34 @@ const (
</div> </div>
</div> </div>
{{end}}` {{end}}`
pageBlacklist = `{{define "body"}} register = `{{define "body"}}
<div class="page-header"> <div class="row">
<div class="row"> <div class="col-xs-4 offset-4">
<div class="col-xs-4 offset-4"> <div class="card border-primary mx-auto">
<div class="card border-danger mx-auto"> <div class="card-header">
<div class="card-header"> <strong>{{.BodyTitle}}</strong>
<strong>{{.BodyTitle}}</strong> </div>
<div class="card-body">
<form class="form-horizontal" action="/" 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" value="{{.Data}}" autofocus required>
</div> </div>
<div class="card-body"> <div class="form-group">
<form class="form-horizontal" action="/blacklist" method="post"> <label class="col-form-label" for="password">Password</label>
<input type="hidden" name="token" value="{{.Token}}"> <input class="form-control input-sm" type="password" id="password" name="password" required>
<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 class="form-group">
</div> <label class="col-form-label" for="repeat">Repeat</label>
</div> <input class="form-control input-sm" type="password" id="repeat" name="repeat" required>
</div>
{{end}}`
pageDel = `{{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>
<div class="card-body"> <button class="btn btn-sm btn-primary" type="submit">Create</button>
<form class="form-horizontal" action="/{{.Data}}/del" method="post"> </form>
<input type="hidden" name="token" value="{{.Token}}"> </div>
<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> </div>
</div> </div>
{{end}}`
pageEdit = `{{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}}`
page = `{{define "body"}}
<div class="page-header">
<div class="row">
<h2>{{.BodyTitle}}</h2>
</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>
<div class="row mt-5">
<div class="col">
{{.Data.TOC}}
</div>
<div class="col">
<div class="btn-group float-md-right">
<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 3}}
<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>
{{.Data.HTML}}
{{end}}`
pageMD = `{{define "body"}}
<div class="page-header">
<div class="row">
<a href="{{if .Key}}/view/{{.Key}}{{else}}/{{.Data.StoreTitle}}{{end}}"><h2>{{.BodyTitle}}</h2></a>
</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}}`
pageNew = `{{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}}`
pageShare = `{{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}}`
pageView = `{{define "body"}}
<div class="page-header">
<div class="row">
<h2>{{.BodyTitle}}</h2>
</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>
<div class="row mt-5">
<div class="col">
{{.Data.TOC}}
</div>
<div class="col">
<div class="btn-group float-md-right">
<a href="/view/{{.Key}}/md" class="btn btn-sm btn-primary">Markdown</a>
</div>
</div>
</div>
{{.Data.HTML}}
{{end}}` {{end}}`
search = `{{define "body"}} search = `{{define "body"}}
<div class="page-header"> <div class="page-header">
@ -444,205 +335,32 @@ const (
</div> </div>
<div class="row mt-3"> <div class="row mt-3">
<div class="col"> <div class="col">
{{if .Data}}
{{range $item := .Data}} {{range $item := .Data}}
<a href="/{{$item.StoreTitle}}"><b>{{$item.Title}}</b></a> <a href="{{$item.Path}}"><b>{{$item.Path}}</b></a>
<pre class="wrap">{{$item.HTML}}</pre><br> <pre class="wrap">{{$item.HTML}}</pre><br>
{{end}} {{end}}
</div> {{else}}
</div> <b>No search results found.</b>
{{end}}`
sectionDel = `{{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}}`
sectionEdit = `{{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}}`
section = `{{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}} {{end}}
</div> </div>
</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}}`
sectionNew = `{{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}}`
sections = `{{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">
{{range $page := $item.Pages}}
<strong><a href="/{{$page.StoreTitle}}">{{$page.Title}}</a></strong><br>
{{end}}
</div>
</div>
</div>
<div class="offset-1"></div>
{{end}}
</div>
{{end}}
{{end}}`
userDel = `{{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}}` {{end}}`
userEdit = `{{define "body"}} userEdit = `{{define "body"}}
{{if .Data}}
<div class="row"> <div class="row">
<div class="col-xs-4 offset-4"> <div class="col-xs-4 offset-4">
<div class="card border-primary mx-auto"> <div class="card border-primary mx-auto">
<div class="card-header"> <div class="card-header">
<strong>{{.BodyTitle}}</strong> <strong>{{.BodyTitle}}</strong>
{{if .Data.Secret}} {{if .Data.Secret}}
<a href="/user/totp/{{.Data.Username}}" class="btn btn-sm btn-danger float-right">Disable TOTP</a> <a href="/user/totp" class="btn btn-sm btn-danger float-right">Disable TOTP</a>
{{else}} {{else}}
<a href="/user/totp/{{.Data.Username}}" class="btn btn-sm btn-success float-right">Enable TOTP</a> <a href="/user/totp" class="btn btn-sm btn-success float-right">Enable TOTP</a>
{{end}} {{end}}
</div> </div>
<div class="card-body"> <div class="card-body">
<form class="form-horizontal" action="/user/edit/{{.Data.Username}}" method="post"> <form class="form-horizontal" action="/user/edit" method="post">
<input type="hidden" name="token" value="{{.Token}}"> <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"> <div class="form-group">
<label class="col-form-label" for="password">Password</label> <label class="col-form-label" for="password">Password</label>
<input class="form-control input-sm" type="password" id="password" name="password" autofocus> <input class="form-control input-sm" type="password" id="password" name="password" autofocus>
@ -651,42 +369,15 @@ const (
<label class="col-form-label" for="repeat">Repeat</label> <label class="col-form-label" for="repeat">Repeat</label>
<input class="form-control input-sm" type="password" id="repeat" name="repeat"> <input class="form-control input-sm" type="password" id="repeat" name="repeat">
</div> </div>
{{if .Admin}}
<div class="form-group">
<label class="col-form-label" for="admin">Admin Privileges</label>
{{if .Data.Admin}}
<input type="checkbox" id="admin" name="admin" value="0" checked>
{{else}}
<input type="checkbox" id="admin" name="admin" value="0">
{{end}}
</div>
{{end}}
<button class="btn btn-sm btn-primary" type="submit">Update</button> <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> <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> </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>
</div> </div>
</div> </div>
{{end}}
{{end}}` {{end}}`
userNew = `{{define "body"}} userTotp = `{{define "body"}}
<div class="row"> <div class="row">
<div class="col-xs-4 offset-4"> <div class="col-xs-4 offset-4">
<div class="card border-primary mx-auto"> <div class="card border-primary mx-auto">
@ -694,97 +385,7 @@ const (
<strong>{{.BodyTitle}}</strong> <strong>{{.BodyTitle}}</strong>
</div> </div>
<div class="card-body"> <div class="card-body">
<form class="form-horizontal" action="/user/new" method="post"> <form class="form-horizontal" action="/user/totp" 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">
<label class="col-form-label" for="admin">Admin Privileges</label>
<input type="checkbox" id="admin" name="admin" value="0">
</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}}`
users = `{{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}}`
userTotp = `{{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}}"> <input type="hidden" name="token" value="{{.Token}}">
{{if .Data.Secret}} {{if .Data.Secret}}
<input type="hidden" name="url" value="{{.Data.URL}}"> <input type="hidden" name="url" value="{{.Data.URL}}">
@ -805,13 +406,12 @@ const (
{{else}} {{else}}
<button class="btn btn-sm btn-danger" type="submit">Disable</button> <button class="btn btn-sm btn-danger" type="submit">Disable</button>
{{end}} {{end}}
<a href="/user/edit/{{.Data.Username}}" class="btn btn-sm btn-primary">Back</a> <a href="/user/edit" class="btn btn-sm btn-primary">Back</a>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{{end}}
{{end}}` {{end}}`
bootstrapCSS = `/*! bootstrapCSS = `/*!
* Bootswatch v4.1.3 * Bootswatch v4.1.3
@ -1005,31 +605,19 @@ func (s *HTTPServer) loadTemplates() {
} }
s.res["favicon.ico"] = fav s.res["favicon.ico"] = fav
// pages // register
s.templ["registerHandler"] = parse(index, menu, register)
s.templ["dirHandler"] = parse(index, menu, dir)
s.templ["fileHandler"] = parse(index, menu, file)
s.templ["moveHandler"] = parse(index, menu, move)
s.templ["deleteHandler"] = parse(index, menu, delete)
s.templ["notFoundHandler"] = parse(index, menu, notFound) s.templ["notFoundHandler"] = parse(index, menu, notFound)
s.templ["forbiddenHandler"] = parse(index, menu, forbidden) s.templ["forbiddenHandler"] = parse(index, menu, forbidden)
s.templ["loginHandler"] = parse(index, menu, login) s.templ["loginHandler"] = parse(index, menu, login)
s.templ["loginTotpHandler"] = parse(index, menu, loginTotp) s.templ["loginTotpHandler"] = parse(index, menu, loginTotp)
s.templ["searchHandler"] = parse(index, menu, search) s.templ["searchHandler"] = parse(index, menu, search)
s.templ["pageHandler"] = parse(index, menu, page)
s.templ["pageNewHandler"] = parse(index, menu, pageNew)
s.templ["pageEditHandler"] = parse(index, menu, pageEdit)
s.templ["pageViewHandler"] = parse(index, menu, pageView)
s.templ["pageMDHandler"] = parse(index, menu, pageMD)
s.templ["pageShareHandler"] = parse(index, menu, pageShare)
s.templ["pageBlacklistHandler"] = parse(index, menu, pageBlacklist)
s.templ["pageDelHandler"] = parse(index, menu, pageDel)
// sections
s.templ["sectionHandler"] = parse(index, menu, section)
s.templ["sectionsHandler"] = parse(index, menu, sections)
s.templ["sectionNewHandler"] = parse(index, menu, sectionNew)
s.templ["sectionEditHandler"] = parse(index, menu, sectionEdit)
s.templ["sectionDelHandler"] = parse(index, menu, sectionDel)
// users
s.templ["usersHandler"] = parse(index, menu, users)
s.templ["userNewHandler"] = parse(index, menu, userNew)
s.templ["userEditHandler"] = parse(index, menu, userEdit) s.templ["userEditHandler"] = parse(index, menu, userEdit)
s.templ["userDelHandler"] = parse(index, menu, userDel)
// totp // totp
s.templ["userTotpHandler"] = parse(index, menu, userTotp) s.templ["userTotpHandler"] = parse(index, menu, userTotp)

View file

@ -8,14 +8,16 @@ import (
"time" "time"
) )
const DefaultLang = "eng+deu"
type Tesseract struct { type Tesseract struct {
cmd string cmd string
args []string args []string
dir string base string
timeout time.Duration timeout time.Duration
} }
func NewTesseract(dir string, langs []string, timeout time.Duration) (t *Tesseract, err error) { func NewTesseract(base string, langs []string, timeout time.Duration) (t *Tesseract, err error) {
var ( var (
cmd string cmd string
lang string lang string
@ -30,23 +32,28 @@ func NewTesseract(dir string, langs []string, timeout time.Duration) (t *Tessera
lang += "+" lang += "+"
} }
} }
if lang == "" {
lang = DefaultLang
}
args := []string{"-l", lang, "--oem", "2"} args := []string{"-l", lang, "--oem", "2"}
t = &Tesseract{cmd, args, dir, timeout} t = &Tesseract{cmd, args, base, timeout}
return return
} }
func (t *Tesseract) Scan(file string) (filename, text string, err error) { func (t *Tesseract) Scan(file string) (filename, text string, err error) {
filename = core.FileName() + ".pdf" scanfile := filepath.Join(t.base, file)
txt, err := core.Timeout(exec.Command(t.cmd, append(t.args, []string{file, "stdout", "txt"}...)...), t.timeout) p, _ := filepath.Split(file)
filename = filepath.Join(p, core.FileName()+".pdf")
txt, err := core.Timeout(exec.Command(t.cmd, append(t.args, []string{scanfile, "stdout", "txt"}...)...), t.timeout)
if err != nil { if err != nil {
return return
} }
text = string(txt) text = string(txt)
pdf, err := core.Timeout(exec.Command(t.cmd, append(t.args, []string{file, "stdout", "pdf"}...)...), t.timeout) pdf, err := core.Timeout(exec.Command(t.cmd, append(t.args, []string{scanfile, "stdout", "pdf"}...)...), t.timeout)
if err != nil { if err != nil {
return return
} }
f, err := os.OpenFile(filepath.Join(t.dir, filename), os.O_RDWR|os.O_CREATE, 0640) f, err := os.OpenFile(filepath.Join(t.base, filename), os.O_RDWR|os.O_CREATE, 0640)
if err != nil { if err != nil {
return return
} }
@ -55,6 +62,6 @@ func (t *Tesseract) Scan(file string) (filename, text string, err error) {
if err != nil { if err != nil {
return return
} }
err = os.Remove(file) err = os.Remove(scanfile)
return return
} }

View file

@ -77,31 +77,19 @@ func (s *HTTPServer) loadTemplates() {
} }
s.res["favicon.ico"] = fav s.res["favicon.ico"] = fav
// pages // register
s.templ["registerHandler"] = parse(index, menu, register)
s.templ["dirHandler"] = parse(index, menu, dir)
s.templ["fileHandler"] = parse(index, menu, file)
s.templ["moveHandler"] = parse(index, menu, move)
s.templ["deleteHandler"] = parse(index, menu, delete)
s.templ["notFoundHandler"] = parse(index, menu, notFound) s.templ["notFoundHandler"] = parse(index, menu, notFound)
s.templ["forbiddenHandler"] = parse(index, menu, forbidden) s.templ["forbiddenHandler"] = parse(index, menu, forbidden)
s.templ["loginHandler"] = parse(index, menu, login) s.templ["loginHandler"] = parse(index, menu, login)
s.templ["loginTotpHandler"] = parse(index, menu, loginTotp) s.templ["loginTotpHandler"] = parse(index, menu, loginTotp)
s.templ["searchHandler"] = parse(index, menu, search) s.templ["searchHandler"] = parse(index, menu, search)
s.templ["pageHandler"] = parse(index, menu, page)
s.templ["pageNewHandler"] = parse(index, menu, pageNew)
s.templ["pageEditHandler"] = parse(index, menu, pageEdit)
s.templ["pageViewHandler"] = parse(index, menu, pageView)
s.templ["pageMDHandler"] = parse(index, menu, pageMD)
s.templ["pageShareHandler"] = parse(index, menu, pageShare)
s.templ["pageBlacklistHandler"] = parse(index, menu, pageBlacklist)
s.templ["pageDelHandler"] = parse(index, menu, pageDel)
// sections
s.templ["sectionHandler"] = parse(index, menu, section)
s.templ["sectionsHandler"] = parse(index, menu, sections)
s.templ["sectionNewHandler"] = parse(index, menu, sectionNew)
s.templ["sectionEditHandler"] = parse(index, menu, sectionEdit)
s.templ["sectionDelHandler"] = parse(index, menu, sectionDel)
// users
s.templ["usersHandler"] = parse(index, menu, users)
s.templ["userNewHandler"] = parse(index, menu, userNew)
s.templ["userEditHandler"] = parse(index, menu, userEdit) s.templ["userEditHandler"] = parse(index, menu, userEdit)
s.templ["userDelHandler"] = parse(index, menu, userDel)
// totp // totp
s.templ["userTotpHandler"] = parse(index, menu, userTotp) s.templ["userTotpHandler"] = parse(index, menu, userTotp)

22
templates/delete.html Normal file
View file

@ -0,0 +1,22 @@
{{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">
<p>You are about to delete
{{if eq .Data 1}}the directory{{end}}
{{if eq .Data 2}}the file{{end}}
<a href="{{.Path}}">{{.Path}}</a>.
{{if eq .Data 1}}<br>This will also remove all files and directories below.</p>{{end}}
<form class="form-horizontal" action="/delete{{.Path}}" method="post">
<input type="hidden" name="token" value="{{.Token}}">
<button class="btn btn-sm btn-primary" type="submit">Delete</button>
</form>
</div>
</div>
</div>
</div>
{{end}}

50
templates/dir.html Normal file
View file

@ -0,0 +1,50 @@
{{define "body"}}
<div class="page-header">
<div class="row">
<h2>{{.BodyTitle}}</h2>
</div>
</div>
<div class="row">
<a href="/">root</a>&nbsp;/&nbsp;
{{range $item := .Paths}}
<a href="{{$item.Abs}}">{{$item.Name}}</a>&nbsp;/&nbsp;
{{end}}
</div>
<div class="row">
<table class="table table-stripped table-hover">
<thead>
<tr>
<th></th>
<th>Path</th>
<th>Options</th>
</tr>
</thead>
<tbody>
{{if .Data.Dirs}}
{{range $item := .Data.Dirs}}
<tr>
<td><input type="checkbox" name="cb" value="{{$item.Name}}"></td>
<td><a href="{{$item.Abs}}">{{$item.Name}}/</a></td>
<td>Move</td>
</tr>
{{end}}
{{end}}
{{if .Data.Files}}
{{range $item := .Data.Files}}
<tr>
<td><input type="checkbox" name="cb" value="{{$item.Name}}"></td>
<td><a href="{{$item.Abs}}">{{$item.Name}}</a></td>
<td><div class="btn-group">
{{if not $item.Indexed}}
<a href="/index{{$item.Abs}}" class="btn btn-sm btn-primary">Index</a>
{{end}}
<a href="/move{{$item.Abs}}" class="btn btn-sm btn-primary">Move</a>
<a href="/delete{{$item.Abs}}" class="btn btn-sm btn-primary">Delete</a>
</div></td>
</tr>
{{end}}
{{end}}
</tbody>
</table>
</div>
{{end}}

16
templates/file.html Normal file
View file

@ -0,0 +1,16 @@
{{define "body"}}
<div class="page-header">
<div class="row">
<h2>{{.BodyTitle}}</h2>
</div>
</div>
<div class="row">
<a href="/">root</a>
{{range $i, $item := .Paths}}
&nbsp;/&nbsp;<a href="{{$item.Abs}}">{{$item.Name}}</a>
{{end}}
</div>
<div class="row">
<embed src="{{.Data}}" width="100%" height="700px" toolbar="1" statusbar="1" navpanes="1"></embed>
</div>
{{end}}

View file

@ -1,7 +1,7 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
<head> <head>
<title>GoWiki | {{.Title}}</title> <title>DocStore | {{.Title}}</title>
<link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" integrity="sha256-bgotk/IPq4Yvt6s8AQGPTM5ZjPjQ6rrBRa2EyxpnFSc="></link> <link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" integrity="sha256-bgotk/IPq4Yvt6s8AQGPTM5ZjPjQ6rrBRa2EyxpnFSc="></link>
<link rel="stylesheet" type="text/css" href="/bootstrap.css" media="screen" integrity="sha256-NeLmQ7cX66J4MdtgGlG3O/TgvsSyP1b9WbaQtxYZUbQ="></link> <link rel="stylesheet" type="text/css" href="/bootstrap.css" media="screen" integrity="sha256-NeLmQ7cX66J4MdtgGlG3O/TgvsSyP1b9WbaQtxYZUbQ="></link>
<link rel="stylesheet" type="text/css" href="/custom.css" media="screen" integrity="sha256-5/Vbh+c9BC042HeSbJXZZVIc1dD2b9cy6uH9LFAOSfo="></link> <link rel="stylesheet" type="text/css" href="/custom.css" media="screen" integrity="sha256-5/Vbh+c9BC042HeSbJXZZVIc1dD2b9cy6uH9LFAOSfo="></link>
@ -11,7 +11,7 @@
<body id="top"> <body id="top">
<div class="navbar navbar-expand fixed-top navbar-dark bg-primary"> <div class="navbar navbar-expand fixed-top navbar-dark bg-primary">
<div class="container"> <div class="container">
<a href="/" class="navbar-brand">GoWiki</a> <a href="/" class="navbar-brand">DocStore</a>
<div class="collapse navbar-collapse"> <div class="collapse navbar-collapse">
{{template "menu" .}} {{template "menu" .}}
</div> </div>
@ -33,11 +33,8 @@
<div class="col"> <div class="col">
<ul class="list-unstyled"> <ul class="list-unstyled">
<li class="float-md-right"><a href="#top">Back to top</a></li> <li class="float-md-right"><a href="#top">Back to top</a></li>
{{if .Login}}
<li><a href="/blacklist">Blacklist</a></li>
{{end}}
</ul> </ul>
<p>© GoWiki {{.Version}} Request: <strong>{{.Time}}ms</strong></p> <p>© DocStore {{.Version}} Request: <strong>{{.Time}}ms</strong></p>
</div> </div>
</div> </div>
</footer> </footer>

View file

@ -1,10 +1,6 @@
{{define "menu"}} {{define "menu"}}
<ul class="navbar-nav"> <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="/">Root</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> </ul>
<form class="form-inline" method="post" action="/search"> <form class="form-inline" method="post" action="/search">
<input type="hidden" name="token" value="{{.Token}}"> <input type="hidden" name="token" value="{{.Token}}">
@ -13,11 +9,7 @@
</form> </form>
<ul class="nav navbar-nav ml-auto"> <ul class="nav navbar-nav ml-auto">
{{if .Login}} {{if .Login}}
{{if .Admin}} <li class="nav-item"><a class="nav-link" href="/user/edit">Profile</a></li>
<li class="nav-item"><a class="nav-link" href="/users">Users</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> <li class="nav-item"><a class="nav-link" href="/logout">Logout</a></li>
{{else}} {{else}}
<li class="nav-item"><a class="nav-link" href="/login">Login</a></li> <li class="nav-item"><a class="nav-link" href="/login">Login</a></li>

29
templates/move.html Normal file
View file

@ -0,0 +1,29 @@
{{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="/move{{.Path}}" method="post">
<input type="hidden" name="token" value="{{.Token}}">
<div class="form-group">
<label class="col-form-label" for="destination">Destination</label>
<select class="form-control input-sm" id="destination" name="destination" required>
{{range $item := .Paths}}
<option value="{{$item.Abs}}">{{$item.Abs}}</option>
{{end}}
</select>
</div>
<div class="form-group">
<label class="col-form-label" for="name">Name</label>
<input class="form-control input-sm" type="text" id="name" name="name" autocomplete="off" value="{{.Data}}">
</div>
<button class="btn btn-sm btn-primary" type="submit">Move</button>
</form>
</div>
</div>
</div>
</div>
{{end}}

View file

@ -1,43 +0,0 @@
{{define "body"}}
<div class="page-header">
<div class="row">
<h2>{{.BodyTitle}}</h2>
</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>
<div class="row mt-5">
<div class="col">
{{.Data.TOC}}
</div>
<div class="col">
<div class="btn-group float-md-right">
<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 3}}
<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>
{{.Data.HTML}}
{{end}}

View file

@ -1,23 +0,0 @@
{{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}}

View file

@ -1,20 +0,0 @@
{{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}}

View file

@ -1,30 +0,0 @@
{{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}}

View file

@ -1,10 +0,0 @@
{{define "body"}}
<div class="page-header">
<div class="row">
<a href="{{if .Key}}/view/{{.Key}}{{else}}/{{.Data.StoreTitle}}{{end}}"><h2>{{.BodyTitle}}</h2></a>
</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}}

View file

@ -1,41 +0,0 @@
{{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}}

View file

@ -1,43 +0,0 @@
{{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}}

View file

@ -1,34 +0,0 @@
{{define "body"}}
<div class="page-header">
<div class="row">
<h2>{{.BodyTitle}}</h2>
</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>
<div class="row mt-5">
<div class="col">
{{.Data.TOC}}
</div>
<div class="col">
<div class="btn-group float-md-right">
<a href="/view/{{.Key}}/md" class="btn btn-sm btn-primary">Markdown</a>
</div>
</div>
</div>
{{.Data.HTML}}
{{end}}

View file

@ -6,11 +6,11 @@
<strong>{{.BodyTitle}}</strong> <strong>{{.BodyTitle}}</strong>
</div> </div>
<div class="card-body"> <div class="card-body">
<form class="form-horizontal" action="/user/new" method="post"> <form class="form-horizontal" action="/" method="post">
<input type="hidden" name="token" value="{{.Token}}"> <input type="hidden" name="token" value="{{.Token}}">
<div class="form-group"> <div class="form-group">
<label class="col-form-label" for="user">Username</label> <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> <input class="form-control input-sm" type="text" id="user" name="user" autocomplete="off" value="{{.Data}}" autofocus required>
</div> </div>
<div class="form-group"> <div class="form-group">
<label class="col-form-label" for="password">Password</label> <label class="col-form-label" for="password">Password</label>
@ -20,12 +20,7 @@
<label class="col-form-label" for="repeat">Repeat</label> <label class="col-form-label" for="repeat">Repeat</label>
<input class="form-control input-sm" type="password" id="repeat" name="repeat" required> <input class="form-control input-sm" type="password" id="repeat" name="repeat" required>
</div> </div>
<div class="form-group">
<label class="col-form-label" for="admin">Admin Privileges</label>
<input type="checkbox" id="admin" name="admin" value="0">
</div>
<button class="btn btn-sm btn-primary" type="submit">Create</button> <button class="btn btn-sm btn-primary" type="submit">Create</button>
<a href="/users" class="btn btn-sm btn-primary">Back</a>
</form> </form>
</div> </div>
</div> </div>

View file

@ -21,10 +21,14 @@
</div> </div>
<div class="row mt-3"> <div class="row mt-3">
<div class="col"> <div class="col">
{{if .Data}}
{{range $item := .Data}} {{range $item := .Data}}
<a href="/{{$item.StoreTitle}}"><b>{{$item.Title}}</b></a> <a href="{{$item.Path}}"><b>{{$item.Path}}</b></a>
<pre class="wrap">{{$item.HTML}}</pre><br> <pre class="wrap">{{$item.HTML}}</pre><br>
{{end}} {{end}}
{{else}}
<b>No search results found.</b>
{{end}}
</div> </div>
</div> </div>
{{end}} {{end}}

View file

@ -1,25 +0,0 @@
{{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>
{{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}}

View file

@ -1,23 +0,0 @@
{{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}}

View file

@ -1,34 +0,0 @@
{{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}}

View file

@ -1,34 +0,0 @@
{{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}}

View file

@ -1,33 +0,0 @@
{{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">
{{range $page := $item.Pages}}
<strong><a href="/{{$page.StoreTitle}}">{{$page.Title}}</a></strong><br>
{{end}}
</div>
</div>
</div>
<div class="offset-1"></div>
{{end}}
</div>
{{end}}
{{end}}

View file

@ -1,23 +0,0 @@
{{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}}

View file

@ -1,23 +1,18 @@
{{define "body"}} {{define "body"}}
{{if .Data}}
<div class="row"> <div class="row">
<div class="col-xs-4 offset-4"> <div class="col-xs-4 offset-4">
<div class="card border-primary mx-auto"> <div class="card border-primary mx-auto">
<div class="card-header"> <div class="card-header">
<strong>{{.BodyTitle}}</strong> <strong>{{.BodyTitle}}</strong>
{{if .Data.Secret}} {{if .Data.Secret}}
<a href="/user/totp/{{.Data.Username}}" class="btn btn-sm btn-danger float-right">Disable TOTP</a> <a href="/user/totp" class="btn btn-sm btn-danger float-right">Disable TOTP</a>
{{else}} {{else}}
<a href="/user/totp/{{.Data.Username}}" class="btn btn-sm btn-success float-right">Enable TOTP</a> <a href="/user/totp" class="btn btn-sm btn-success float-right">Enable TOTP</a>
{{end}} {{end}}
</div> </div>
<div class="card-body"> <div class="card-body">
<form class="form-horizontal" action="/user/edit/{{.Data.Username}}" method="post"> <form class="form-horizontal" action="/user/edit" method="post">
<input type="hidden" name="token" value="{{.Token}}"> <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"> <div class="form-group">
<label class="col-form-label" for="password">Password</label> <label class="col-form-label" for="password">Password</label>
<input class="form-control input-sm" type="password" id="password" name="password" autofocus> <input class="form-control input-sm" type="password" id="password" name="password" autofocus>
@ -26,38 +21,11 @@
<label class="col-form-label" for="repeat">Repeat</label> <label class="col-form-label" for="repeat">Repeat</label>
<input class="form-control input-sm" type="password" id="repeat" name="repeat"> <input class="form-control input-sm" type="password" id="repeat" name="repeat">
</div> </div>
{{if .Admin}}
<div class="form-group">
<label class="col-form-label" for="admin">Admin Privileges</label>
{{if .Data.Admin}}
<input type="checkbox" id="admin" name="admin" value="0" checked>
{{else}}
<input type="checkbox" id="admin" name="admin" value="0">
{{end}}
</div>
{{end}}
<button class="btn btn-sm btn-primary" type="submit">Update</button> <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> <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> </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>
</div> </div>
</div> </div>
{{end}} {{end}}
{{end}}

View file

@ -1,13 +1,12 @@
{{define "body"}} {{define "body"}}
{{if .Data}}
<div class="row"> <div class="row">
<div class="col-xs-4 offset-4"> <div class="col-xs-4 offset-4">
<div class="card border-primary mx-auto"> <div class="card border-primary mx-auto">
<div class="card-header"> <div class="card-header">
<strong>{{.BodyTitle}} - {{.Data.Username}}</strong> <strong>{{.BodyTitle}}</strong>
</div> </div>
<div class="card-body"> <div class="card-body">
<form class="form-horizontal" action="/user/totp/{{.Data.Username}}" method="post"> <form class="form-horizontal" action="/user/totp" method="post">
<input type="hidden" name="token" value="{{.Token}}"> <input type="hidden" name="token" value="{{.Token}}">
{{if .Data.Secret}} {{if .Data.Secret}}
<input type="hidden" name="url" value="{{.Data.URL}}"> <input type="hidden" name="url" value="{{.Data.URL}}">
@ -28,11 +27,10 @@
{{else}} {{else}}
<button class="btn btn-sm btn-danger" type="submit">Disable</button> <button class="btn btn-sm btn-danger" type="submit">Disable</button>
{{end}} {{end}}
<a href="/user/edit/{{.Data.Username}}" class="btn btn-sm btn-primary">Back</a> <a href="/user/edit" class="btn btn-sm btn-primary">Back</a>
</form> </form>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
{{end}} {{end}}
{{end}}

View file

@ -1,55 +0,0 @@
{{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}}