upgraded bootstrap, fixed style, added share blacklisting

This commit is contained in:
ston1th 2018-09-14 20:49:50 +02:00
commit 3fb1cfc17d
37 changed files with 811 additions and 892 deletions

View file

@ -1,4 +1,4 @@
// Copyright (C) 2017 Marius Schellenberger
// Copyright (C) 2018 Marius Schellenberger
//go:generate ./templates.sh

6
pkg/cache/cache.go vendored
View file

@ -34,11 +34,13 @@ func (c *Cache) Add(title string, p *core.Page) {
c.Cache[title] = &CachedPage{p, now}
}
func (c *Cache) Get(title string) *core.Page {
func (c *Cache) Get(title string) (p *core.Page) {
c.RLock()
defer c.RUnlock()
if cp, ok := c.Cache[title]; ok {
return cp.Page
p = new(core.Page)
*p = *cp.Page
return
}
return nil
}

View file

@ -1,4 +1,4 @@
// Copyright (C) 2017 Marius Schellenberger
// Copyright (C) 2018 Marius Schellenberger
package cmd

View file

@ -1,4 +1,4 @@
// Copyright (C) 2017 Marius Schellenberger
// Copyright (C) 2018 Marius Schellenberger
package db

View file

@ -1,4 +1,4 @@
// Copyright (C) 2017 Marius Schellenberger
// Copyright (C) 2018 Marius Schellenberger
package db

View file

@ -7,6 +7,7 @@ import (
var reservedUsers = []string{
"all",
"blacklist",
"login",
"logout",
"new",

View file

@ -4,7 +4,6 @@ import (
"git.giftfish.de/ston1th/gowiki/pkg/core"
"git.giftfish.de/ston1th/gowiki/pkg/render"
"git.giftfish.de/ston1th/gowiki/pkg/store"
"git.giftfish.de/ston1th/gowiki/pkg/util"
"sort"
)
@ -65,7 +64,7 @@ func (db *DB) CreatePage(title, section, markdown, username string, p core.Permi
err = errPrivateWikiPage
return
}
st := util.StoreTitle(section, title)
st := render.StoreTitle(section, title)
if _, err = db.getPage(st, username); err != nil && err != store.ErrKeyNotFound {
return
}
@ -104,7 +103,7 @@ func (db *DB) UpdatePage(title, section, markdown, username string, p core.Permi
err = errPrivateWikiPage
return
}
st := util.StoreTitle(section, title)
st := render.StoreTitle(section, title)
page, err := db.getPage(st, username)
if err != nil {
return
@ -140,7 +139,7 @@ func (db *DB) DeletePage(title, section, username string) (err error) {
err = errDeleteIndexPage
return
}
st := util.StoreTitle(section, title)
st := render.StoreTitle(section, title)
if page, err = db.getPage(st, username); err != nil {
return
}

View file

@ -13,7 +13,7 @@ var (
errUserReserved = dbErr("username is reserved")
)
func (db *DB) GetUsers() (users core.Users, err error) {
func (db *DB) DumpUsers() (users core.Users, err error) {
db.users.ForEach(func(username string, _ []byte) error {
u, err := db.GetUser(username)
if err == nil {
@ -21,6 +21,14 @@ func (db *DB) GetUsers() (users core.Users, err error) {
}
return nil
})
return
}
func (db *DB) GetUsers() (users core.Users, err error) {
users, err = db.DumpUsers()
for i := range users {
users[i].Password = ""
}
sort.Sort(users)
return
}
@ -41,28 +49,7 @@ func (db *DB) UserExists(username string) error {
}
// TODO
/*func (bs *BoltStore) DumpUsers() (users []User, err error) {
var buf [][]byte
err = bs.db.View(func(tx *bolt.Tx) error {
return tx.Bucket([]byte(userTable)).ForEach(func(k, v []byte) error {
buf = append(buf, copyBuf(v))
return nil
})
})
if err != nil {
return
}
for _, b := range buf {
u := new(User)
if err = bs.Unmarshal(b, u); err != nil {
return
}
users = append(users, *u)
}
return
}
func (bs *BoltStore) RestoreUsers(users []User) error {
/*func (bs *BoltStore) RestoreUsers(users []User) error {
return bs.db.Update(func(tx *bolt.Tx) error {
b := tx.Bucket([]byte(userTable))
err := b.ForEach(func(k, v []byte) error {

View file

@ -1,4 +1,4 @@
// Copyright (C) 2017 Marius Schellenberger
// Copyright (C) 2018 Marius Schellenberger
package log

View file

@ -1,4 +1,4 @@
// Copyright (C) 2017 Marius Schellenberger
// Copyright (C) 2018 Marius Schellenberger
package render
@ -22,8 +22,9 @@ func makeFilter() analysis.CharFilter {
}
var (
reHeader = regexp.MustCompile("(<h[1-6])>(.+)(</h[1-6]>)")
reLink = regexp.MustCompile(`id="(.+)">`)
//reHeader = regexp.MustCompile("(<h[1-6])>(.+)(</h[1-6]>)")
reHeader = regexp.MustCompile("(<h[1-6]>)(.+)(</h[1-6]>)")
reLink = regexp.MustCompile(`id="(.+?)"`)
whiteSpace = regexp.MustCompile(`\s+`)
reIndex = regexp.MustCompile("<h([1-6])>(.+)</h[1-6]>")
titleRe = regexp.MustCompile("[^a-zA-Z0-9 -]+")
@ -42,13 +43,20 @@ func Render(p *core.Page) string {
func spaceReplace(s string) string {
return whiteSpace.ReplaceAllString(s, "+")
//TODO remove
//return strings.Replace(s, " ", "+", -1)
}
func makeLinkTitle(t string) (newTitle string, title string) {
title = titleRe.ReplaceAllString(t, "")
newTitle = spaceReplace(title)
func Title(title string) string {
return spaceReplace(titleRe.ReplaceAllString(title, ""))
}
func StoreTitle(section, title string) string {
return section + "/" + title
}
func UnstoreTitle(storeTitle string) (section, title string) {
if a := strings.Split(storeTitle, "/"); len(a) == 2 {
return a[0], a[1]
}
return
}
@ -56,10 +64,11 @@ func buildIndex(html string) (string, string) {
var ret string
m := make(map[string]int)
hn := reIndex.FindAllStringSubmatch(html, -1)
html = reHeader.ReplaceAllString(html, `${1} id="${2}">${2}${3}`)
//html = reHeader.ReplaceAllString(html, `${1} id="${2}">${2}${3}`)
html = reHeader.ReplaceAllString(html, `<a id="${2}" class="anchor"></a>${1}${2}${3}`)
html = reLink.ReplaceAllStringFunc(html, spaceReplace)
for k, v := range hn {
link, _ := makeLinkTitle(v[2])
link := Title(v[2])
i := m[link]
m[link] = i + 1
newLink := link + strconv.Itoa(i)
@ -119,11 +128,9 @@ func closeTag(j int) (ret string) {
}
func renderResult(res []core.Result) (html string) {
//TODO fmt.Sprintf ?
for _, r := range res {
html += `<a href="/` + r.StoreTitle + `"><b>` + r.Title + "</b></a>" +
`<pre style="white-space: pre-wrap">` + r.Text + "</pre><br>"
}
return
}

View file

@ -1,4 +1,4 @@
// Copyright (C) 2017 Marius Schellenberger
// Copyright (C) 2018 Marius Schellenberger
package server
@ -99,7 +99,7 @@ func (c *Context) Exec() {
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.Data.Time = time.Since(c.Time).Nanoseconds() / 1e5
c.Err = c.T.Execute(c.Response, c.Data)
if c.Err != nil {

View file

@ -1,11 +1,11 @@
// Copyright (C) 2017 Marius Schellenberger
// Copyright (C) 2018 Marius Schellenberger
package server
import (
"git.giftfish.de/ston1th/gowiki/pkg/core"
"git.giftfish.de/ston1th/gowiki/pkg/log"
"git.giftfish.de/ston1th/gowiki/pkg/util"
"git.giftfish.de/ston1th/gowiki/pkg/render"
"git.giftfish.de/ston1th/jwt"
"net/http"
"time"
@ -122,14 +122,15 @@ func searchHandler(ctx *Context) {
return
}
res, err := ctx.Srv.DB.Index.Search(search)
if err != nil {
ctx.Data.Msg = err.Error()
ctx.Exec()
return
}
//TODO
//res, err := ctx.Srv.DB.Index.Search(search)
//if err != nil {
// ctx.Data.Msg = err.Error()
// ctx.Exec()
// return
//}
//ctx.Data.Data = res
ctx.Data.Search = search
ctx.Data.Data = res
ctx.Exec()
}
}
@ -150,15 +151,17 @@ func pageNewHandler(ctx *Context) {
ctx.Data = webData{Title: "New Page"}
switch ctx.Method() {
case "GET":
ctx.Data.Data = core.Page{Perm: core.Public}
ctx.Exec()
case "POST":
section := ctx.Form("section")
title := ctx.Form("title")
title := render.Title(ctx.Form("title"))
markdown := ctx.Form("markdown")
perm := ctx.Form("perm")
perm := core.ParsePermString(ctx.Form("perm"))
p := core.Page{
Title: title,
Markdown: markdown,
Perm: perm,
}
ctx.Data.Data = p
if !ctx.CheckXsrf(nil) {
@ -167,7 +170,7 @@ func pageNewHandler(ctx *Context) {
if section != core.WikiSection {
section = ctx.User()
}
page, err := ctx.Srv.DB.CreatePage(title, section, markdown, ctx.User(), core.ParsePermString(perm))
page, err := ctx.Srv.DB.CreatePage(title, section, markdown, ctx.User(), perm)
if err != nil {
ctx.Data.Msg = err.Error()
ctx.Exec()
@ -208,7 +211,7 @@ func pageHandler(ctx *Context) {
ctx.Template("pageHandler")
}
}
page, err := ctx.Srv.DB.GetPage(util.StoreTitle(section, title), user)
page, err := ctx.Srv.DB.GetPage(render.StoreTitle(section, title), user)
if err != nil {
log.Println(err)
ctx.NotFound()
@ -227,24 +230,40 @@ func pageMDHandler(f ctxHandler) ctxHandler {
}
}
// TODO add share token blacklisting
func pageShareHandler(ctx *Context) {
ctx.Template("pageShareHandler")
ctx.Data = webData{}
section := ctx.Var("section")
title := ctx.Var("title")
title := render.Title(ctx.Var("title"))
user := ctx.User()
st := util.StoreTitle(section, title)
st := render.StoreTitle(section, title)
page, err := ctx.Srv.DB.GetPage(st, user)
if err != nil {
ctx.NotFound()
return
}
ctx.Data.Data = st
ctx.Data.Title = page.Title
ctx.Data.BodyTitle = page.Title
switch ctx.Method() {
case "GET":
ctx.Exec()
case "POST":
duration := ctx.Form("duration")
if !ctx.CheckXsrf(nil) {
return
}
d, err := time.ParseDuration(duration)
if err != nil {
ctx.Data.Msg = err.Error()
ctx.Exec()
return
}
t := jwt.NewToken(map[string]interface{}{
sharedClaim: user,
sectionClaim: section,
titleClaim: title,
jwt.ExpClaim: jwt.NewExp(time.Hour * 24),
jwt.ExpClaim: jwt.NewExp(d),
}, nil)
if err = ctx.Srv.JWT.Sign(t); err != nil {
@ -252,66 +271,94 @@ func pageShareHandler(ctx *Context) {
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 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(nil) {
return
}
t, err := jwt.DecodeToken(ctx.Form("share"))
if err != nil {
ctx.Data.Msg = err.Error()
ctx.Exec()
return
}
err = ctx.Srv.JWT.Verify(t)
if err != nil {
ctx.Data.Msg = err.Error()
ctx.Exec()
return
}
user := t.Claims.GetString(sharedClaim)
if user == "" {
ctx.Data.Msg = "blacklisting denied: no share token"
ctx.Exec()
return
}
if ctx.User() != user {
ctx.Data.Msg = "blacklisting denied: token was issued by " + user
ctx.Exec()
return
}
err = ctx.Srv.JWT.Invalidate(t)
if err != nil {
ctx.Data.Msg = err.Error()
ctx.Exec()
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(util.StoreTitle(section, title), ctx.User())
title := render.Title(ctx.Var("title"))
page, err := ctx.Srv.DB.GetPage(render.StoreTitle(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.Data.Title = page.Title
ctx.Data.BodyTitle = page.Title
ctx.Exec()
case "POST":
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: page.Title,
StoreTitle: storeTitle,
Markdown: markdown,
Perm: perm,
}
ctx.Data.Title = page.Title
ctx.Data.BodyTitle = page.Title
page.Markdown = ctx.Form("markdown")
page.Perm = core.ParsePermString(ctx.Form("perm"))
ctx.Data.Data = page
if !ctx.CheckXsrf(nil) {
return
}
err = ctx.Srv.DB.UpdatePage(title, section, markdown, ctx.User(), perm)
err = ctx.Srv.DB.UpdatePage(title, section, page.Markdown, ctx.User(), page.Perm)
if err != nil {
ctx.Data.Msg = err.Error()
ctx.Exec()
return
}
ctx.Redirect("/"+page.StoreTitle, 302)
ctx.Redirect(page.StoreTitle, 302)
}
}
func pageDelHandler(ctx *Context) {
section := ctx.Var("section")
title := ctx.Var("title")
st := util.StoreTitle(section, title)
title := render.Title(ctx.Var("title"))
st := render.StoreTitle(section, title)
ctx.Template("pageDelHandler")
ctx.Data.Data = st
ctx.Data.Title = title
@ -473,6 +520,7 @@ func userDelHandler(ctx *Context) {
}
func logoutHandler(ctx *Context) {
ctx.Srv.JWT.Invalidate(&ctx.Token)
ctx.SetCookie(nil)
ctx.Redirect(core.IndexURI, 302)
}

View file

@ -45,6 +45,12 @@ var routes = []route{
allHandler,
[]string{"GET"},
},
{
"/blacklist",
authHandler(
pageBlacklistHandler),
[]string{"GET", "POST"},
},
{
"/user",
adminAuthHandler(
@ -105,7 +111,7 @@ var routes = []route{
{
"/{section}/{title}/share",
pageShareHandler,
[]string{"GET"},
[]string{"GET", "POST"},
},
{
"/{section}/{title}/edit",

File diff suppressed because one or more lines are too long

View file

@ -1,47 +0,0 @@
package util
import (
"fmt"
"strconv"
"strings"
"time"
)
const timeFmt = "2006-01-02 15:04:05"
func now() string {
return time.Now().Format(timeFmt)
}
func since(t time.Time) string {
s := time.Since(t)
u := uint64(s)
if u <= 0 {
return "0s"
}
str := s.String()
switch {
case u >= uint64(time.Second):
f, _ := strconv.ParseFloat(str[:len(str)-1], 64)
return fmt.Sprintf("%.fs", f)
case u < uint64(time.Microsecond):
f, _ := strconv.ParseFloat(str[:len(str)-2], 64)
return fmt.Sprintf("%.fns", f)
case u < uint64(time.Millisecond):
f, _ := strconv.ParseFloat(str[:len(str)-3], 64)
return fmt.Sprintf("%.fµs", f)
default:
f, _ := strconv.ParseFloat(str[:len(str)-2], 64)
return fmt.Sprintf("%.fms", f)
}
}
func StoreTitle(section, title string) string {
return section + "/" + title
}
func UnstoreTitle(storeTitle string) (section, title string) {
if a := strings.Split(storeTitle, "/"); len(a) == 2 {
return a[0], a[1]
}
return
}

2
sri.sh
View file

@ -1,5 +1,5 @@
#!/bin/bash
# Copyright (C) 2017 Marius Schellenberger
# Copyright (C) 2018 Marius Schellenberger
HASH="sha256"
DIR="templates"

View file

@ -79,6 +79,7 @@ func (s *HTTPServer) loadTemplates() {
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)
// users.go
s.templ["userHandler"] = parse(index, menu, user)

View file

@ -4,13 +4,11 @@
<h2>{{.BodyTitle}}</h2>
</div>
</div>
<div class="row">
<div class="row mt-5">
<ul type="circle">
{{if .Data}}
{{range $item := .Data}}
<li><a href="/{{$item.StoreTitle}}"><b>{{$item.Title}}</b></a></li>
{{end}}
{{end}}
</ul>
</div>
{{end}}

File diff suppressed because one or more lines are too long

View file

@ -30,6 +30,12 @@ a.para {
text-decoration: none;
font-size: 18px;
}
a.anchor {
display: block;
position: relative;
top: -70px;
visibility: hidden;
}
.alert-primary {
background-color: #375a7f;
border-color: #375a7f;
@ -84,7 +90,7 @@ code {
background-color: #444444;
border-radius: 2px;
}
textarea,input {
textarea,input,select {
color: #e2e2e2 !important;
background-color: #444444 !important;
}

View file

@ -2,23 +2,19 @@
<html>
<head>
<title>GoWiki | {{.Title}}</title>
<link rel="stylesheet" type="text/css" href="/bootstrap.css" media="screen" integrity="sha256-KC5lAzGRWwscU0sTmXtd8ka5mt7Vk8a0L5JqOhwA28s="></link>
<link rel="stylesheet" type="text/css" href="/custom.css" media="screen" integrity="sha256-gOyQhDAZKjv7dAP8YCFkpN+Hgg7JAfK8pZsZz4+bqXA="></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-18JBUnR8x0NOdgbyBI/2x1NPN5IZvpdHdKTeGR2PLuA="></link>
</head>
<body>
<div class="navbar navbar-default navbar-fixed-top">
<body style="padding-top:60px">
<div class="navbar navbar-expand-lg fixed-top navbar-dark bg-primary">
<div class="container">
<div class="navbar-header">
<a href="/" class="navbar-brand">GoWiki</a>
</div>
<div class="navbar-collapse collapse">
<div class="collapse navbar-collapse">
{{template "menu" .}}
</div>
</div>
</div>
<div class="container">
<div class="content">
<div class="wrapper">
<div id="back-to-top" class="proper-content">
{{if .Msg}}
<div class="alert alert-danger" style="margin-top:30px">
@ -29,20 +25,19 @@
{{template "body" .}}
</div>
<div class="push"></div>
</div>
<div class="footer-wrapper">
<footer>
<div class="row">
<div class="col-lg-12">
<ul class="list-unstyled">
<li class="pull-right"><a href="#back-to-top">Back to top</a></li>
<li>© 2017 GoWiki {{.Version}} Request: <strong>{{.Time}}ms</strong></li>
<li class="float-lg-right"><a href="#back-to-top">Back to top</a></li>
{{if .Login}}
<li><a href="/blacklist">Blacklist</a></li>
{{end}}
</ul>
<p>© 2018 GoWiki {{.Version}} Request: <strong>{{.Time}}ms</strong></p>
</div>
</div>
</footer>
</div>
</div>
</div>
</body>
</html>

View file

@ -1,33 +1,23 @@
{{define "body"}}
<div class="page-header">
<div class="row">
<div class="col-xs-6 col-xs-offset-3">
<div class="panel panel-default">
<div class="panel-heading">
<h3 class="panel-title">{{.BodyTitle}}</h3>
<div class="col-xs-4 offset-4">
<div class="card border-primary mx-auto">
<div class="card-header">
<strong>{{.BodyTitle}}</strong>
</div>
<div class="panel-body">
<div class="card-body">
<form class="form-horizontal" action="/login" method="post">
<input type="hidden" name="token" value="{{.Token}}">
<fieldset>
<div class="form-group">
<label class="col-lg-4 control-label">Username</label>
<div class="col-lg-6">
<input class="form-control input-sm" type="text" name="user" autofocus required>
</div>
<label class="col-form-label" for="user">Username</label>
<input class="form-control input-sm" type="text" id="user" name="user" autofocus required>
</div>
<div class="form-group">
<label class="col-lg-4 control-label">Password</label>
<div class="col-lg-6">
<input class="form-control input-sm" type="password" name="password" required>
<label class="col-form-label" for="password">Password</label>
<input class="form-control input-sm" type="text" id="password" name="password" autofocus required>
</div>
</div>
<div class="form-group">
<div class="col-lg-6 col-lg-offset-4">
<button class="btn btn-sm btn-primary" type="submit" name="submit" value="Login">Login</button>
</div>
</div>
</fieldset>
<button class="btn btn-sm btn-primary" type="submit">Login</button>
</form>
</div>
</div>

View file

@ -1,46 +1,26 @@
{{define "menu"}}
<ul class="nav navbar-nav">
<li><a href="/wiki/Index">
Index
</a></li>
<li><a href="/all">
All Pages
</a></li>
<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="/all">All Pages</a></li>
{{if .Login}}
<li><a href="/new">
New Page
</a></li>
<li class="nav-item"><a class="nav-link" href="/new">New Page</a></li>
{{end}}
</ul>
<form class="navbar-form navbar-left menu-search-bar" role="search" method="post" action="/search">
<form class="form-inline my2 my-lg-0" method="post" action="/search">
<input type="hidden" name="token" value="{{.Token}}">
<div class="form-group">
<div class="input-group">
<input type="text" class="form-control input-sm menu-search-input" placeholder="Search" name="search" value="{{.Search}}">
<span class="input-group-btn">
<button class="btn btn-sm btn-info" type="submit" name="submit" value="Search">Search</button>
</span>
</div>
</div>
<input type="text" class="form-control mr-sm-2" placeholder="Search" name="search" value="{{.Search}}" autocomplete="off">
<button class="btn btn-sm btn-info" type="submit">Search</button>
</form>
<ul class="nav navbar-nav navbar-right">
<ul class="nav navbar-nav ml-auto">
{{if .Login}}
{{if .Admin}}
<li><a href="/user">
Users
</a></li>
<li class="nav-item"><a class="nav-link" href="/user">Users</a></li>
{{else}}
<li><a href="/user/edit/{{.User}}">
Profile
</a></li>
<li class="nav-item"><a class="nav-link" href="/user/edit/{{.User}}">Profile</a></li>
{{end}}
<li><a href="/logout">
Logout
</a></li>
<li class="nav-item"><a class="nav-link" href="/logout">Logout</a></li>
{{else}}
<li><a href="/login">
Login
</a></li>
<li class="nav-item"><a class="nav-link" href="/login">Login</a></li>
{{end}}
</ul>
{{end}}

View file

@ -1,14 +1,14 @@
{{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">404</h3>
<div class="col-xs-4 offset-4">
<div class="card border-danger mx-auto">
<div class="card-header">
<strong>404</strong>
</div>
<div class="panel-body text-center">
<div class="card-body">
<h4>Page not found</h4>
<a href="/"><b>Back</b></a>
<a href="/" class="btn btn-sm btn-primary">Back</a>
</div>
</div>
</div>

View file

@ -4,37 +4,28 @@
<h2>{{.BodyTitle}}</h2>
</div>
</div>
<div class="row">
<div class="row mt-3">
<div class="col-lg-12">
<strong>Created by:</strong> {{.Data.Created}}<br>
{{if .Data.Updated}}
<strong>Updated by:</strong> {{.Data.Updated}}{{end}}
<strong>Updated by:</strong> {{.Data.Updated}}
<br><br>
{{end}}
</div>
</div>
<div class="row">
<div class="col-xs-6">
<div class="row mt-5">
<div class="col-lg-6">
{{.Data.PageIndex}}
</div>
<div class="col-xs-6">
<div class="col-lg-6">
<div class="btn-group float-lg-right">
<a href="/{{.Data.StoreTitle}}/md" class="btn btn-sm btn-primary">Markdown</a>
{{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>
<a href="/{{.Data.StoreTitle}}/edit" class="btn btn-sm btn-primary">Edit</a>
<a href="/{{.Data.StoreTitle}}/share" class="btn btn-sm btn-primary">Share</a>
{{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>
<a href="/{{.Data.StoreTitle}}/del" class="btn btn-sm btn-primary">Delete</a>
{{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>

View file

@ -0,0 +1,23 @@
{{define "body"}}
<div class="page-header">
<div class="row">
<div class="col-xs-4 offset-4">
<div class="card border-danger mx-auto">
<div class="card-header">
<strong>{{.BodyTitle}}</strong>
</div>
<div class="card-body">
<form class="form-horizontal" action="/blacklist" method="post">
<input type="hidden" name="token" value="{{.Token}}">
<div class="form-group">
<label class="col-form-label" for="token">Token</label>
<input class="form-control input-sm" type="text" id="token" name="share" autocomplete="off" autofocus required>
</div>
<button class="btn btn-sm btn-danger" type="submit">Blacklist</button>
</form>
</div>
</div>
</div>
</div>
</div>
{{end}}

View file

@ -1,17 +1,17 @@
{{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 class="col-xs-4 offset-4">
<div class="card border-danger mx-auto">
<div class="card-header">
<strong>Delete {{.Data}}</strong>
</div>
<div class="card-body">
<form class="form-horizontal" action="/{{.Data}}/del" method="post">
<input type="hidden" name="token" value="{{.Token}}">
<button class="btn btn-sm btn-danger lg-pull-right" type="submit">Delete</button>
</form>
<div class="panel-body text-center">
<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>

View file

@ -6,41 +6,28 @@
</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>
<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-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>
<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="col-xs-11">
<div class="custom-control custom-radio">
<input type="radio" id="perm1" name="perm" value="1" class="custom-control-input" checked="">
<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">
<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">Internal</label>
</div>
<div class="custom-control custom-radio">
<input type="radio" id="perm3" name="perm" value="3" class="custom-control-input">
<input type="radio" id="perm3" name="perm" value="3" class="custom-control-input" {{if eq .Data.Perm 3}}checked=""{{end}}>
<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}}

View file

@ -4,5 +4,7 @@
<h2>{{.BodyTitle}}</h2>
</div>
</div>
<textarea class="form-control input-sm md-text" rows="30" spellcheck="false" autofocus>{{if .Data}}{{.Data.Markdown}}{{end}}</textarea>
<div class="row mt-3">
<textarea class="form-control input-sm md-text" rows="30" spellcheck="false" autofocus>{{.Data.Markdown}}</textarea>
</div>
{{end}}

View file

@ -6,47 +6,35 @@
</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>
<label class="col-form-label" for="section">Section</label>
<select class="form-control input-sm" id="section" 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 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.Title}}" spellcheck="false" autocomplete="off" autofocus required>
</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>
<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="col-xs-11">
<div class="custom-control custom-radio">
<input type="radio" id="perm1" name="perm" value="1" class="custom-control-input" checked="">
<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">
<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">Internal</label>
</div>
<div class="custom-control custom-radio">
<input type="radio" id="perm3" name="perm" value="3" class="custom-control-input">
<input type="radio" id="perm3" name="perm" value="3" class="custom-control-input" {{if eq .Data.Perm 3}}checked=""{{end}}>
<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}}

View file

@ -1,13 +1,24 @@
{{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 class="col-xs-4 offset-4">
<div class="card border-primary mx-auto">
<div class="card-header">
<strong>Share {{.Data}}</strong>
</div>
<div class="panel-body text-center">
<div class="card-body">
{{if .Key}}
<a href="/view/{{.Key}}">Share Link</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" autofocus required>
</div>
<button class="btn btn-sm btn-primary" type="submit">Share</button>
</form>
{{end}}
</div>
</div>
</div>

View file

@ -4,21 +4,22 @@
<h2>{{.BodyTitle}}</h2>
</div>
</div>
<div class="row">
<div class="row mt-3">
<div class="col-lg-12">
<strong>Created by:</strong> {{.Data.Created}}<br>
{{if .Data.Updated}}
<strong>Updated by:</strong> {{.Data.Updated}}{{end}}
<strong>Updated by:</strong> {{.Data.Updated}}
<br><br>
{{end}}
</div>
</div>
<div class="row">
<div class="col-xs-6">
<div class="row mt-5">
<div class="col-lg-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 class="col-lg-6">
<div class="btn-group float-lg-right">
<a href="/view/{{.Key}}/md" class="btn btn-sm btn-primary">Markdown</a>
</div>
</div>
</div>

View file

@ -4,27 +4,27 @@
<h2>{{.BodyTitle}}</h2>
</div>
</div>
<div class="row col-xs-6">
<form class="form-horizontal" role="search" method="post" action="/search">
<div class="row mt-3">
<div class="col-xs-6">
<form class="form-horizontal" action="/search" method="post">
<input type="hidden" name="token" value="{{.Token}}">
<fieldset>
<div class="form-group">
<div class="col-xs-8">
<div class="input-group">
<input type="text" class="form-control input-sm" placeholder="Search" name="search" value="{{.Search}}" autofocus>
<span class="input-group-btn">
<button class="btn btn-sm btn-info" type="submit" name="submit" value="Search">Search</button>
</span>
<input type="text" class="form-control input-sm" placeholder="Search" name="search" value="{{.Search}}" autocomplete="off" autofocus>
<div class="input-group-append">
<button class="btn btn-sm btn-primary" type="submit">Search</button>
</div>
</div>
</div>
</fieldset>
</form>
</div>
</div>
<div class="row col-xs-12">
{{range $item := .Data}}
<a href="/{{$item.StireTitle}}"><b>{{$item.Title}}</b></a>
<pre style="white-space:pre-wrap">{{$item.Text}}</pre><br>
{{end}}
<div class="row mt-3">
<div class="col-lg-12">
{{range $item := .Data}}
<a href="/{{$item.StoreTitle}}"><b>{{$item.Title}}</b></a>
<pre style="white-space:pre-wrap">{{$item.Text}}</pre><br>
{{end}}
</div>
</div>
{{end}}

View file

@ -5,15 +5,16 @@
</div>
</div>
{{if .Admin}}
<div class="row">
<div class="row mb-3">
<div class="col-lg-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-lg-12">
{{if .Data}}
<table class="table table-stripped table-hover">
<table class="table table-hover">
<thead>
<tr>
<th>Username</th>
@ -27,25 +28,24 @@
{{range $item := .Data}}
<tr>
<td>{{$item.Username}}</td>
{{if $item.Admin}}
<td>yes</td>
{{else}}
<td>no</td>
{{end}}
{{if $item.Admin}}<td>yes</td>{{else}}<td>no</td>{{end}}
<td>
<div class="btn-group">
{{if $admin}}
<td><a class="btn btn-xs btn-primary" href="/user/edit/{{$item.Username}}">Edit</a>&nbsp;
<a class="btn btn-xs btn-danger" href="/user/del/{{$item.Username}}">Delete</a></td>
<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}}
<td><a class="btn btn-xs btn-primary" href="/user/edit/{{$item.Username}}">Edit</a></td>
{{else}}
<td></td>
<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}}

View file

@ -1,19 +1,23 @@
{{define "body"}}
{{if .Data}}
<div class="page-header">
<div class="row">
<h2>{{.BodyTitle}}</h2>
<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 <a href="/user/edit/{{.Data.Username}}">{{.Data.Username}}</a>.<br>This will also remove all private 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="/user" class="btn btn-sm btn-primary">Back</a>
</form>
</div>
</div>
</div>
</div>
</div>
{{if .Data}}
<div class="alert alert-danger">
<h4>Delete?</h4>
<p>You are about to delete the user <a class="dark" href="/user/edit/{{.Data.Username}}">{{.Data.Username}}</a></p>
</div>
<form class="form-horizontal" action="/user/del/{{.Data.Username}}" method="post">
<input type="hidden" name="token" value="{{.Token}}">
<fieldset>
<button class="btn btn-sm btn-danger" type="submit" name="submit" value="Delete">Delete</button>
</fieldset>
</form>
{{end}}
{{end}}

View file

@ -1,68 +1,56 @@
{{define "body"}}
<div class="page-header">
<div class="row">
<h2>{{.BodyTitle}}</h2>
</div>
</div>
{{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>
</div>
<div class="card-body">
<form class="form-horizontal" action="/user/edit/{{.Data.Username}}" 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" value="{{.Data.Username}}" disabled>
</div>
<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-lg-3 control-label">Password</label>
<div class="col-lg-6">
<input class="form-control input-sm" type="password" name="password">
</div>
<label class="col-form-label" for="password">Password</label>
<input class="form-control input-sm" type="password" id="password" name="password" autofocus>
</div>
<div class="form-group">
<label class="col-lg-3 control-label">Repeat</label>
<div class="col-lg-6">
<input class="form-control input-sm" type="password" name="repeat">
</div>
<label class="col-form-label" for="repeat">Repeat</label>
<input class="form-control input-sm" type="password" id="repeat" name="repeat" autofocus>
</div>
{{if .Admin}}
<div class="form-group">
<label class="col-lg-3 control-label">Admin Privileges</label>
<div class="col-lg-6">
<label class="col-form-label" for="admin">Admin Privileges</label>
{{if .Data.Admin}}
<input type="checkbox" name="admin" value="0" checked>
<input type="checkbox" id="admin" name="admin" value="0" checked>
{{else}}
<input type="checkbox" name="admin" value="0">
<input type="checkbox" id="admin" name="admin" value="0">
{{end}}
</div>
</div>
{{end}}
<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>
<button class="btn btn-sm btn-primary" type="submit">Submit</button>
{{if .Admin}}
<a href="/user" class="btn btn-sm btn-primary">Back</a>
{{else}}
<a href="/" class="btn btn-sm btn-primary">Back</a>
{{end}}
</form>
</div>
{{if .Admin}}
{{if eq .Data.Locked 3}}
<div class="row">
{{if .Admin}}
{{if eq .Data.Locked 3}}
<form class="form-horizontal" action="/user/unlock/{{.Data.Username}}" method="post">
<fieldset>
<input type="hidden" name="token" value="{{.Token}}">
<div class="form-group">
<label class="col-lg-3 control-label">User is locked</label>
<div class="col-lg-6">
<button class="btn btn-sm btn-primary" type="submit" name="submit" value="Unlock">Unlock</button>
<label class="col-form-label" for="unlock">Admin Privileges</label>
<button class="btn btn-sm btn-primary" type="submit" id="unlock">Unlock</button>
</div>
</div>
</fieldset>
</form>
{{end}}
{{end}}
</div>
</div>
</div>
{{end}}
{{end}}
{{end}}
{{end}}

View file

@ -1,41 +1,33 @@
{{define "body"}}
<div class="page-header">
<div class="row">
<h2>{{.BodyTitle}}</h2>
<div class="row">
<div class="col-xs-4 offset-4">
<div class="card border-primary mx-auto">
<div class="card-header">
<strong>{{.BodyTitle}}</strong>
</div>
<div class="card-body">
<form class="form-horizontal" action="/user/new" method="post">
<input type="hidden" name="token" value="{{.Token}}">
<div class="form-group">
<label class="col-form-label" for="user">Username</label>
<input class="form-control input-sm" type="text" id="user" name="user" autocomplete="off" autofocus required>
</div>
<div class="form-group">
<label class="col-form-label" for="password">Password</label>
<input class="form-control input-sm" type="password" id="password" name="password" autofocus 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" autofocus 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">Submit</button>
<a href="/user" class="btn btn-sm btn-primary">Back</a>
</form>
</div>
</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}}