first working version

This commit is contained in:
ston1th 2018-09-14 00:14:49 +02:00
commit 85f93bae69
32 changed files with 863 additions and 571 deletions

View file

@ -12,6 +12,14 @@ import (
"time"
)
const (
userClaim = "user"
adminClaim = "admin"
sharedClaim = "shared"
sectionClaim = "section"
titleClaim = "title"
)
func newContext(w http.ResponseWriter, r *http.Request, s *HTTPServer) (ctx *Context) {
h := w.Header()
h.Set("X-Frame-Options", "DENY")
@ -65,6 +73,8 @@ type webData struct {
Admin bool
Login bool
Key string
User string
Token string
Msg string
@ -86,10 +96,11 @@ func (c *Context) Exec() {
if c.Data.BodyTitle == "" {
c.Data.BodyTitle = c.Data.Title
}
c.Data.Admin = c.GetAdmin()
c.Data.User = c.GetUser()
c.Data.Admin = c.Admin()
c.Data.User = c.User()
c.Data.Login = c.LoggedOn()
c.Data.Time = time.Since(c.Time).Nanoseconds() / 1e6
c.Err = c.T.Execute(c.Response, c.Data)
if c.Err != nil {
c.Status = http.StatusInternalServerError
@ -114,6 +125,7 @@ func (c *Context) NotFound() {
c.Response.WriteHeader(http.StatusNotFound)
c.Exec()
}
func (c *Context) Template(name string) {
c.T = c.Srv.templ[name]
}
@ -128,6 +140,9 @@ func (c *Context) SetHeader(name, value string) {
}
func (c *Context) Redirect(uri string, code int) {
if len(uri) > 0 && uri[0] != '/' {
uri = "/" + uri
}
log.Printf("%s %s %s %d %s\n", c.Request.RemoteAddr, c.Request.Method, c.Request.URL, code, uri)
http.Redirect(c.Response, c.Request, uri, code)
}
@ -146,9 +161,8 @@ func (c *Context) FormSlice(name string) []string {
return c.Request.PostForm[name]
}
func (c *Context) Form(name string) (ret string) {
ret = c.Request.PostFormValue(name)
return
func (c *Context) Form(name string) string {
return c.Request.PostFormValue(name)
}
func (c *Context) Var(name string) (ret string) {
@ -156,6 +170,7 @@ func (c *Context) Var(name string) (ret string) {
return
}
// TODO remove f
// CheckToken validates the xsrf token
func (c *Context) CheckXsrf(f func()) (ok bool) {
ok = checkXsrf(c.Form("token"), c.Token.RawSig()[:keySize])
@ -171,7 +186,27 @@ func (c *Context) CheckXsrf(f func()) (ok bool) {
}
func (c *Context) LoggedOn() (ok bool) {
_, ok = c.Token.Claims["user"]
_, ok = c.Token.Claims.Get(userClaim)
return
}
func newCookie(value string, maxAge int) (cookie *http.Cookie) {
cookie = &http.Cookie{
Name: cookieName,
Value: value,
Path: "/",
MaxAge: maxAge,
//TODO make secure configurable
Secure: false,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
}
if maxAge > 0 {
d := time.Duration(maxAge) * time.Second
cookie.Expires = time.Now().Add(d)
} else if maxAge < 0 {
cookie.Expires = time.Unix(1, 0)
}
return
}
@ -182,28 +217,15 @@ func (c *Context) SetCookie(claims map[string]interface{}) {
return
}
c.Token = *t
http.SetCookie(c.Response, &http.Cookie{
Name: cookieName,
Value: t.Sig(),
MaxAge: 3600 * 12,
Secure: true,
HttpOnly: true,
SameSite: http.SameSiteStrictMode,
})
http.SetCookie(c.Response, newCookie(c.Token.String(), int(jwt.DefaultExpiry.Seconds())))
}
func (c *Context) GetUser() string {
if ret, ok := c.Token.Claims["user"].(string); ok {
return ret
}
return ""
func (c *Context) User() string {
return c.Token.Claims.GetString(userClaim)
}
func (c *Context) GetAdmin() bool {
if ret, ok := c.Token.Claims["admin"].(bool); ok {
return ret
}
return false
func (c *Context) Admin() bool {
return c.Token.Claims.GetBool(adminClaim)
}
type ctxHandler func(*Context)

View file

@ -5,7 +5,8 @@ package server
import (
"git.giftfish.de/ston1th/gowiki/pkg/core"
"git.giftfish.de/ston1th/gowiki/pkg/log"
"html/template"
"git.giftfish.de/ston1th/gowiki/pkg/util"
"git.giftfish.de/ston1th/jwt"
"net/http"
"time"
)
@ -20,7 +21,7 @@ func (nf *notFoundHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
func adminAuthHandler(h ctxHandler) ctxHandler {
return func(ctx *Context) {
if ctx.GetAdmin() {
if ctx.Admin() {
h(ctx)
return
}
@ -30,11 +31,11 @@ func adminAuthHandler(h ctxHandler) ctxHandler {
func userAuthHandler(h ctxHandler) ctxHandler {
return func(ctx *Context) {
if ctx.GetAdmin() {
if ctx.Admin() {
h(ctx)
return
}
if ctx.GetUser() == ctx.Var("user") {
if ctx.User() == ctx.Var("user") {
h(ctx)
return
}
@ -68,12 +69,12 @@ func staticHandler(ctx *Context) {
}
func indexHandler(ctx *Context) {
ctx.Redirect("/wiki/Index", 302)
ctx.Redirect(core.IndexURI, 302)
}
func loginHandler(ctx *Context) {
if ctx.LoggedOn() {
ctx.Redirect("/wiki/Index", 302)
ctx.Redirect(core.IndexURI, 302)
return
}
ctx.Template("loginHandler")
@ -87,11 +88,10 @@ func loginHandler(ctx *Context) {
case "POST":
user := ctx.Form("user")
password := ctx.Form("password")
submit := ctx.Form("submit")
if !ctx.CheckXsrf(nil) {
return
}
if user == "" || password == "" || submit != "Login" {
if user == "" || password == "" {
ctx.Data.Msg = "wrong inputs"
ctx.Exec()
return
@ -102,8 +102,8 @@ func loginHandler(ctx *Context) {
ctx.Exec()
return
}
ctx.SetCookie(map[string]interface{}{"username": u.Username, "admin": u.Admin})
ctx.Redirect("/wiki/Index", 302)
ctx.SetCookie(map[string]interface{}{userClaim: u.Username, adminClaim: u.Admin})
ctx.Redirect(core.IndexURI, 302)
}
}
@ -118,15 +118,9 @@ func searchHandler(ctx *Context) {
ctx.Exec()
case "POST":
search := ctx.Form("search")
submit := ctx.Form("submit")
if !ctx.CheckXsrf(nil) {
return
}
if submit != "Search" {
ctx.Data.Msg = "wrong inputs"
ctx.Exec()
return
}
res, err := ctx.Srv.DB.Index.Search(search)
if err != nil {
@ -135,7 +129,7 @@ func searchHandler(ctx *Context) {
return
}
ctx.Data.Search = search
ctx.Data.Data = template.HTML(res)
ctx.Data.Data = res
ctx.Exec()
}
}
@ -143,167 +137,213 @@ func searchHandler(ctx *Context) {
func allHandler(ctx *Context) {
ctx.Template("allHandler")
ctx.Data = webData{
Title: "All Articles",
BodyTitle: "All Articles",
Admin: ctx.GetAdmin(),
Title: "All Pages",
BodyTitle: "All Pages",
Admin: ctx.Admin(),
}
all, err := ctx.Srv.DB.Index.GetAll(!ctx.LoggedOn())
if err != nil {
ctx.Data.Msg = err.Error()
}
ctx.Data.Data = all
ctx.Data.Data = ctx.Srv.DB.GetAllPages(ctx.User())
ctx.Exec()
}
func wikiNewHandler(ctx *Context) {
ctx.Template("wikiNewHandler")
ctx.Data = webData{Title: "New Article"}
func pageNewHandler(ctx *Context) {
ctx.Template("pageNewHandler")
ctx.Data = webData{Title: "New Page"}
switch ctx.Method() {
case "GET":
ctx.Exec()
case "POST":
section := ctx.Form("section")
title := ctx.Form("title")
text := ctx.Form("text")
public := ctx.Form("public")
pub := false
if public == "0" {
pub = true
}
submit := ctx.Form("submit")
markdown := ctx.Form("markdown")
perm := ctx.Form("perm")
p := core.Page{
Title: title,
MD: text,
Title: title,
Markdown: markdown,
}
ctx.Data.Data = p
if !ctx.CheckXsrf(nil) {
return
}
if submit != "Submit" {
ctx.Data.Msg = "wrong inputs"
ctx.Exec()
return
if section != core.WikiSection {
section = ctx.User()
}
newTitle, err := ctx.Srv.DB.Index.Index(title, text, ctx.GetUser(), pub)
page, err := ctx.Srv.DB.CreatePage(title, section, markdown, ctx.User(), core.ParsePermString(perm))
if err != nil {
ctx.Data.Msg = err.Error()
ctx.Exec()
return
}
ctx.Redirect("/wiki/"+newTitle, 302)
ctx.Redirect(page.StoreTitle, 302)
}
}
func wikiHandler(ctx *Context) {
if ctx.T == nil {
ctx.Template("wikiHandler")
}
func pageHandler(ctx *Context) {
ctx.Data = webData{
Admin: ctx.GetAdmin(),
Admin: ctx.Admin(),
}
section := ctx.Var("section")
title := ctx.Var("title")
art, err := ctx.Srv.DB.Index.Get(!ctx.LoggedOn(), title)
user := ctx.User()
if key := ctx.Var("key"); key != "" {
t, err := jwt.DecodeToken(key)
if err != nil {
log.Println("DecodeToken:", err)
ctx.NotFound()
return
}
if err = ctx.Srv.JWT.Verify(t); err != nil {
log.Println("VerifyToken:", err)
ctx.NotFound()
return
}
user = t.Claims.GetString(sharedClaim)
section = t.Claims.GetString(sectionClaim)
title = t.Claims.GetString(titleClaim)
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(util.StoreTitle(section, title), user)
if err != nil {
log.Println(err)
ctx.NotFound()
return
}
art.LinkTitle = title
ctx.Data.Data = art
ctx.Data.Title = art.Title
ctx.Data.BodyTitle = art.Title
ctx.Data.Data = page
ctx.Data.Title = page.Title
ctx.Data.BodyTitle = page.Title
ctx.Exec()
}
func wikiMDHandler(f ctxHandler) ctxHandler {
func pageMDHandler(f ctxHandler) ctxHandler {
return func(ctx *Context) {
ctx.Template("wikiMDHandler")
ctx.Template("pageMDHandler")
f(ctx)
}
}
func wikiEditHandler(ctx *Context) {
ctx.Template("wikiEditHandler")
// TODO add share token blacklisting
func pageShareHandler(ctx *Context) {
ctx.Template("pageShareHandler")
ctx.Data = webData{}
section := ctx.Var("section")
title := ctx.Var("title")
user := ctx.User()
st := util.StoreTitle(section, title)
page, err := ctx.Srv.DB.GetPage(st, user)
if err != nil {
ctx.NotFound()
return
}
t := jwt.NewToken(map[string]interface{}{
sharedClaim: user,
sectionClaim: section,
titleClaim: title,
jwt.ExpClaim: jwt.NewExp(time.Hour * 24),
}, nil)
if err = ctx.Srv.JWT.Sign(t); err != nil {
log.Println("SignToken:", err)
ctx.NotFound()
return
}
/* TODO
host := ctx.Request.Header.Get("X-Forwarded-For")
if host == "" {
ctx.Request.Host
}
ctx.Data.Data = "https://" + host + "/view/" + t.String()
*/
ctx.Data.Key = t.String()
ctx.Data.Data = st
ctx.Data.Title = page.Title
ctx.Data.BodyTitle = page.Title
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(util.StoreTitle(section, title), ctx.User())
if err != nil {
ctx.NotFound()
return
}
switch ctx.Method() {
case "GET":
art, err := ctx.Srv.DB.Index.Get(!ctx.LoggedOn(), title)
if err != nil {
ctx.NotFound()
return
}
art.LinkTitle = title
ctx.Data.Data = art
ctx.Data.Title = art.Title
ctx.Data.BodyTitle = art.Title
ctx.Data.Data = page
ctx.Data.Title = page.Title
ctx.Data.BodyTitle = page.Title
ctx.Exec()
case "POST":
ftitle := ctx.Form("title")
text := ctx.Form("text")
public := ctx.Form("public")
pub := false
if public == "0" {
pub = true
}
submit := ctx.Form("submit")
storeTitle := ctx.Form("store_title")
section, title = util.UnstoreTitle(storeTitle)
markdown := ctx.Form("markdown")
perm := core.ParsePermString(ctx.Form("perm"))
ctx.Data.Data = core.Page{
Title: ftitle,
LinkTitle: title,
MD: text,
Public: pub,
Title: page.Title,
StoreTitle: storeTitle,
Markdown: markdown,
Perm: perm,
}
ctx.Data.Title = ftitle
ctx.Data.BodyTitle = ftitle
ctx.Data.Title = page.Title
ctx.Data.BodyTitle = page.Title
if !ctx.CheckXsrf(nil) {
return
}
if submit != "Submit" {
ctx.Data.Msg = "wrong inputs"
ctx.Exec()
return
}
err := ctx.Srv.DB.Index.Update(title, text, ctx.GetUser(), pub)
err = ctx.Srv.DB.UpdatePage(title, section, markdown, ctx.User(), perm)
if err != nil {
ctx.Data.Msg = err.Error()
ctx.Exec()
return
}
ctx.Srv.Editors.Remove(title)
ctx.Redirect("/wiki/"+title, 302)
ctx.Redirect("/"+page.StoreTitle, 302)
}
}
func wikiDelHandler(ctx *Context) {
func pageDelHandler(ctx *Context) {
section := ctx.Var("section")
title := ctx.Var("title")
submit := ctx.Form("submit")
f := func() {
ctx.Redirect("/wiki/"+title, 302)
st := util.StoreTitle(section, title)
ctx.Template("pageDelHandler")
ctx.Data.Data = st
ctx.Data.Title = title
ctx.Data.BodyTitle = title
switch ctx.Method() {
case "GET":
ctx.Exec()
case "POST":
if !ctx.CheckXsrf(nil) {
return
}
if err := ctx.Srv.DB.DeletePage(title, section, ctx.User()); err != nil {
ctx.Data.Msg = err.Error()
ctx.Exec()
return
}
ctx.Redirect(core.IndexURI, 302)
}
if !ctx.CheckXsrf(f) {
return
}
if submit != "Delete" {
f()
return
}
if err := ctx.Srv.Index.Delete(title); err != nil {
log.Println("delete:", err)
f()
}
ctx.Redirect("/wiki/Index", 302)
}
func usersHandler(ctx *Context) {
func userHandler(ctx *Context) {
if !ctx.LoggedOn() {
ctx.NotFound()
return
}
ctx.Template("usersHandler")
ctx.Template("userHandler")
ctx.Data = webData{
Title: "Users",
BodyTitle: "Users",
User: ctx.GetUser(),
User: ctx.User(),
}
us, err := ctx.Srv.BS.GetUsers()
us, err := ctx.Srv.DB.GetUsers()
if err != nil {
ctx.Data.Msg = err.Error()
}
@ -329,21 +369,20 @@ func userNewHandler(ctx *Context) {
if admin == "0" {
adm = true
}
submit := ctx.Form("submit")
if !ctx.CheckXsrf(nil) {
return
}
if user == "" || password == "" || password != repeat || submit != "Submit" {
if user == "" || password == "" || password != repeat {
ctx.Data.Msg = "wrong inputs"
ctx.Exec()
return
}
if err := ctx.Srv.BS.CreateUser(user, password, adm); err != nil {
if err := ctx.Srv.DB.CreateUser(user, password, adm); err != nil {
ctx.Data.Msg = err.Error()
ctx.Exec()
return
}
ctx.Redirect("/users", 302)
ctx.Redirect("/user", 302)
}
}
@ -355,7 +394,7 @@ func userEditHandler(ctx *Context) {
}
switch ctx.Method() {
case "GET":
u, err := ctx.Srv.BS.GetUserWithoutPassword(ctx.Var("user"))
u, err := ctx.Srv.DB.GetUserWithoutPassword(ctx.Var("user"))
if err != nil {
ctx.Data.Msg = err.Error()
ctx.Exec()
@ -372,42 +411,41 @@ func userEditHandler(ctx *Context) {
if admin == "0" {
adm = true
}
submit := ctx.Form("submit")
if !ctx.CheckXsrf(nil) {
return
}
if user == "" || password != repeat || submit != "Submit" {
if user == "" || password != repeat {
ctx.Data.Msg = "wrong inputs"
ctx.Exec()
return
}
if ctx.GetAdmin() {
if err := ctx.Srv.BS.AdminUpdateUser(user, password, adm); err != nil {
if ctx.Admin() {
if err := ctx.Srv.DB.AdminUpdateUser(user, password, adm); err != nil {
ctx.Data.Msg = err.Error()
ctx.Exec()
return
}
} else {
if user == ctx.GetUser() {
if err := ctx.Srv.BS.UpdateUserPassword(user, password); err != nil {
if user == ctx.User() {
if err := ctx.Srv.DB.UpdateUserPassword(user, password); err != nil {
ctx.Data.Msg = err.Error()
ctx.Exec()
return
}
ctx.Redirect("/wiki/Index", 302)
ctx.Redirect(core.IndexURI, 302)
return
}
}
ctx.Redirect("/users", 302)
ctx.Redirect("/user", 302)
}
}
func userUnlockHandler(ctx *Context) {
user := ctx.Var("user")
if err := ctx.Srv.BS.UnlockUser(user); err != nil {
if err := ctx.Srv.DB.UnlockUser(user); err != nil {
log.Println("unlock:", err)
}
ctx.Redirect("/users", 302)
ctx.Redirect("/user", 302)
}
func userDelHandler(ctx *Context) {
@ -418,7 +456,7 @@ func userDelHandler(ctx *Context) {
}
switch ctx.Method() {
case "GET":
user, err := ctx.Srv.BS.GetUserWithoutPassword(ctx.Var("user"))
user, err := ctx.Srv.DB.GetUserWithoutPassword(ctx.Var("user"))
if err != nil {
ctx.Data.Msg = err.Error()
ctx.Exec()
@ -427,14 +465,14 @@ func userDelHandler(ctx *Context) {
ctx.Data.Data = user
ctx.Exec()
case "POST":
if err := ctx.Srv.BS.DeleteUser(ctx.Var("user")); err != nil {
if err := ctx.Srv.DB.DeleteUser(ctx.Var("user")); err != nil {
log.Println("delete:", err)
}
ctx.Redirect("/users", 302)
ctx.Redirect("/user", 302)
}
}
func logoutHandler(ctx *Context) {
ctx.SetCookie(nil)
ctx.Redirect("/wiki/Index", 302)
ctx.Redirect(core.IndexURI, 302)
}

View file

@ -1,4 +1,4 @@
// Copyright (C) 2017 Marius Schellenberger
// Copyright (C) 2018 Marius Schellenberger
package server
@ -37,7 +37,7 @@ var routes = []route{
{
"/new",
authHandler(
wikiNewHandler),
pageNewHandler),
[]string{"GET", "POST"},
},
{
@ -46,55 +46,11 @@ var routes = []route{
[]string{"GET"},
},
{
"/wiki/{title:[a-zA-Z0-9+-]+$}",
wikiHandler,
[]string{"GET"},
},
{
"/wiki/{title:[a-zA-Z0-9+-]+}/md",
wikiMDHandler(
wikiHandler),
[]string{"GET"},
},
{
"/wiki/{title:[a-zA-Z0-9+-]+}/edit",
authHandler(
wikiEditHandler),
[]string{"GET", "POST"},
},
{
"/wiki/{title:[a-zA-Z0-9+-]+}/del",
authHandler(
wikiDelHandler),
[]string{"POST"},
},
{
"/users",
"/user",
adminAuthHandler(
usersHandler),
userHandler),
[]string{"GET"},
},
{
"/users/{user:[a-zA-Z0-9]+}/{title:[a-zA-Z0-9+-]+$}",
wikiHandler,
[]string{"GET"},
},
{
"/users/{user:[a-zA-Z0-9]+}/{title:[a-zA-Z0-9+-]+}/md",
wikiMDHandler(
wikiHandler),
[]string{"GET"},
},
{
"/users/{user:[a-zA-Z0-9]+}/{title:[a-zA-Z0-9+-]+}/edit",
wikiEditHandler,
[]string{"GET", "POST"},
},
{
"/users/{user:[a-zA-Z0-9]+}/{title:[a-zA-Z0-9+-]+}/del",
wikiDelHandler,
[]string{"POST"},
},
{
"/user/new",
adminAuthHandler(
@ -124,4 +80,43 @@ var routes = []route{
logoutHandler,
[]string{"GET", "POST"},
},
{
"/view/{key}",
pageHandler,
[]string{"GET"},
},
{
"/view/{key}/md",
pageMDHandler(
pageHandler),
[]string{"GET"},
},
{
"/{section}/{title}",
pageHandler,
[]string{"GET"},
},
{
"/{section}/{title}/md",
pageMDHandler(
pageHandler),
[]string{"GET"},
},
{
"/{section}/{title}/share",
pageShareHandler,
[]string{"GET"},
},
{
"/{section}/{title}/edit",
authHandler(
pageEditHandler),
[]string{"GET", "POST"},
},
{
"/{section}/{title}/del",
authHandler(
pageDelHandler),
[]string{"GET", "POST"},
},
}

View file

@ -1,6 +1,7 @@
package server
import (
"bytes"
"context"
"github.com/gorilla/mux"
"html/template"
@ -9,6 +10,7 @@ import (
"time"
"git.giftfish.de/ston1th/gowiki/pkg/db"
"git.giftfish.de/ston1th/gowiki/pkg/log"
"git.giftfish.de/ston1th/jwt"
)
@ -17,9 +19,10 @@ const (
)
type HTTPServer struct {
Version string
listener net.Listener
srv *http.Server
Version string
indexPath string
listener net.Listener
srv *http.Server
DB *db.DB
JWT *jwt.JWT
@ -28,14 +31,11 @@ type HTTPServer struct {
res map[string][]byte
}
func NewHTTPServer(l net.Listener, versin string, secCookie bool) (srv *HTTPServer) {
func NewHTTPServer(l net.Listener, version, indexPath string, secCookie bool) (srv *HTTPServer) {
srv = &HTTPServer{
Version: version,
SecCookie: secCookie,
listener: l,
JWT: jwt.New(time.Hour*12, nil, nil),
Version: version,
listener: l,
}
srv.cookieStore()
srv.loadTemplates()
srv.srv = &http.Server{
Handler: srv.buildRoutes(),
@ -44,11 +44,13 @@ func NewHTTPServer(l net.Listener, versin string, secCookie bool) (srv *HTTPServ
}
func (s *HTTPServer) Start() (err error) {
s.DB, err = db.New()
//TODO make secret configurable
buf := bytes.NewBufferString("be688838ca8686e5c90689bf2ab585cef1137c999b48c70b92f67a5c34dc15697b5d11c982ed6d71be1e1e7f7b4e0733884aa97c3f7a339a8ed03577cf74be09")
s.JWT, err = jwt.New(jwt.DefaultExpiry, jwt.NewMemBlacklist(), buf)
if err != nil {
return
}
s.Index, err = NewIndex(blevePath)
s.DB, err = db.New(s.indexPath)
if err != nil {
return
}
@ -59,8 +61,8 @@ func (s *HTTPServer) Start() (err error) {
}
func (s *HTTPServer) Stop() error {
s.BS.Close()
s.Index.Close()
s.DB.Close()
s.JWT.Stop()
ctx, cancel := context.WithTimeout(context.Background(), time.Second*5)
defer cancel()
return s.srv.Shutdown(ctx)

View file

@ -1,12 +1,13 @@
// Copyright (C) 2017 Marius Schellenberger
// Copyright (C) 2018 Marius Schellenberger
// This file is auto-generated by build/templates.sh
// This file is auto-generated by templates.sh
// using the contents of the templates directory
package server
import (
"errors"
"git.giftfish.de/ston1th/gowiki/pkg/log"
"html/template"
)
@ -21,7 +22,7 @@ const (
<ul type="circle">
{{if .Data}}
{{range $item := .Data}}
<li><a href="/wiki/{{$item.LinkTitle}}"><b>{{$item.Title}}</b></a></li>
<li><a href="/{{$item.StoreTitle}}"><b>{{$item.Title}}</b></a></li>
{{end}}
{{end}}
</ul>
@ -118,16 +119,13 @@ const (
Index
</a></li>
<li><a href="/all">
All Articles
All Pages
</a></li>
{{if .Login}}
<li><a href="/new">
New Article
New Page
</a></li>
{{end}}
<li><a href="/snippets">
Snippets
</a></li>
</ul>
<form class="navbar-form navbar-left menu-search-bar" role="search" method="post" action="/search">
<input type="hidden" name="token" value="{{.Token}}">
@ -143,7 +141,7 @@ const (
<ul class="nav navbar-nav navbar-right">
{{if .Login}}
{{if .Admin}}
<li><a href="/users">
<li><a href="/user">
Users
</a></li>
{{else}}
@ -177,6 +175,217 @@ const (
</div>
</div>
</div>
{{end}}`
pageDel = `{{define "body"}}
<div class="page-header">
<div class="row">
<div class="col-xs-4 col-xs-offset-4">
<div class="panel panel-danger">
<div class="panel-heading">
<h3 class="panel-title">Delete {{.Data}}</h3>
</div>
<form class="form-horizontal" action="/{{.Data}}/del" method="post">
<input type="hidden" name="token" value="{{.Token}}">
<button class="btn btn-sm btn-danger lg-pull-right" type="submit">Delete</button>
</form>
<div class="panel-body text-center">
<a href="/{{.Data}}" class="btn btn-sm btn-primary">Back</a>
</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}}">
<input type="hidden" name="store_title" value="{{if .Data}}{{.Data.StoreTitle}}{{end}}">
<fieldset>
<div class="form-group">
<label class="col-xs-1 control-label">Title</label>
<div class="col-xs-11">
<input class="form-control input-sm" type="text" value="{{if .Data}}{{.Data.Title}}{{end}}" spellcheck="false" disabled>
</div>
</div>
<div class="form-group">
<label class="col-xs-1 control-label">Markdown</label>
<div class="col-xs-11">
<textarea class="form-control input-sm md-text" rows="30" name="markdown" spellcheck="false" autofocus>{{if .Data}}{{.Data.Markdown}}{{end}}</textarea>
</div>
</div>
<div class="form-group">
<div class="col-xs-11">
<div class="custom-control custom-radio">
<input type="radio" id="perm1" name="perm" value="1" class="custom-control-input" checked="">
<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">
<label class="custom-control-label" for="perm2">Internal</label>
</div>
<div class="custom-control custom-radio">
<input type="radio" id="perm3" name="perm" value="3" class="custom-control-input">
<label class="custom-control-label" for="perm3">Private</label>
</div>
</div>
</div>
<div class="form-group">
<div class="col-xs-11 col-xs-offset-1">
<button class="btn btn-sm btn-primary" type="submit">Submit</button>
</div>
</div>
</fieldset>
</form>
{{end}}`
page = `{{define "body"}}
<div class="page-header">
<div class="row">
<h2>{{.BodyTitle}}</h2>
</div>
</div>
<div class="row">
<strong>Created by:</strong> {{.Data.Created}}<br>
{{if .Data.Updated}}
<strong>Updated by:</strong> {{.Data.Updated}}{{end}}
<br><br>
</div>
<div class="row">
<div class="col-xs-6">
{{.Data.PageIndex}}
</div>
<div class="col-xs-6">
{{if .Login}}
<div class="row" style="padding-bottom: 14px">
<div class="col-xs-6 {{if ne .Data.StoreTitle "wiki/Index"}}col-xs-offset-4{{else}}col-xs-offset-6{{end}}">
<a href="/{{.Data.StoreTitle}}/edit" class="btn btn-sm btn-primary lg-pull-right">Edit</a>
</div>
{{if ne .Data.StoreTitle "wiki/Index"}}
<div class="col-xs-2">
<a href="/{{.Data.StoreTitle}}/del" class="btn btn-sm btn-primary lg-pull-right">Delete</a>
</div>
{{end}}
</div>
{{end}}
<div class="row">
<div class="col-xs-6 col-xs-offset-6">
<a href="/{{.Data.StoreTitle}}/md" class="btn btn-sm btn-primary lg-pull-right">Markdown</a>
</div>
{{if .Login}}
<div class="col-xs-6 col-xs-offset-6">
<a href="/{{.Data.StoreTitle}}/share" class="btn btn-sm btn-primary lg-pull-right">Share</a>
</div>
{{end}}
</div>
</div>
</div>
{{.Data.HTML}}
{{end}}`
pageMD = `{{define "body"}}
<div class="page-header">
<div class="row">
<h2>{{.BodyTitle}}</h2>
</div>
</div>
<textarea class="form-control input-sm md-text" rows="30" spellcheck="false" autofocus>{{if .Data}}{{.Data.Markdown}}{{end}}</textarea>
{{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}}">
<fieldset>
<div class="form-group">
<label class="col-xs-1 control-label">Section</label>
<div class="col-xs-11">
<select class="form-control input-sm" name="section" autofocus required>
<option>wiki</option>
<option>{{.User}}</option>
</select>
</div>
<label class="col-xs-1 control-label">Title</label>
<div class="col-xs-11">
<input class="form-control input-sm" type="text" placeholder="Title" name="title" value="{{if .Data}}{{.Data.Title}}{{end}}" spellcheck="false" autofocus required>
</div>
</div>
<div class="form-group">
<label class="col-xs-1 control-label">Markdown</label>
<div class="col-xs-11">
<textarea class="form-control input-sm md-text" rows="30" name="markdown" spellcheck="false">{{if .Data}}{{.Data.Markdown}}{{end}}</textarea>
</div>
</div>
<div class="form-group">
<div class="col-xs-11">
<div class="custom-control custom-radio">
<input type="radio" id="perm1" name="perm" value="1" class="custom-control-input" checked="">
<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">
<label class="custom-control-label" for="perm2">Internal</label>
</div>
<div class="custom-control custom-radio">
<input type="radio" id="perm3" name="perm" value="3" class="custom-control-input">
<label class="custom-control-label" for="perm3">Private</label>
</div>
</div>
</div>
<div class="form-group">
<div class="col-xs-11 col-xs-offset-1">
<button class="btn btn-sm btn-primary" type="submit">Submit</button>
</div>
</div>
</fieldset>
</form>
{{end}}`
pageShare = `{{define "body"}}
<div class="page-header">
<div class="row">
<div class="col-xs-4 col-xs-offset-4">
<div class="panel panel-info">
<div class="panel-heading">
<h3 class="panel-title">Share {{.Data}}</h3>
</div>
<div class="panel-body text-center">
<a href="/view/{{.Key}}">Share Link</a>
</div>
</div>
</div>
</div>
</div>
{{end}}`
pageView = `{{define "body"}}
<div class="page-header">
<div class="row">
<h2>{{.BodyTitle}}</h2>
</div>
</div>
<div class="row">
<strong>Created by:</strong> {{.Data.Created}}<br>
{{if .Data.Updated}}
<strong>Updated by:</strong> {{.Data.Updated}}{{end}}
<br><br>
</div>
<div class="row">
<div class="col-xs-6">
{{.Data.PageIndex}}
</div>
<div class="col-xs-6">
<div class="row">
<div class="col-xs-6 col-xs-offset-6">
<a href="/view/{{.Key}}/md" class="btn btn-sm btn-primary lg-pull-right">Markdown</a>
</div>
</div>
</div>
</div>
{{.Data.HTML}}
{{end}}`
search = `{{define "body"}}
<div class="page-header">
@ -202,8 +411,9 @@ const (
</form>
</div>
<div class="row col-xs-12">
{{if .Data}}
{{.Data}}
{{range $item := .Data}}
<a href="/{{$item.StireTitle}}"><b>{{$item.Title}}</b></a>
<pre style="white-space:pre-wrap">{{$item.Text}}</pre><br>
{{end}}
</div>
{{end}}`
@ -294,48 +504,7 @@ const (
{{end}}
{{end}}
{{end}}`
userNew = `{{define "body"}}
<div class="page-header">
<div class="row">
<h2>{{.BodyTitle}}</h2>
</div>
</div>
<form class="form-horizontal" action="/user/new" method="post">
<input type="hidden" name="token" value="{{.Token}}">
<fieldset>
<div class="form-group">
<label class="col-lg-3 control-label">Username</label>
<div class="col-lg-6">
<input class="form-control input-sm" type="text" name="user" value required>
</div>
</div>
<div class="form-group">
<label class="col-lg-3 control-label">Password</label>
<div class="col-lg-6">
<input class="form-control input-sm" type="password" name="password" value required>
</div>
</div>
<div class="form-group">
<label class="col-lg-3 control-label">Repeat</label>
<div class="col-lg-6">
<input class="form-control input-sm" type="password" name="repeat" value required>
</div>
</div>
<div class="form-group">
<label class="col-lg-3 control-label">Admin Privileges</label>
<div class="col-lg-6">
<input type="checkbox" name="admin" value="0">
</div>
</div>
<div class="form-group">
<div class="col-lg-6 col-lg-offset-3">
<button class="btn btn-sm btn-primary" type="submit" name="submit" value="Submit">Submit</button>
</div>
</div>
</fieldset>
</form>
{{end}}`
users = `{{define "body"}}
user = `{{define "body"}}
<div class="page-header">
<div class="row">
<h2>{{.BodyTitle}}</h2>
@ -386,151 +555,45 @@ const (
{{end}}
</div>
{{end}}`
wikiEdit = `{{define "body"}}
{{if .Editor}}
<div class="alert alert-warning" style="margin-top:30px">
<h4>Warning!</h4>
<p><strong>{{.Editor.Username}}</strong> is editing this page since {{.Editor.Time}}</p>
</div>
{{end}}
userNew = `{{define "body"}}
<div class="page-header">
<div class="row">
<h2>{{.BodyTitle}}</h2>
</div>
</div>
<form class="form-horizontal" action="/wiki/{{.Data.LinkTitle}}/edit" method="post">
<input type="hidden" name="token" value="{{.Token}}">
<input type="hidden" name="title" value="{{if .Data}}{{.Data.Title}}{{end}}">
<fieldset>
<div class="form-group">
<label class="col-xs-1 control-label">Title</label>
<div class="col-xs-11">
<input class="form-control input-sm" type="text" value="{{if .Data}}{{.Data.Title}}{{end}}" spellcheck="false" disabled>
</div>
</div>
<div class="form-group">
<label class="col-xs-1 control-label">Text</label>
<div class="col-xs-11">
<textarea class="form-control input-sm md-text" rows="30" name="text" spellcheck="false" autofocus>{{if .Data}}{{.Data.MD}}{{end}}</textarea>
</div>
</div>
<div class="form-group">
<div class="col-xs-11">
<div class="custom-control custom-radio">
<input type="radio" id="perm1" name="perm" value="0" class="custom-control-input" checked="">
<label class="custom-control-label" for="perm1">Public</label>
</div>
<div class="custom-control custom-radio">
<input type="radio" id="perm2" name="perm" value="1" class="custom-control-input">
<label class="custom-control-label" for="perm2">Internal</label>
</div>
<div class="custom-control custom-radio">
<input type="radio" id="perm3" name="perm" value="2" class="custom-control-input">
<label class="custom-control-label" for="perm3">Private</label>
</div>
</div>
</div>
<div class="form-group">
<div class="col-xs-11 col-xs-offset-1">
<button class="btn btn-sm btn-primary" type="submit" name="submit" value="Submit">Submit</button>
<button class="btn btn-sm btn-default" type="submit" name="submit" value="Back">Back</button>
</div>
</div>
</fieldset>
</form>
{{end}}`
wiki = `{{define "body"}}
<div class="page-header">
<div class="row">
<h2>{{.BodyTitle}}</h2>
</div>
</div>
<div class="row">
<strong>Created by:</strong> {{.Data.Created}}<br>
{{if .Data.Updated}}
<strong>Updated by:</strong> {{.Data.Updated}}{{end}}
<br><br>
</div>
<div class="row">
<div class="col-xs-6">
{{.Data.Index}}
</div>
<div class="col-xs-6">
{{if .Login}}
<div class="row" style="padding-bottom: 14px">
<div class="col-xs-6 {{if ne .Data.LinkTitle "Index"}}col-xs-offset-4{{else}}col-xs-offset-6{{end}}">
<a href="/wiki/{{.Data.LinkTitle}}/edit" class="btn btn-sm btn-primary lg-pull-right">Edit</a>
</div>
{{if ne .Data.LinkTitle "Index"}}
<div class="col-xs-2">
<form class="form-horizontal" action="/wiki/{{.Data.LinkTitle}}/del" method="post">
<input type="hidden" name="token" value="{{.Token}}">
<button class="btn btn-sm btn-danger lg-pull-right" type="submit" name="submit" value="Delete">Delete</button>
</form>
</div>
{{end}}
</div>
{{end}}
<div class="row">
<div class="col-xs-6 col-xs-offset-6">
<a href="/wiki/{{.Data.LinkTitle}}/md" class="btn btn-sm btn-primary lg-pull-right">Markdown</a>
</div>
<form class="form-horizontal" action="/user/new" method="post">
<input type="hidden" name="token" value="{{.Token}}">
<fieldset>
<div class="form-group">
<label class="col-lg-3 control-label">Username</label>
<div class="col-lg-6">
<input class="form-control input-sm" type="text" name="user" value required>
</div>
</div>
</div>
{{.Data.Text}}
{{end}}`
wikiMD = `{{define "body"}}
<div class="page-header">
<div class="row">
<h2>{{.BodyTitle}}</h2>
<div class="form-group">
<label class="col-lg-3 control-label">Password</label>
<div class="col-lg-6">
<input class="form-control input-sm" type="password" name="password" value required>
</div>
</div>
</div>
<textarea class="form-control input-sm md-text" rows="30" spellcheck="false" autofocus>{{if .Data}}{{.Data.MD}}{{end}}</textarea>
{{end}}`
wikiNew = `{{define "body"}}
<div class="page-header">
<div class="row">
<h2>{{.BodyTitle}}</h2>
<div class="form-group">
<label class="col-lg-3 control-label">Repeat</label>
<div class="col-lg-6">
<input class="form-control input-sm" type="password" name="repeat" value required>
</div>
</div>
</div>
<form class="form-horizontal" action="/new" method="post">
<input type="hidden" name="token" value="{{.Token}}">
<fieldset>
<div class="form-group">
<label class="col-xs-1 control-label">Title</label>
<div class="col-xs-11">
<input class="form-control input-sm" type="text" placeholder="Title" name="title" value="{{if .Data}}{{.Data.Title}}{{end}}" spellcheck="false" autofocus required>
</div>
<div class="form-group">
<label class="col-lg-3 control-label">Admin Privileges</label>
<div class="col-lg-6">
<input type="checkbox" name="admin" value="0">
</div>
<div class="form-group">
<label class="col-xs-1 control-label">Text</label>
<div class="col-xs-11">
<textarea class="form-control input-sm md-text" rows="30" name="text" spellcheck="false">{{if .Data}}{{.Data.MD}}{{end}}</textarea>
</div>
</div>
<div class="form-group">
<div class="col-lg-6 col-lg-offset-3">
<button class="btn btn-sm btn-primary" type="submit" name="submit" value="Submit">Submit</button>
</div>
<div class="form-group">
<div class="col-xs-11">
<div class="custom-control custom-radio">
<input type="radio" id="perm1" name="perm" value="0" class="custom-control-input" checked="">
<label class="custom-control-label" for="perm1">Public</label>
</div>
<div class="custom-control custom-radio">
<input type="radio" id="perm2" name="perm" value="1" class="custom-control-input">
<label class="custom-control-label" for="perm2">Internal</label>
</div>
<div class="custom-control custom-radio">
<input type="radio" id="perm3" name="perm" value="2" class="custom-control-input">
<label class="custom-control-label" for="perm3">Private</label>
</div>
</div>
</div>
<div class="form-group">
<div class="col-xs-11 col-xs-offset-1">
<button class="btn btn-sm btn-primary" type="submit" name="submit" value="Submit">Submit</button>
</div>
</div>
</fieldset>
</div>
</fieldset>
</form>
{{end}}`
bootstrapCSS = `@import url("https://fonts.googleapis.com/css?family=Lato:400,700,400italic");/*!
@ -693,12 +756,15 @@ func (s *HTTPServer) loadTemplates() {
s.templ["loginHandler"] = parse(index, menu, login)
s.templ["searchHandler"] = parse(index, menu, search)
s.templ["allHandler"] = parse(index, menu, all)
s.templ["wikiNewHandler"] = parse(index, menu, wikiNew)
s.templ["wikiEditHandler"] = parse(index, menu, wikiEdit)
s.templ["wikiHandler"] = parse(index, menu, wiki)
s.templ["wikiMDHandler"] = parse(index, menu, wikiMD)
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["pageDelHandler"] = parse(index, menu, pageDel)
// users.go
s.templ["usersHandler"] = parse(index, menu, users)
s.templ["userHandler"] = parse(index, menu, user)
s.templ["userNewHandler"] = parse(index, menu, userNew)
s.templ["userEditHandler"] = parse(index, menu, userEdit)
s.templ["userDelHandler"] = parse(index, menu, userDel)

View file

@ -12,7 +12,7 @@ const keySize = jwt.KeySize / 2
func genKey(length int) (bytes []byte) {
bytes = make([]byte, length)
if _, err := io.ReadFull(rand.Reader, bytes); err != nil {
if _, err := rand.Reader.Read(bytes); err != nil {
log.Println("genKey:", err)
}
return
@ -31,13 +31,14 @@ func checkXsrf(xsrf string, secret []byte) bool {
return subtle.ConstantTimeCompare(secret, xor(t[keySize:], t[:keySize])) == 1
}
func xor(a, b []byte) []byte {
func xor(a, b []byte) (c []byte) {
n := len(a)
if len(b) != n {
if len(b) < n {
return nil
}
c = make([]byte, n)
for i := 0; i < n; i++ {
a[i] = a[i] ^ b[i]
c[i] = a[i] ^ b[i]
}
return a
return
}