first working version
This commit is contained in:
parent
18995db757
commit
de359ab415
47 changed files with 1016 additions and 2012 deletions
|
|
@ -11,6 +11,7 @@ import (
|
|||
"html/template"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"reflect"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
|
@ -21,59 +22,29 @@ const (
|
|||
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.Set("X-Frame-Options", "DENY")
|
||||
h.Set("X-Frame-Options", "sameorigin")
|
||||
h.Set("X-Content-Type-Options", "nosniff")
|
||||
h.Set("X-XSS-Protection", "1; mode=block")
|
||||
h.Set("Content-Security-Policy", "default-src 'none';style-src 'self';img-src 'self' https: data:;connect-src 'self';frame-ancestors 'none'")
|
||||
h.Set("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")
|
||||
ctx = &Context{
|
||||
return &Context{
|
||||
Request: r,
|
||||
Response: w,
|
||||
Srv: s,
|
||||
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 {
|
||||
Request *http.Request
|
||||
Response http.ResponseWriter
|
||||
Srv *HTTPServer
|
||||
T *template.Template
|
||||
Status int
|
||||
Err error
|
||||
Time time.Time
|
||||
Request *http.Request
|
||||
Response http.ResponseWriter
|
||||
Srv *HTTPServer
|
||||
T *template.Template
|
||||
HTTPStatus int
|
||||
Err error
|
||||
Time time.Time
|
||||
|
||||
Token jwt.Token
|
||||
|
||||
|
|
@ -96,7 +67,9 @@ type webData struct {
|
|||
Msg string
|
||||
Search string
|
||||
|
||||
Data interface{}
|
||||
Path string
|
||||
Paths core.Paths
|
||||
Data interface{}
|
||||
}
|
||||
|
||||
type loginData struct {
|
||||
|
|
@ -104,20 +77,10 @@ type loginData struct {
|
|||
Referer string
|
||||
}
|
||||
|
||||
type sectionData struct {
|
||||
Name string
|
||||
Members map[string]bool
|
||||
}
|
||||
|
||||
type pageData struct {
|
||||
Page core.Page
|
||||
Sections map[string]bool
|
||||
}
|
||||
|
||||
func (c *Context) Exec() {
|
||||
defer c.log()
|
||||
defer c.Log()
|
||||
if c.T == nil {
|
||||
c.Status = http.StatusInternalServerError
|
||||
c.Status(http.StatusInternalServerError)
|
||||
c.Err = errors.New("template is nil")
|
||||
return
|
||||
}
|
||||
|
|
@ -132,24 +95,24 @@ func (c *Context) Exec() {
|
|||
c.Data.Time = time.Since(c.Time).Nanoseconds() / 1e6
|
||||
|
||||
if c.Data.Msg != "" {
|
||||
c.Status = http.StatusBadRequest
|
||||
c.Status(http.StatusBadRequest)
|
||||
}
|
||||
|
||||
c.Err = c.T.Execute(c.Response, c.Data)
|
||||
if c.Err != nil {
|
||||
c.Status = http.StatusInternalServerError
|
||||
}
|
||||
if c.Status == 0 {
|
||||
c.Status = http.StatusOK
|
||||
c.Status(http.StatusInternalServerError)
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
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
|
||||
}
|
||||
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{}) {
|
||||
|
|
@ -165,20 +128,19 @@ func (c *Context) Error(i interface{}) {
|
|||
func (c *Context) NotFound() {
|
||||
c.Template("notFoundHandler")
|
||||
c.Data = webData{Title: "404"}
|
||||
c.Status = http.StatusNotFound
|
||||
c.Response.WriteHeader(http.StatusNotFound)
|
||||
c.Status(http.StatusNotFound)
|
||||
c.Exec()
|
||||
}
|
||||
|
||||
func (c *Context) Forbidden() {
|
||||
c.Template("forbiddenHandler")
|
||||
c.Data = webData{Title: "403"}
|
||||
c.Status = http.StatusForbidden
|
||||
c.Response.WriteHeader(http.StatusForbidden)
|
||||
c.Status(http.StatusForbidden)
|
||||
c.Exec()
|
||||
}
|
||||
|
||||
func (c *Context) SwitchHandler() {
|
||||
log.Debug("server: switching to normal handler")
|
||||
c.Srv.srv.Handler = c.Srv.handler
|
||||
c.Redirect(core.IndexURI, http.StatusFound)
|
||||
}
|
||||
|
|
@ -192,6 +154,11 @@ func (c *Context) Write(buf []byte) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
func (c *Context) Status(status int) {
|
||||
c.HTTPStatus = status
|
||||
c.Response.WriteHeader(status)
|
||||
}
|
||||
|
||||
func (c *Context) SetHeader(name, value string) {
|
||||
c.Response.Header().Set(name, value)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,13 +4,20 @@ package server
|
|||
|
||||
import (
|
||||
"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/otp"
|
||||
"git.giftfish.de/ston1th/jwt/v3"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
//TODO add missing CSRF checks
|
||||
|
||||
const noSuchFile = "no such file or directory"
|
||||
|
||||
type notFoundHandler struct {
|
||||
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 {
|
||||
return func(ctx *Context) {
|
||||
if ctx.LoggedOn() {
|
||||
h(ctx)
|
||||
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("Expires", time.Now().UTC().Add(max).Format(http.TimeFormat))
|
||||
switch ctx.Path() {
|
||||
case "/bootstrap.css":
|
||||
case core.BootstrapCSS:
|
||||
err = ctx.Write(ctx.Srv.res["bootstrap.css"])
|
||||
case "/custom.css":
|
||||
case core.CustomCSS:
|
||||
err = ctx.Write(ctx.Srv.res["custom.css"])
|
||||
case "/favicon.ico":
|
||||
case core.Favicon:
|
||||
ctx.SetHeader("Content-Type", "image/x-icon")
|
||||
err = ctx.Write(ctx.Srv.res["favicon.ico"])
|
||||
}
|
||||
|
|
@ -75,12 +135,14 @@ func registerHandler(ctx *Context) {
|
|||
case "GET":
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
user := ctx.Form("user")
|
||||
password := ctx.Form("password")
|
||||
repeat := ctx.Form("repeat")
|
||||
ctx.Data.Data = user
|
||||
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
|
||||
if user == "" || 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) {
|
||||
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) {
|
||||
|
|
@ -201,492 +411,23 @@ func searchHandler(ctx *Context) {
|
|||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
search := ctx.Form("search")
|
||||
|
||||
res, err := ctx.Srv.DB.Index.Search(search)
|
||||
ctx.Data.Search = ctx.Form("search")
|
||||
if ctx.Data.Search == "" {
|
||||
ctx.Error("empty search request")
|
||||
return
|
||||
}
|
||||
res, err := ctx.Srv.DB.Index.Search(ctx.Data.Search)
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
user := ctx.User()
|
||||
r := make([]core.Result, len(res))
|
||||
i := 0
|
||||
for _, v := range res {
|
||||
if ctx.Srv.DB.ValidatePage(v.StoreTitle, user) {
|
||||
r[i] = v
|
||||
i++
|
||||
}
|
||||
}
|
||||
|
||||
ctx.Data.Data = r[:i]
|
||||
ctx.Data.Search = search
|
||||
ctx.Exec()
|
||||
}
|
||||
}
|
||||
|
||||
func sectionsHandler(ctx *Context) {
|
||||
ctx.Template("sectionsHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "All Sections",
|
||||
BodyTitle: "All Sections",
|
||||
Admin: ctx.Admin(),
|
||||
}
|
||||
ctx.Data.Data = ctx.Srv.DB.GetAllSections(ctx.User(), ctx.Admin())
|
||||
ctx.Exec()
|
||||
}
|
||||
|
||||
func sectionHandler(ctx *Context) {
|
||||
ctx.Template("sectionHandler")
|
||||
section := ctx.Var("section")
|
||||
ctx.Data = webData{
|
||||
Title: section,
|
||||
BodyTitle: section,
|
||||
Admin: ctx.Admin(),
|
||||
}
|
||||
var err error
|
||||
ctx.Data.Data, err = ctx.Srv.DB.GetAllSectionPages(section, ctx.User(), ctx.Admin())
|
||||
if err != nil {
|
||||
ctx.NotFound()
|
||||
return
|
||||
}
|
||||
ctx.Exec()
|
||||
}
|
||||
|
||||
func sectionNewHandler(ctx *Context) {
|
||||
ctx.Template("sectionNewHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "New Section",
|
||||
BodyTitle: "New Section",
|
||||
}
|
||||
users, err := ctx.Srv.DB.GetUsers()
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
sd := sectionData{Members: make(map[string]bool)}
|
||||
for _, u := range users {
|
||||
sd.Members[u.Username] = false
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.Data.Data = sd
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
section := ctx.Form("section")
|
||||
members := ctx.FormSlice("members")
|
||||
for _, m := range members {
|
||||
sd.Members[m] = true
|
||||
}
|
||||
ctx.Data.Data = sd
|
||||
if section == "" {
|
||||
ctx.Error("empty section")
|
||||
return
|
||||
}
|
||||
err = ctx.Srv.DB.CreateSection(section, members, false)
|
||||
err = ctx.Srv.DB.GetPaths(res)
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
log.Printf("create: section %s created by %s\n", section, ctx.User())
|
||||
ctx.Redirect(core.SectionsURI, http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
func sectionEditHandler(ctx *Context) {
|
||||
ctx.Template("sectionEditHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "Edit Section",
|
||||
BodyTitle: "Edit Section",
|
||||
}
|
||||
users, err := ctx.Srv.DB.GetUsers()
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
sd := sectionData{Members: make(map[string]bool)}
|
||||
for _, u := range users {
|
||||
sd.Members[u.Username] = false
|
||||
}
|
||||
s, err := ctx.Srv.DB.GetSection(ctx.Var("section"))
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
sd.Name = s.Name
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
for _, m := range s.Members {
|
||||
sd.Members[m] = true
|
||||
}
|
||||
ctx.Data.Data = sd
|
||||
ctx.Data.Data = res
|
||||
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",
|
||||
BodyTitle: "Edit User",
|
||||
}
|
||||
u, err := ctx.Srv.DB.GetUserWithoutPassword(ctx.Var("user"))
|
||||
u, err := ctx.Srv.DB.GetUserWithoutPassword()
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
|
|
@ -709,33 +450,18 @@ func userEditHandler(ctx *Context) {
|
|||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
user := ctx.Var("user")
|
||||
password := ctx.Form("password")
|
||||
repeat := ctx.Form("repeat")
|
||||
admin := ctx.Form("admin")
|
||||
adm := false
|
||||
if admin == "0" {
|
||||
adm = true
|
||||
}
|
||||
|
||||
if password != repeat {
|
||||
ctx.Error("passwords do not match")
|
||||
return
|
||||
}
|
||||
if ctx.Admin() {
|
||||
if err := ctx.Srv.DB.AdminUpdateUser(user, password, adm); err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
log.Printf("update: user %s updated by %s\n", user, ctx.User())
|
||||
ctx.Redirect("/users", http.StatusFound)
|
||||
return
|
||||
}
|
||||
if err := ctx.Srv.DB.UpdateUserPassword(user, password); err != nil {
|
||||
if err := ctx.Srv.DB.UpdateUserPassword(password); err != nil {
|
||||
ctx.Error(err)
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -747,7 +473,7 @@ func userTotpHandler(ctx *Context) {
|
|||
BodyTitle: "TOTP",
|
||||
}
|
||||
req := otp.Request{Username: ctx.Var("user")}
|
||||
u, err := ctx.Srv.DB.GetUser(req.Username)
|
||||
u, err := ctx.Srv.DB.GetUser()
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
|
|
@ -787,68 +513,23 @@ func userTotpHandler(ctx *Context) {
|
|||
if u.Secret != "" {
|
||||
secret = u.Secret
|
||||
}
|
||||
valid := otp.Validate(pin, secret)
|
||||
user := ctx.User()
|
||||
if user != req.Username && ctx.Admin() {
|
||||
valid = true
|
||||
}
|
||||
|
||||
if !valid {
|
||||
if !otp.Validate(pin, secret) {
|
||||
ctx.Error("validation failed")
|
||||
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)
|
||||
return
|
||||
}
|
||||
if req.Secret == "" {
|
||||
log.Printf("totp: disabled by %s for %s\n", user, req.Username)
|
||||
log.Printf("totp: disabled for %s\n", user)
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
ctx.Redirect("/user/edit", http.StatusFound)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
|
||||
package server
|
||||
|
||||
import "git.giftfish.de/ston1th/docstore/pkg/core"
|
||||
|
||||
type route struct {
|
||||
Path string
|
||||
Handler ctxHandler
|
||||
|
|
@ -10,90 +12,106 @@ type route struct {
|
|||
|
||||
var static = []route{
|
||||
{
|
||||
"/favicon.ico",
|
||||
core.Favicon,
|
||||
staticHandler,
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/bootstrap.css",
|
||||
core.BootstrapCSS,
|
||||
staticHandler,
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/custom.css",
|
||||
core.CustomCSS,
|
||||
staticHandler,
|
||||
[]string{"GET"},
|
||||
},
|
||||
}
|
||||
|
||||
var register = append(static, route{
|
||||
"/",
|
||||
registerHandler,
|
||||
var registerRoutes = append(static, route{
|
||||
core.IndexURI,
|
||||
jwtRegisterHandler(
|
||||
registerHandler),
|
||||
[]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{
|
||||
{
|
||||
"/",
|
||||
authHandler(
|
||||
dirHandler),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/sections",
|
||||
sectionsHandler,
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/search",
|
||||
authHandler(
|
||||
searchHandler),
|
||||
jwtHandler(
|
||||
authHandler(
|
||||
searchHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/upload",
|
||||
authHandler(
|
||||
searchHandler),
|
||||
core.LoginURI,
|
||||
jwtHandler(
|
||||
loginHandler),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/login",
|
||||
loginHandler,
|
||||
core.TotpURI,
|
||||
jwtHandler(
|
||||
totpAuthHandler(
|
||||
loginTotpHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/totp",
|
||||
totpAuthHandler(
|
||||
loginTotpHandler),
|
||||
"/user/edit",
|
||||
jwtHandler(
|
||||
authHandler(
|
||||
userEditHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/user/edit/{user:[a-zA-Z0-9]+$}",
|
||||
authHandler(
|
||||
userEditHandler),
|
||||
"/user/totp",
|
||||
jwtHandler(
|
||||
authHandler(
|
||||
userTotpHandler)),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/user/totp/{user:[a-zA-Z0-9]+$}",
|
||||
authHandler(
|
||||
userTotpHandler),
|
||||
core.LogoutURI,
|
||||
jwtHandler(
|
||||
logoutHandler),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/logout",
|
||||
logoutHandler,
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/{dir}",
|
||||
authHandler(
|
||||
dirHandler),
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/{dir}/{file}",
|
||||
authHandler(
|
||||
fileHandler),
|
||||
[]string{"GET"},
|
||||
},
|
||||
}...)
|
||||
|
|
|
|||
|
|
@ -8,7 +8,9 @@ import (
|
|||
"errors"
|
||||
"git.giftfish.de/ston1th/docstore/pkg/core"
|
||||
"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/tesseract"
|
||||
"git.giftfish.de/ston1th/godrop/v2"
|
||||
"git.giftfish.de/ston1th/jwt/v3"
|
||||
"github.com/gorilla/mux"
|
||||
|
|
@ -19,7 +21,7 @@ import (
|
|||
)
|
||||
|
||||
const (
|
||||
cookieName = "gosession"
|
||||
cookieName = "docstore_session"
|
||||
)
|
||||
|
||||
type HTTPServer struct {
|
||||
|
|
@ -29,8 +31,10 @@ type HTTPServer struct {
|
|||
srv *http.Server
|
||||
handler http.Handler
|
||||
|
||||
DB *db.DB
|
||||
JWT *jwt.JWT
|
||||
DB *db.DB
|
||||
JWT *jwt.JWT
|
||||
Tesseract *tesseract.Tesseract
|
||||
FS *fs.Filesystem
|
||||
|
||||
templ map[string]*template.Template
|
||||
res map[string][]byte
|
||||
|
|
@ -40,14 +44,22 @@ func NewHTTPServer(cfg core.Config, l net.Listener) (srv *HTTPServer) {
|
|||
srv = &HTTPServer{
|
||||
Config: cfg,
|
||||
listener: l,
|
||||
handler: srv.buildRoutes(),
|
||||
}
|
||||
srv.handler = srv.buildRoutes()
|
||||
srv.loadTemplates()
|
||||
srv.srv = &http.Server{}
|
||||
return
|
||||
}
|
||||
|
||||
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)
|
||||
if err != nil {
|
||||
return errors.New("db: " + err.Error())
|
||||
|
|
@ -65,6 +77,7 @@ func (s *HTTPServer) Start() (err error) {
|
|||
return errors.New("server: " + err.Error())
|
||||
}
|
||||
if !s.DB.UserExists() {
|
||||
log.Debug("server: starting register handler")
|
||||
s.srv.Handler = s.register()
|
||||
} else {
|
||||
s.srv.Handler = s.handler
|
||||
|
|
@ -84,29 +97,28 @@ func (s *HTTPServer) Stop() error {
|
|||
}
|
||||
|
||||
func (s *HTTPServer) register() http.Handler {
|
||||
m := mux.NewRouter()
|
||||
m.NotFoundHandler = ®isterNotFoundHandler{s}
|
||||
for _, v := range register {
|
||||
m.HandleFunc(v.Path, s.contextWrapper(v.Handler)).Methods(v.Methods...)
|
||||
r := mux.NewRouter()
|
||||
r.NotFoundHandler = ®isterNotFoundHandler{s}
|
||||
for _, v := range registerRoutes {
|
||||
r.HandleFunc(v.Path, s.contextWrapper(v.Handler)).Methods(v.Methods...)
|
||||
}
|
||||
return m
|
||||
return r
|
||||
}
|
||||
|
||||
func (s *HTTPServer) buildRoutes() http.Handler {
|
||||
m := mux.NewRouter()
|
||||
m.NotFoundHandler = ¬FoundHandler{s}
|
||||
r := mux.NewRouter()
|
||||
r.NotFoundHandler = ¬FoundHandler{s}
|
||||
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 {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
ctx := newContext(w, r, s)
|
||||
if ctx == nil {
|
||||
return
|
||||
}
|
||||
h(ctx)
|
||||
h(newContext(w, r, s))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,6 +13,94 @@ import (
|
|||
)
|
||||
|
||||
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> /
|
||||
{{range $item := .Paths}}
|
||||
<a href="{{$item.Abs}}">{{$item.Name}}</a> /
|
||||
{{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}}
|
||||
/ <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"}}
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
|
|
@ -33,7 +121,7 @@ const (
|
|||
index = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<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="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>
|
||||
|
|
@ -43,7 +131,7 @@ const (
|
|||
<body id="top">
|
||||
<div class="navbar navbar-expand fixed-top navbar-dark bg-primary">
|
||||
<div class="container">
|
||||
<a href="/" class="navbar-brand">GoWiki</a>
|
||||
<a href="/" class="navbar-brand">DocStore</a>
|
||||
<div class="collapse navbar-collapse">
|
||||
{{template "menu" .}}
|
||||
</div>
|
||||
|
|
@ -65,11 +153,8 @@ const (
|
|||
<div class="col">
|
||||
<ul class="list-unstyled">
|
||||
<li class="float-md-right"><a href="#top">Back to top</a></li>
|
||||
{{if .Login}}
|
||||
<li><a href="/blacklist">Blacklist</a></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
<p>© GoWiki {{.Version}} Request: <strong>{{.Time}}ms</strong></p>
|
||||
<p>© DocStore {{.Version}} Request: <strong>{{.Time}}ms</strong></p>
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
|
@ -136,11 +221,7 @@ const (
|
|||
{{end}}`
|
||||
menu = `{{define "menu"}}
|
||||
<ul class="navbar-nav">
|
||||
<li class="nav-item"><a class="nav-link" href="/wiki/Index">Index</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/sections">Sections</a></li>
|
||||
{{if .Login}}
|
||||
<li class="nav-item"><a class="nav-link" href="/new">New Page</a></li>
|
||||
{{end}}
|
||||
<li class="nav-item"><a class="nav-link" href="/">Root</a></li>
|
||||
</ul>
|
||||
<form class="form-inline" method="post" action="/search">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
|
|
@ -149,16 +230,41 @@ const (
|
|||
</form>
|
||||
<ul class="nav navbar-nav ml-auto">
|
||||
{{if .Login}}
|
||||
{{if .Admin}}
|
||||
<li class="nav-item"><a class="nav-link" href="/users">Users</a></li>
|
||||
{{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="/user/edit">Profile</a></li>
|
||||
<li class="nav-item"><a class="nav-link" href="/logout">Logout</a></li>
|
||||
{{else}}
|
||||
<li class="nav-item"><a class="nav-link" href="/login">Login</a></li>
|
||||
{{end}}
|
||||
</ul>
|
||||
{{end}}`
|
||||
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}}`
|
||||
notFound = `{{define "body"}}
|
||||
<div class="page-header">
|
||||
|
|
@ -177,249 +283,34 @@ const (
|
|||
</div>
|
||||
</div>
|
||||
{{end}}`
|
||||
pageBlacklist = `{{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>
|
||||
register = `{{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="/" 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 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 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>
|
||||
</div>
|
||||
</div>
|
||||
</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 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="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>
|
||||
<button class="btn btn-sm btn-primary" type="submit">Create</button>
|
||||
</form>
|
||||
</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}}`
|
||||
search = `{{define "body"}}
|
||||
<div class="page-header">
|
||||
|
|
@ -444,205 +335,32 @@ const (
|
|||
</div>
|
||||
<div class="row mt-3">
|
||||
<div class="col">
|
||||
{{if .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>
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
{{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>
|
||||
{{else}}
|
||||
<b>No search results found.</b>
|
||||
{{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}}`
|
||||
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}}`
|
||||
userEdit = `{{define "body"}}
|
||||
{{if .Data}}
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-primary mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>{{.BodyTitle}}</strong>
|
||||
{{if .Data.Secret}}
|
||||
<a href="/user/totp/{{.Data.Username}}" class="btn btn-sm btn-danger float-right">Disable TOTP</a>
|
||||
<a href="/user/totp" class="btn btn-sm btn-danger float-right">Disable TOTP</a>
|
||||
{{else}}
|
||||
<a href="/user/totp/{{.Data.Username}}" class="btn btn-sm btn-success float-right">Enable TOTP</a>
|
||||
<a href="/user/totp" class="btn btn-sm btn-success float-right">Enable TOTP</a>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="form-horizontal" action="/user/edit/{{.Data.Username}}" method="post">
|
||||
<form class="form-horizontal" action="/user/edit" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="user">Username</label>
|
||||
<input class="form-control input-sm" type="text" id="user" name="user" value="{{.Data.Username}}" disabled>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="password">Password</label>
|
||||
<input class="form-control input-sm" type="password" id="password" name="password" autofocus>
|
||||
|
|
@ -651,42 +369,15 @@ const (
|
|||
<label class="col-form-label" for="repeat">Repeat</label>
|
||||
<input class="form-control input-sm" type="password" id="repeat" name="repeat">
|
||||
</div>
|
||||
{{if .Admin}}
|
||||
<div class="form-group">
|
||||
<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>
|
||||
{{if .Admin}}
|
||||
<a href="/users" class="btn btn-sm btn-primary">Back</a>
|
||||
{{else}}
|
||||
<a href="/" class="btn btn-sm btn-primary">Back</a>
|
||||
{{end}}
|
||||
<a class="btn btn-sm btn-danger" href="/user/del/{{.Data.Username}}">Delete</a>
|
||||
</form>
|
||||
{{if .Admin}}
|
||||
{{if eq .Data.Locked 3}}
|
||||
<form class="form-horizontal" action="/user/unlock/{{.Data.Username}}" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="unlock">Locked</label>
|
||||
<button class="btn btn-sm btn-warning" type="submit" id="unlock">Unlock</button>
|
||||
</div>
|
||||
</form>
|
||||
{{end}}
|
||||
{{end}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}`
|
||||
userNew = `{{define "body"}}
|
||||
userTotp = `{{define "body"}}
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-primary mx-auto">
|
||||
|
|
@ -694,97 +385,7 @@ const (
|
|||
<strong>{{.BodyTitle}}</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="form-horizontal" action="/user/new" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="user">Username</label>
|
||||
<input class="form-control input-sm" type="text" id="user" name="user" autocomplete="off" autofocus required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="password">Password</label>
|
||||
<input class="form-control input-sm" type="password" id="password" name="password" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="repeat">Repeat</label>
|
||||
<input class="form-control input-sm" type="password" id="repeat" name="repeat" required>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<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">
|
||||
<form class="form-horizontal" action="/user/totp" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
{{if .Data.Secret}}
|
||||
<input type="hidden" name="url" value="{{.Data.URL}}">
|
||||
|
|
@ -805,13 +406,12 @@ const (
|
|||
{{else}}
|
||||
<button class="btn btn-sm btn-danger" type="submit">Disable</button>
|
||||
{{end}}
|
||||
<a href="/user/edit/{{.Data.Username}}" class="btn btn-sm btn-primary">Back</a>
|
||||
<a href="/user/edit" class="btn btn-sm btn-primary">Back</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}`
|
||||
bootstrapCSS = `/*!
|
||||
* Bootswatch v4.1.3
|
||||
|
|
@ -1005,31 +605,19 @@ func (s *HTTPServer) loadTemplates() {
|
|||
}
|
||||
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["forbiddenHandler"] = parse(index, menu, forbidden)
|
||||
s.templ["loginHandler"] = parse(index, menu, login)
|
||||
s.templ["loginTotpHandler"] = parse(index, menu, loginTotp)
|
||||
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["userDelHandler"] = parse(index, menu, userDel)
|
||||
// totp
|
||||
s.templ["userTotpHandler"] = parse(index, menu, userTotp)
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue