general improvements
This commit is contained in:
parent
bac08dc7cf
commit
ddb11f0619
25 changed files with 305 additions and 140 deletions
|
|
@ -28,27 +28,27 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
conf core.Config
|
config core.Config
|
||||||
dumpFile string
|
dumpFile string
|
||||||
config string
|
configFile string
|
||||||
)
|
)
|
||||||
|
|
||||||
func initServer(conf core.Config) (err error) {
|
func initServer(cfg core.Config) (err error) {
|
||||||
if err = godrop.PledgePromises("stdio rpath wpath cpath inet fattr flock proc exec id unveil"); err != nil {
|
if err = godrop.PledgePromises("stdio rpath wpath cpath inet fattr flock proc exec id unveil"); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
dropCfg := godrop.Config{
|
dropCfg := godrop.Config{
|
||||||
User: conf.User,
|
User: cfg.User,
|
||||||
Group: conf.Group,
|
Group: cfg.Group,
|
||||||
Foreground: conf.Foreground,
|
Foreground: cfg.Foreground,
|
||||||
}
|
}
|
||||||
err = godrop.Drop(dropCfg, func() (net.Listener, error) {
|
err = godrop.Drop(dropCfg, func() (net.Listener, error) {
|
||||||
return net.Listen("tcp", conf.ListenAddr)
|
return net.Listen("tcp", cfg.ListenAddr)
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = os.Chdir(conf.DataDir); err != nil {
|
if err = os.Chdir(cfg.DataDir); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -61,14 +61,14 @@ func initServer(conf core.Config) (err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.InitLogger(conf.DataDir, conf.LogFile, conf.Debug)
|
log.InitLogger(cfg)
|
||||||
|
|
||||||
srv := server.NewHTTPServer(l, conf)
|
srv := server.NewHTTPServer(cfg, l)
|
||||||
err = srv.Start()
|
err = srv.Start()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Println("gowiki " + conf.Version + " started")
|
log.Println("gowiki " + cfg.Version + " started")
|
||||||
sigs := make(chan os.Signal)
|
sigs := make(chan os.Signal)
|
||||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||||
sig := <-sigs
|
sig := <-sigs
|
||||||
|
|
@ -86,14 +86,14 @@ func initServer(conf core.Config) (err error) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadConfig() error {
|
func loadConfig() error {
|
||||||
if config == "" {
|
if configFile == "" {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
file, err := os.Open(config)
|
file, err := os.Open(configFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = json.NewDecoder(file).Decode(&conf)
|
err = json.NewDecoder(file).Decode(&config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
@ -114,8 +114,8 @@ func Run(version string) {
|
||||||
if err := loadConfig(); err != nil {
|
if err := loadConfig(); err != nil {
|
||||||
stdlog.Fatal("server: ", err)
|
stdlog.Fatal("server: ", err)
|
||||||
}
|
}
|
||||||
conf.Version = app.Version
|
config.Version = app.Version
|
||||||
if err := initServer(conf); err != nil {
|
if err := initServer(config); err != nil {
|
||||||
stdlog.Fatal("server: ", err)
|
stdlog.Fatal("server: ", err)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -139,7 +139,7 @@ func Run(version string) {
|
||||||
stdlog.Fatal("dump: ", err)
|
stdlog.Fatal("dump: ", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DB, err := db.New(conf.DataDir)
|
DB, err := db.New(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
stdlog.Fatal("dump: ", err)
|
stdlog.Fatal("dump: ", err)
|
||||||
}
|
}
|
||||||
|
|
@ -172,7 +172,7 @@ func Run(version string) {
|
||||||
stdlog.Fatal("restore: ", err)
|
stdlog.Fatal("restore: ", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
DB, err := db.New(conf.DataDir)
|
DB, err := db.New(config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
stdlog.Fatal("restore: ", err)
|
stdlog.Fatal("restore: ", err)
|
||||||
}
|
}
|
||||||
|
|
@ -197,45 +197,50 @@ func serverFlags() []cli.Flag {
|
||||||
Name: "user, u",
|
Name: "user, u",
|
||||||
Value: defRunUser,
|
Value: defRunUser,
|
||||||
Usage: "drop privileges to user",
|
Usage: "drop privileges to user",
|
||||||
Destination: &conf.User,
|
Destination: &config.User,
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "group, g",
|
Name: "group, g",
|
||||||
Value: defRunGroup,
|
Value: defRunGroup,
|
||||||
Usage: "drop privileges to group",
|
Usage: "drop privileges to group",
|
||||||
Destination: &conf.Group,
|
Destination: &config.Group,
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "listen, l",
|
Name: "listen, l",
|
||||||
Value: defListen,
|
Value: defListen,
|
||||||
Usage: "listening <address>:<port>",
|
Usage: "listening <address>:<port>",
|
||||||
Destination: &conf.ListenAddr,
|
Destination: &config.ListenAddr,
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "log",
|
Name: "log",
|
||||||
Value: defLogFile,
|
Value: defLogFile,
|
||||||
Usage: "log file (use - for stdout, this only works in combination with -f)",
|
Usage: "log file (use - for stdout, this only works in combination with -f)",
|
||||||
Destination: &conf.LogFile,
|
Destination: &config.LogFile,
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "secret",
|
Name: "secret",
|
||||||
Usage: "static hmac secret (at least 64 chars, used for JWT signing)",
|
Usage: "static hmac secret (at least 64 chars, used for JWT signing)",
|
||||||
Destination: &conf.Secret,
|
Destination: &config.Secret,
|
||||||
|
},
|
||||||
|
cli.BoolTFlag{
|
||||||
|
Name: "admin, a",
|
||||||
|
Usage: "use -a=false to disable the admin user",
|
||||||
|
Destination: &config.Admin,
|
||||||
},
|
},
|
||||||
cli.BoolFlag{
|
cli.BoolFlag{
|
||||||
Name: "foreground, f",
|
Name: "foreground, f",
|
||||||
Usage: "do not fork into the background",
|
Usage: "do not fork into the background",
|
||||||
Destination: &conf.Foreground,
|
Destination: &config.Foreground,
|
||||||
},
|
},
|
||||||
cli.BoolFlag{
|
cli.BoolFlag{
|
||||||
Name: "secure, s",
|
Name: "secure, s",
|
||||||
Usage: "enable secure cookie",
|
Usage: "enable secure cookie",
|
||||||
Destination: &conf.SecureCookie,
|
Destination: &config.SecureCookie,
|
||||||
},
|
},
|
||||||
cli.BoolFlag{
|
cli.BoolFlag{
|
||||||
Name: "debug",
|
Name: "debug",
|
||||||
Usage: "enable debugging",
|
Usage: "enable debugging",
|
||||||
Destination: &conf.Debug,
|
Destination: &config.Debug,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -256,13 +261,13 @@ func defaultFlags(f []cli.Flag) []cli.Flag {
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "config, c",
|
Name: "config, c",
|
||||||
Usage: "config file",
|
Usage: "config file",
|
||||||
Destination: &config,
|
Destination: &configFile,
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "data, d",
|
Name: "data, d",
|
||||||
Value: defDataDir,
|
Value: defDataDir,
|
||||||
Usage: "data directory",
|
Usage: "data directory",
|
||||||
Destination: &conf.DataDir,
|
Destination: &config.DataDir,
|
||||||
},
|
},
|
||||||
}, f...)
|
}, f...)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -51,6 +51,7 @@ type Config struct {
|
||||||
LogFile string `json:"log_file,omitempty"`
|
LogFile string `json:"log_file,omitempty"`
|
||||||
Version string `json:"-"`
|
Version string `json:"-"`
|
||||||
Secret string `json:"secret,omitempty"`
|
Secret string `json:"secret,omitempty"`
|
||||||
|
Admin bool `json:"admin,omitempty"`
|
||||||
Foreground bool `json:"foreground,omitempty"`
|
Foreground bool `json:"foreground,omitempty"`
|
||||||
SecureCookie bool `json:"secure_cookie,omitempty"`
|
SecureCookie bool `json:"secure_cookie,omitempty"`
|
||||||
Debug bool `json:"debug,omitempty"`
|
Debug bool `json:"debug,omitempty"`
|
||||||
|
|
|
||||||
|
|
@ -26,14 +26,15 @@ You can customize it how you like.`
|
||||||
const blevePath = "bleve"
|
const blevePath = "bleve"
|
||||||
|
|
||||||
type DB struct {
|
type DB struct {
|
||||||
|
admin bool
|
||||||
store store.Store
|
store store.Store
|
||||||
cache *cache.Cache
|
cache *cache.Cache
|
||||||
Index *index.Index
|
Index *index.Index
|
||||||
}
|
}
|
||||||
|
|
||||||
func New(dir string) (db *DB, err error) {
|
func New(cfg core.Config) (db *DB, err error) {
|
||||||
db = &DB{cache: cache.NewCache()}
|
db = &DB{admin: cfg.Admin, cache: cache.NewCache()}
|
||||||
dbFile := filepath.Join(dir, storeFile)
|
dbFile := filepath.Join(cfg.DataDir, storeFile)
|
||||||
err = godrop.Unveil(dbFile, "rwc")
|
err = godrop.Unveil(dbFile, "rwc")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
|
@ -42,7 +43,7 @@ func New(dir string) (db *DB, err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
db.Index, err = index.NewIndex(filepath.Join(dir, blevePath))
|
db.Index, err = index.NewIndex(filepath.Join(cfg.DataDir, blevePath))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -83,6 +83,10 @@ func (db *DB) CreateUser(username, password string, admin bool) error {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) Login(username, password string) (u core.User, err error) {
|
func (db *DB) Login(username, password string) (u core.User, err error) {
|
||||||
|
if username == defUser && !db.admin {
|
||||||
|
err = errors.New("db: user disabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
u, err = db.GetUser(username)
|
u, err = db.GetUser(username)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
|
|
|
||||||
|
|
@ -4,6 +4,7 @@ package log
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"git.giftfish.de/ston1th/godrop/v2"
|
"git.giftfish.de/ston1th/godrop/v2"
|
||||||
|
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||||
stdlog "log"
|
stdlog "log"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
|
@ -14,12 +15,12 @@ var (
|
||||||
debug = false
|
debug = false
|
||||||
)
|
)
|
||||||
|
|
||||||
func InitLogger(dir, file string, d bool) {
|
func InitLogger(cfg core.Config) {
|
||||||
debug = d
|
debug = cfg.Debug
|
||||||
if file == "-" {
|
if cfg.LogFile == "-" {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
logfile := filepath.Join(dir, file)
|
logfile := filepath.Join(cfg.DataDir, cfg.LogFile)
|
||||||
err := godrop.Unveil(logfile, "rwc")
|
err := godrop.Unveil(logfile, "rwc")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
stdlog.Fatal(err)
|
stdlog.Fatal(err)
|
||||||
|
|
|
||||||
|
|
@ -5,25 +5,28 @@ package otp
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
"github.com/pquerna/otp"
|
"github.com/pquerna/otp"
|
||||||
"github.com/pquerna/otp/totp"
|
"github.com/pquerna/otp/totp"
|
||||||
"html/template"
|
"html/template"
|
||||||
"image/png"
|
"image/png"
|
||||||
)
|
)
|
||||||
|
|
||||||
const dataUrl = "data:image/png;base64,"
|
const size = 200
|
||||||
|
|
||||||
func base64Image(key *otp.Key) (data template.URL, err error) {
|
func base64Image(key *otp.Key) (data template.URL, err error) {
|
||||||
var buf bytes.Buffer
|
var buf bytes.Buffer
|
||||||
img, err := key.Image(200, 200)
|
img, err := key.Image(size, size)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
err = errors.New("otp: " + err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
err = png.Encode(&buf, img)
|
err = png.Encode(&buf, img)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
err = errors.New("otp: " + err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data = template.URL(dataUrl + base64.StdEncoding.EncodeToString(buf.Bytes()))
|
data = template.URL("data:image/png;base64," + base64.StdEncoding.EncodeToString(buf.Bytes()))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -64,6 +67,7 @@ func Validate(pin, secret string) bool {
|
||||||
func ImageSecretFromURL(url string) (data template.URL, secret string, err error) {
|
func ImageSecretFromURL(url string) (data template.URL, secret string, err error) {
|
||||||
key, err := otp.NewKeyFromURL(url)
|
key, err := otp.NewKeyFromURL(url)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
err = errors.New("otp: " + err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
secret = key.Secret()
|
secret = key.Secret()
|
||||||
|
|
|
||||||
|
|
@ -55,7 +55,7 @@ func newContext(w http.ResponseWriter, r *http.Request, s *HTTPServer) (ctx *Con
|
||||||
ctx.Token = *t
|
ctx.Token = *t
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Redirect(core.TotpURI, 302)
|
ctx.Redirect(core.TotpURI, http.StatusFound)
|
||||||
return nil
|
return nil
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
@ -114,7 +114,7 @@ func (c *Context) Exec() {
|
||||||
}
|
}
|
||||||
c.Data.Token = newXsrf(c.Token.RawSig()[:keySize])
|
c.Data.Token = newXsrf(c.Token.RawSig()[:keySize])
|
||||||
|
|
||||||
c.Data.Version = c.Srv.Version
|
c.Data.Version = c.Srv.Config.Version
|
||||||
if c.Data.BodyTitle == "" {
|
if c.Data.BodyTitle == "" {
|
||||||
c.Data.BodyTitle = c.Data.Title
|
c.Data.BodyTitle = c.Data.Title
|
||||||
}
|
}
|
||||||
|
|
@ -162,6 +162,14 @@ func (c *Context) NotFound() {
|
||||||
c.Exec()
|
c.Exec()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (c *Context) Forbidden() {
|
||||||
|
c.Template("forbiddenHandler")
|
||||||
|
c.Data = webData{Title: "403"}
|
||||||
|
c.Status = http.StatusForbidden
|
||||||
|
c.Response.WriteHeader(http.StatusForbidden)
|
||||||
|
c.Exec()
|
||||||
|
}
|
||||||
|
|
||||||
func (c *Context) Template(name string) {
|
func (c *Context) Template(name string) {
|
||||||
c.T = c.Srv.templ[name]
|
c.T = c.Srv.templ[name]
|
||||||
}
|
}
|
||||||
|
|
@ -221,7 +229,7 @@ func (c *Context) LoggedOn() (ok bool) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Context) LogSetCookie(msg string, err error) {
|
func (c *Context) LogSetCookie(msg string, err error) {
|
||||||
log.Println(msg, err)
|
log.Printf("ctx: %s %s\n", msg, err)
|
||||||
c.SetCookie(nil)
|
c.SetCookie(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -240,7 +248,7 @@ func (c *Context) SetCookieToken(t *jwt.Token) {
|
||||||
Value: c.Token.String(),
|
Value: c.Token.String(),
|
||||||
Path: "/",
|
Path: "/",
|
||||||
MaxAge: int(jwt.DefaultExpiry.Seconds()),
|
MaxAge: int(jwt.DefaultExpiry.Seconds()),
|
||||||
Secure: c.Srv.Secure,
|
Secure: c.Srv.Config.SecureCookie,
|
||||||
HttpOnly: true,
|
HttpOnly: true,
|
||||||
SameSite: http.SameSiteStrictMode,
|
SameSite: http.SameSiteStrictMode,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -26,7 +26,7 @@ func totpAuthHandler(h ctxHandler) ctxHandler {
|
||||||
h(ctx)
|
h(ctx)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Redirect(core.IndexURI, 302)
|
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -36,7 +36,7 @@ func adminAuthHandler(h ctxHandler) ctxHandler {
|
||||||
h(ctx)
|
h(ctx)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.NotFound()
|
ctx.Forbidden()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -50,7 +50,7 @@ func userAuthHandler(h ctxHandler) ctxHandler {
|
||||||
h(ctx)
|
h(ctx)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.NotFound()
|
ctx.Forbidden()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -60,7 +60,7 @@ func authHandler(h ctxHandler) ctxHandler {
|
||||||
h(ctx)
|
h(ctx)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.NotFound()
|
ctx.Forbidden()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -80,12 +80,12 @@ func staticHandler(ctx *Context) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func indexHandler(ctx *Context) {
|
func indexHandler(ctx *Context) {
|
||||||
ctx.Redirect(core.IndexURI, 302)
|
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loginHandler(ctx *Context) {
|
func loginHandler(ctx *Context) {
|
||||||
if ctx.LoggedOn() {
|
if ctx.LoggedOn() {
|
||||||
ctx.Redirect(core.IndexURI, 302)
|
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Template("loginHandler")
|
ctx.Template("loginHandler")
|
||||||
|
|
@ -93,17 +93,20 @@ func loginHandler(ctx *Context) {
|
||||||
Title: "Login",
|
Title: "Login",
|
||||||
BodyTitle: "Login",
|
BodyTitle: "Login",
|
||||||
}
|
}
|
||||||
|
var user string
|
||||||
switch ctx.Method() {
|
switch ctx.Method() {
|
||||||
case "GET":
|
case "GET":
|
||||||
|
ctx.Data.Data = user
|
||||||
ctx.Exec()
|
ctx.Exec()
|
||||||
case "POST":
|
case "POST":
|
||||||
if !ctx.CheckXsrf() {
|
if !ctx.CheckXsrf() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user := ctx.Form("user")
|
user = ctx.Form("user")
|
||||||
|
ctx.Data.Data = user
|
||||||
password := ctx.Form("password")
|
password := ctx.Form("password")
|
||||||
if user == "" || password == "" {
|
if user == "" || password == "" {
|
||||||
ctx.Error("wrong inputs")
|
ctx.Error("empty user or password")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
u, err := ctx.Srv.DB.Login(user, password)
|
u, err := ctx.Srv.DB.Login(user, password)
|
||||||
|
|
@ -112,21 +115,22 @@ func loginHandler(ctx *Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if u.Secret == "" {
|
if u.Secret == "" {
|
||||||
|
log.Println("login:", u.Username)
|
||||||
ctx.SetCookie(jwt.Claims{userClaim: u.Username, createdClaim: u.Created, adminClaim: u.Admin})
|
ctx.SetCookie(jwt.Claims{userClaim: u.Username, createdClaim: u.Created, adminClaim: u.Admin})
|
||||||
ctx.Redirect(core.IndexURI, 302)
|
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.SetCookieToken(jwt.NewToken(map[string]interface{}{
|
ctx.SetCookieToken(jwt.NewToken(map[string]interface{}{
|
||||||
totpClaim: u.Username,
|
totpClaim: u.Username,
|
||||||
jwt.ExpClaim: jwt.NewExp(time.Minute),
|
jwt.ExpClaim: jwt.NewExp(time.Minute),
|
||||||
}, nil))
|
}, nil))
|
||||||
ctx.Redirect(core.TotpURI, 302)
|
ctx.Redirect(core.TotpURI, http.StatusFound)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func loginTotpHandler(ctx *Context) {
|
func loginTotpHandler(ctx *Context) {
|
||||||
if ctx.LoggedOn() {
|
if ctx.LoggedOn() {
|
||||||
ctx.Redirect(core.IndexURI, 302)
|
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Template("loginTotpHandler")
|
ctx.Template("loginTotpHandler")
|
||||||
|
|
@ -143,7 +147,7 @@ func loginTotpHandler(ctx *Context) {
|
||||||
}
|
}
|
||||||
pin := ctx.Form("pin")
|
pin := ctx.Form("pin")
|
||||||
if pin == "" {
|
if pin == "" {
|
||||||
ctx.Error("wrong inputs")
|
ctx.Error("empty pin")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
u, err := ctx.Srv.DB.Totp(ctx.Totp(), pin)
|
u, err := ctx.Srv.DB.Totp(ctx.Totp(), pin)
|
||||||
|
|
@ -151,8 +155,9 @@ func loginTotpHandler(ctx *Context) {
|
||||||
ctx.Error(err)
|
ctx.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
log.Println("login:", u.Username)
|
||||||
ctx.SetCookie(jwt.Claims{userClaim: u.Username, createdClaim: u.Created, adminClaim: u.Admin})
|
ctx.SetCookie(jwt.Claims{userClaim: u.Username, createdClaim: u.Created, adminClaim: u.Admin})
|
||||||
ctx.Redirect(core.IndexURI, 302)
|
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -214,6 +219,14 @@ func pageNewHandler(ctx *Context) {
|
||||||
Perm: perm,
|
Perm: perm,
|
||||||
}
|
}
|
||||||
ctx.Data.Data = p
|
ctx.Data.Data = p
|
||||||
|
if title == "" {
|
||||||
|
ctx.Error("empty title")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if perm == core.Invalid {
|
||||||
|
ctx.Error("invalid permission")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
if section != core.WikiSection {
|
if section != core.WikiSection {
|
||||||
section = ctx.User()
|
section = ctx.User()
|
||||||
|
|
@ -223,7 +236,7 @@ func pageNewHandler(ctx *Context) {
|
||||||
ctx.Error(err)
|
ctx.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Redirect(page.StoreTitle, 302)
|
ctx.Redirect(page.StoreTitle, http.StatusFound)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -237,19 +250,19 @@ func pageHandler(ctx *Context) {
|
||||||
if key := ctx.Var("key"); key != "" {
|
if key := ctx.Var("key"); key != "" {
|
||||||
t, err := jwt.DecodeToken(key)
|
t, err := jwt.DecodeToken(key)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Println("DecodeToken:", err)
|
log.Println("share: DecodeToken:", err)
|
||||||
ctx.NotFound()
|
ctx.NotFound()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err = ctx.Srv.JWT.Verify(t); err != nil {
|
if err = ctx.Srv.JWT.Verify(t); err != nil {
|
||||||
log.Println("VerifyToken:", err)
|
log.Println("share: VerifyToken:", err)
|
||||||
ctx.NotFound()
|
ctx.NotFound()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user = t.Claims.GetString(sharedClaim)
|
user = t.Claims.GetString(sharedClaim)
|
||||||
if ctx.Srv.DB.LockedOut(user, t.Claims.GetString(createdClaim)) {
|
if ctx.Srv.DB.LockedOut(user, t.Claims.GetString(createdClaim)) {
|
||||||
if err = ctx.Srv.JWT.Invalidate(t); err != nil {
|
if err = ctx.Srv.JWT.Invalidate(t); err != nil {
|
||||||
log.Println("Invalidate:", err)
|
log.Println("share: Invalidate:", err)
|
||||||
}
|
}
|
||||||
ctx.NotFound()
|
ctx.NotFound()
|
||||||
return
|
return
|
||||||
|
|
@ -310,7 +323,7 @@ func pageShareHandler(ctx *Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
d, err := time.ParseDuration(ctx.Form("duration"))
|
d, err := duration(ctx.Form("duration"), ctx.Form("mode"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error(err)
|
ctx.Error(err)
|
||||||
return
|
return
|
||||||
|
|
@ -324,7 +337,7 @@ func pageShareHandler(ctx *Context) {
|
||||||
}, nil)
|
}, nil)
|
||||||
|
|
||||||
if err = ctx.Srv.JWT.Sign(t); err != nil {
|
if err = ctx.Srv.JWT.Sign(t); err != nil {
|
||||||
log.Println("SignToken:", err)
|
log.Println("share: SignToken:", err)
|
||||||
ctx.NotFound()
|
ctx.NotFound()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -404,7 +417,7 @@ func pageEditHandler(ctx *Context) {
|
||||||
ctx.Error(err)
|
ctx.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Redirect(page.StoreTitle, 302)
|
ctx.Redirect(page.StoreTitle, http.StatusFound)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -426,7 +439,7 @@ func pageDelHandler(ctx *Context) {
|
||||||
ctx.Error(err)
|
ctx.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Redirect(core.IndexURI, 302)
|
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -471,15 +484,20 @@ func userNewHandler(ctx *Context) {
|
||||||
adm = true
|
adm = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if user == "" || password == "" || password != repeat {
|
if user == "" || password == "" {
|
||||||
ctx.Error("wrong inputs")
|
ctx.Error("empty user or password")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if password != repeat {
|
||||||
|
ctx.Error("passwords do not match")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := ctx.Srv.DB.CreateUser(user, password, adm); err != nil {
|
if err := ctx.Srv.DB.CreateUser(user, password, adm); err != nil {
|
||||||
ctx.Error(err)
|
ctx.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Redirect("/user", 302)
|
log.Printf("create: %s created by %s\n", user, ctx.User())
|
||||||
|
ctx.Redirect("/user", http.StatusFound)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -511,8 +529,8 @@ func userEditHandler(ctx *Context) {
|
||||||
adm = true
|
adm = true
|
||||||
}
|
}
|
||||||
|
|
||||||
if user == "" || password != repeat {
|
if password != repeat {
|
||||||
ctx.Error("wrong inputs")
|
ctx.Error("passwords do not match")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if ctx.Admin() {
|
if ctx.Admin() {
|
||||||
|
|
@ -520,17 +538,16 @@ func userEditHandler(ctx *Context) {
|
||||||
ctx.Error(err)
|
ctx.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
} else {
|
log.Printf("update: %s updated by %s\n", user, ctx.User())
|
||||||
if user == ctx.User() {
|
ctx.Redirect("/user", http.StatusFound)
|
||||||
if err := ctx.Srv.DB.UpdateUserPassword(user, password); err != nil {
|
return
|
||||||
ctx.Error(err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
ctx.Redirect(core.IndexURI, 302)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ctx.Redirect("/user", 302)
|
if err := ctx.Srv.DB.UpdateUserPassword(user, password); err != nil {
|
||||||
|
ctx.Error(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Printf("update: %s updated by %s\n", user, ctx.User())
|
||||||
|
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -558,7 +575,7 @@ func userTotpHandler(ctx *Context) {
|
||||||
req.URL = ctx.Form("url")
|
req.URL = ctx.Form("url")
|
||||||
req.Image, req.Secret, err = otp.ImageSecretFromURL(req.URL)
|
req.Image, req.Secret, err = otp.ImageSecretFromURL(req.URL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.Error("wrong inputs")
|
ctx.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -582,7 +599,8 @@ func userTotpHandler(ctx *Context) {
|
||||||
secret = u.Secret
|
secret = u.Secret
|
||||||
}
|
}
|
||||||
valid := otp.Validate(pin, secret)
|
valid := otp.Validate(pin, secret)
|
||||||
if ctx.User() != req.Username && ctx.Admin() {
|
user := ctx.User()
|
||||||
|
if user != req.Username && ctx.Admin() {
|
||||||
valid = true
|
valid = true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -594,7 +612,12 @@ func userTotpHandler(ctx *Context) {
|
||||||
ctx.Error(err)
|
ctx.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Redirect("/user/edit/"+req.Username, 302)
|
if req.Secret == "" {
|
||||||
|
log.Printf("totp: disabled by %s for %s\n", user, req.Username)
|
||||||
|
} else {
|
||||||
|
log.Printf("totp: enabled by %s for %s\n", user, req.Username)
|
||||||
|
}
|
||||||
|
ctx.Redirect("/user/edit/"+req.Username, http.StatusFound)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -603,7 +626,8 @@ func userUnlockHandler(ctx *Context) {
|
||||||
if err := ctx.Srv.DB.UnlockUser(user); err != nil {
|
if err := ctx.Srv.DB.UnlockUser(user); err != nil {
|
||||||
log.Println("unlock:", err)
|
log.Println("unlock:", err)
|
||||||
}
|
}
|
||||||
ctx.Redirect("/user", 302)
|
log.Printf("unlock: %s unlocked by %s\n", user, ctx.User())
|
||||||
|
ctx.Redirect("/user", http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func userDelHandler(ctx *Context) {
|
func userDelHandler(ctx *Context) {
|
||||||
|
|
@ -625,15 +649,23 @@ func userDelHandler(ctx *Context) {
|
||||||
if !ctx.CheckXsrf() {
|
if !ctx.CheckXsrf() {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := ctx.Srv.DB.DeleteUser(ctx.Var("user")); err != nil {
|
self := ctx.User()
|
||||||
|
user := ctx.Var("user")
|
||||||
|
if err := ctx.Srv.DB.DeleteUser(user); err != nil {
|
||||||
log.Println("delete:", err)
|
log.Println("delete:", err)
|
||||||
}
|
}
|
||||||
ctx.Redirect("/user", 302)
|
log.Printf("delete: %s deleted by %s\n", user, self)
|
||||||
|
if user == self {
|
||||||
|
ctx.Redirect("/logout", http.StatusFound)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Redirect("/user", http.StatusFound)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func logoutHandler(ctx *Context) {
|
func logoutHandler(ctx *Context) {
|
||||||
|
log.Println("logout:", ctx.User())
|
||||||
ctx.Srv.JWT.Invalidate(&ctx.Token)
|
ctx.Srv.JWT.Invalidate(&ctx.Token)
|
||||||
ctx.SetCookie(nil)
|
ctx.SetCookie(nil)
|
||||||
ctx.Redirect(core.IndexURI, 302)
|
ctx.Redirect(core.IndexURI, http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
47
pkg/server/helper.go
Normal file
47
pkg/server/helper.go
Normal file
|
|
@ -0,0 +1,47 @@
|
||||||
|
// Copyright (C) 2018 Marius Schellenberger
|
||||||
|
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
day = time.Hour * 24
|
||||||
|
min = time.Second * 30
|
||||||
|
max = day * 90
|
||||||
|
)
|
||||||
|
|
||||||
|
func duration(duration, mode string) (d time.Duration, err error) {
|
||||||
|
var m int
|
||||||
|
m, _ = strconv.Atoi(mode)
|
||||||
|
switch m {
|
||||||
|
case 0:
|
||||||
|
err = errors.New("invalid mode")
|
||||||
|
return
|
||||||
|
case 1:
|
||||||
|
d, err = time.ParseDuration(duration)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.New("could not parse duration")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
case 2:
|
||||||
|
var t int
|
||||||
|
t, err = strconv.Atoi(duration)
|
||||||
|
if err != nil {
|
||||||
|
err = errors.New("could not parse duration")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
d = time.Duration(int(day) * t)
|
||||||
|
}
|
||||||
|
|
||||||
|
if d < min {
|
||||||
|
err = errors.New("minimum duration is 30 seconds")
|
||||||
|
}
|
||||||
|
if d > max {
|
||||||
|
err = errors.New("maximum duration is 90 days")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
@ -71,16 +71,10 @@ var routes = []route{
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"/user/del/{user:[a-zA-Z0-9]+$}",
|
"/user/del/{user:[a-zA-Z0-9]+$}",
|
||||||
adminAuthHandler(
|
userAuthHandler(
|
||||||
userDelHandler),
|
userDelHandler),
|
||||||
[]string{"GET", "POST"},
|
[]string{"GET", "POST"},
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"/user/unlock/{user:[a-zA-Z0-9]+$}",
|
|
||||||
adminAuthHandler(
|
|
||||||
userUnlockHandler),
|
|
||||||
[]string{"POST"},
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"/user/edit/{user:[a-zA-Z0-9]+$}",
|
"/user/edit/{user:[a-zA-Z0-9]+$}",
|
||||||
userAuthHandler(
|
userAuthHandler(
|
||||||
|
|
@ -93,6 +87,12 @@ var routes = []route{
|
||||||
userTotpHandler),
|
userTotpHandler),
|
||||||
[]string{"GET", "POST"},
|
[]string{"GET", "POST"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"/user/unlock/{user:[a-zA-Z0-9]+$}",
|
||||||
|
adminAuthHandler(
|
||||||
|
userUnlockHandler),
|
||||||
|
[]string{"POST"},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"/logout",
|
"/logout",
|
||||||
logoutHandler,
|
logoutHandler,
|
||||||
|
|
|
||||||
|
|
@ -23,10 +23,7 @@ const (
|
||||||
)
|
)
|
||||||
|
|
||||||
type HTTPServer struct {
|
type HTTPServer struct {
|
||||||
Version string
|
Config core.Config
|
||||||
Secure bool
|
|
||||||
Secret string
|
|
||||||
DataDir string
|
|
||||||
|
|
||||||
listener net.Listener
|
listener net.Listener
|
||||||
srv *http.Server
|
srv *http.Server
|
||||||
|
|
@ -38,12 +35,9 @@ type HTTPServer struct {
|
||||||
res map[string][]byte
|
res map[string][]byte
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHTTPServer(l net.Listener, conf core.Config) (srv *HTTPServer) {
|
func NewHTTPServer(cfg core.Config, l net.Listener) (srv *HTTPServer) {
|
||||||
srv = &HTTPServer{
|
srv = &HTTPServer{
|
||||||
Version: conf.Version,
|
Config: cfg,
|
||||||
Secure: conf.SecureCookie,
|
|
||||||
Secret: conf.Secret,
|
|
||||||
DataDir: conf.DataDir,
|
|
||||||
listener: l,
|
listener: l,
|
||||||
}
|
}
|
||||||
srv.loadTemplates()
|
srv.loadTemplates()
|
||||||
|
|
@ -54,7 +48,7 @@ func NewHTTPServer(l net.Listener, conf core.Config) (srv *HTTPServer) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *HTTPServer) Start() (err error) {
|
func (s *HTTPServer) Start() (err error) {
|
||||||
s.DB, err = db.New(s.DataDir)
|
s.DB, err = db.New(s.Config)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("db: " + err.Error())
|
return errors.New("db: " + err.Error())
|
||||||
}
|
}
|
||||||
|
|
@ -62,8 +56,8 @@ func (s *HTTPServer) Start() (err error) {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if s.Secret != "" {
|
if s.Config.Secret != "" {
|
||||||
s.JWT, err = jwt.New(jwt.DefaultExpiry, s.DB, bytes.NewBufferString(s.Secret))
|
s.JWT, err = jwt.New(jwt.DefaultExpiry, s.DB, bytes.NewBufferString(s.Config.Secret))
|
||||||
} else {
|
} else {
|
||||||
s.JWT, err = jwt.New(jwt.DefaultExpiry, s.DB, nil)
|
s.JWT, err = jwt.New(jwt.DefaultExpiry, s.DB, nil)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,6 +42,23 @@ const (
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
{{end}}`
|
||||||
|
forbidden = `{{define "body"}}
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-4 offset-4">
|
||||||
|
<div class="card border-danger mx-auto">
|
||||||
|
<div class="card-header">
|
||||||
|
<strong>403 - Forbidden</strong>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h5>Access to this page is restricted</h5>
|
||||||
|
<a href="/" class="btn btn-sm btn-primary">Back</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{{end}}`
|
{{end}}`
|
||||||
index = `<!DOCTYPE html>
|
index = `<!DOCTYPE html>
|
||||||
<html>
|
<html>
|
||||||
|
|
@ -99,7 +116,7 @@ const (
|
||||||
<input type="hidden" name="token" value="{{.Token}}">
|
<input type="hidden" name="token" value="{{.Token}}">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-form-label" for="user">Username</label>
|
<label class="col-form-label" for="user">Username</label>
|
||||||
<input class="form-control input-sm" type="text" id="user" name="user" autofocus required>
|
<input class="form-control input-sm" type="text" id="user" name="user" value="{{.Data}}" autofocus required>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-form-label" for="password">Password</label>
|
<label class="col-form-label" for="password">Password</label>
|
||||||
|
|
@ -168,10 +185,10 @@ const (
|
||||||
<div class="col-xs-4 offset-4">
|
<div class="col-xs-4 offset-4">
|
||||||
<div class="card border-danger mx-auto">
|
<div class="card border-danger mx-auto">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<strong>404</strong>
|
<strong>404 - Page Not Found</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h4>Page not found</h4>
|
<h5>The requested page could not be found</h5>
|
||||||
<a href="/" class="btn btn-sm btn-primary">Back</a>
|
<a href="/" class="btn btn-sm btn-primary">Back</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -252,7 +269,8 @@ const (
|
||||||
<label class="custom-control-label" for="perm3">Private</label>
|
<label class="custom-control-label" for="perm3">Private</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-sm btn-primary" type="submit">Submit</button>
|
<button class="btn btn-sm btn-primary" type="submit">Update</button>
|
||||||
|
<a href="/{{.Data.StoreTitle}}" class="btn btn-sm btn-primary">Back</a>
|
||||||
</form>
|
</form>
|
||||||
{{end}}`
|
{{end}}`
|
||||||
page = `{{define "body"}}
|
page = `{{define "body"}}
|
||||||
|
|
@ -293,7 +311,7 @@ const (
|
||||||
<a href="/{{.Data.StoreTitle}}/share" class="btn btn-sm btn-primary">Share</a>
|
<a href="/{{.Data.StoreTitle}}/share" class="btn btn-sm btn-primary">Share</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if ne .Data.StoreTitle "wiki/Index"}}
|
{{if ne .Data.StoreTitle "wiki/Index"}}
|
||||||
<a href="/{{.Data.StoreTitle}}/del" class="btn btn-sm btn-primary">Delete</a>
|
<a href="/{{.Data.StoreTitle}}/del" class="btn btn-sm btn-danger">Delete</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -304,7 +322,7 @@ const (
|
||||||
pageMD = `{{define "body"}}
|
pageMD = `{{define "body"}}
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<h2>{{.BodyTitle}}</h2>
|
<a href="{{if .Key}}/view/{{.Key}}{{else}}/{{.Data.StoreTitle}}{{end}}"><h2>{{.BodyTitle}}</h2></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mt-3">
|
<div class="row mt-3">
|
||||||
|
|
@ -348,11 +366,10 @@ const (
|
||||||
<label class="custom-control-label" for="perm3">Private</label>
|
<label class="custom-control-label" for="perm3">Private</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-sm btn-primary" type="submit">Submit</button>
|
<button class="btn btn-sm btn-primary" type="submit">Create</button>
|
||||||
</form>
|
</form>
|
||||||
{{end}}`
|
{{end}}`
|
||||||
pageShare = `{{define "body"}}
|
pageShare = `{{define "body"}}
|
||||||
{{if not .Msg}}
|
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-4 offset-4">
|
<div class="col-xs-4 offset-4">
|
||||||
|
|
@ -366,15 +383,27 @@ const (
|
||||||
<label class="col-form-label" for="key">Key</label>
|
<label class="col-form-label" for="key">Key</label>
|
||||||
<input class="form-control input-sm" type="text" id="key" value="{{.Key}}" disabled>
|
<input class="form-control input-sm" type="text" id="key" value="{{.Key}}" disabled>
|
||||||
</div>
|
</div>
|
||||||
<a href="/view/{{.Key}}">Share Link</a>
|
<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}}
|
{{else}}
|
||||||
<form class="form-horizontal" action="/{{.Data}}/share" method="post">
|
<form class="form-horizontal" action="/{{.Data}}/share" method="post">
|
||||||
<input type="hidden" name="token" value="{{.Token}}">
|
<input type="hidden" name="token" value="{{.Token}}">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-form-label" for="duration">Lifetime</label>
|
<label class="col-form-label" for="duration">Lifetime</label>
|
||||||
<input class="form-control input-sm" type="text" id="duration" name="duration" placeholder="1d (day) 10m (minutes)" autofocus required>
|
<input class="form-control input-sm" type="text" id="duration" name="duration" placeholder="min: 30s max: 90d" 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</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-sm btn-primary" type="submit">Share</button>
|
<button class="btn btn-sm btn-primary" type="submit">Share</button>
|
||||||
|
<a href="/{{.Data}}" class="btn btn-sm btn-primary">Back</a>
|
||||||
</form>
|
</form>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -382,7 +411,6 @@ const (
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
|
||||||
{{end}}`
|
{{end}}`
|
||||||
pageView = `{{define "body"}}
|
pageView = `{{define "body"}}
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
|
|
@ -512,12 +540,13 @@ const (
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
<button class="btn btn-sm btn-primary" type="submit">Submit</button>
|
<button class="btn btn-sm btn-primary" type="submit">Update</button>
|
||||||
{{if .Admin}}
|
{{if .Admin}}
|
||||||
<a href="/user" class="btn btn-sm btn-primary">Back</a>
|
<a href="/user" class="btn btn-sm btn-primary">Back</a>
|
||||||
{{else}}
|
{{else}}
|
||||||
<a href="/" class="btn btn-sm btn-primary">Back</a>
|
<a href="/" class="btn btn-sm btn-primary">Back</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
<a class="btn btn-sm btn-danger" href="/user/del/{{.Data.Username}}">Delete</a>
|
||||||
</form>
|
</form>
|
||||||
{{if .Admin}}
|
{{if .Admin}}
|
||||||
{{if eq .Data.Locked 3}}
|
{{if eq .Data.Locked 3}}
|
||||||
|
|
@ -555,7 +584,7 @@ const (
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Username</th>
|
<th>Username</th>
|
||||||
<th>Admin</th>
|
<th>Flags</th>
|
||||||
<th>Options</th>
|
<th>Options</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
@ -564,8 +593,12 @@ const (
|
||||||
{{$admin := .Admin}}
|
{{$admin := .Admin}}
|
||||||
{{range $item := .Data}}
|
{{range $item := .Data}}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{$item.Username}} {{if $item.Secret}}<span class="badge badge-success">TOTP</span>{{end}}</td>
|
<td>{{$item.Username}}</td>
|
||||||
{{if $item.Admin}}<td>yes</td>{{else}}<td>no</td>{{end}}
|
<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>
|
<td>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
{{if $admin}}
|
{{if $admin}}
|
||||||
|
|
@ -612,7 +645,7 @@ const (
|
||||||
<label class="col-form-label" for="admin">Admin Privileges</label>
|
<label class="col-form-label" for="admin">Admin Privileges</label>
|
||||||
<input type="checkbox" id="admin" name="admin" value="0">
|
<input type="checkbox" id="admin" name="admin" value="0">
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-sm btn-primary" type="submit">Submit</button>
|
<button class="btn btn-sm btn-primary" type="submit">Create</button>
|
||||||
<a href="/user" class="btn btn-sm btn-primary">Back</a>
|
<a href="/user" class="btn btn-sm btn-primary">Back</a>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -637,7 +670,7 @@ const (
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-form-label" for="secret">Secret</label>
|
<label class="col-form-label" for="secret">Secret</label>
|
||||||
<p id="secret">{{.Data.Secret}}</p>
|
<pre><code id="secret">{{.Data.Secret}}</code></pre>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|
@ -836,6 +869,7 @@ func (s *HTTPServer) loadTemplates() {
|
||||||
|
|
||||||
// pages
|
// pages
|
||||||
s.templ["notFoundHandler"] = parse(index, menu, notFound)
|
s.templ["notFoundHandler"] = parse(index, menu, notFound)
|
||||||
|
s.templ["forbiddenHandler"] = parse(index, menu, forbidden)
|
||||||
s.templ["loginHandler"] = parse(index, menu, login)
|
s.templ["loginHandler"] = parse(index, menu, login)
|
||||||
s.templ["loginTotpHandler"] = parse(index, menu, loginTotp)
|
s.templ["loginTotpHandler"] = parse(index, menu, loginTotp)
|
||||||
s.templ["searchHandler"] = parse(index, menu, search)
|
s.templ["searchHandler"] = parse(index, menu, search)
|
||||||
|
|
|
||||||
|
|
@ -70,6 +70,7 @@ func (s *HTTPServer) loadTemplates() {
|
||||||
|
|
||||||
// pages
|
// pages
|
||||||
s.templ["notFoundHandler"] = parse(index, menu, notFound)
|
s.templ["notFoundHandler"] = parse(index, menu, notFound)
|
||||||
|
s.templ["forbiddenHandler"] = parse(index, menu, forbidden)
|
||||||
s.templ["loginHandler"] = parse(index, menu, login)
|
s.templ["loginHandler"] = parse(index, menu, login)
|
||||||
s.templ["loginTotpHandler"] = parse(index, menu, loginTotp)
|
s.templ["loginTotpHandler"] = parse(index, menu, loginTotp)
|
||||||
s.templ["searchHandler"] = parse(index, menu, search)
|
s.templ["searchHandler"] = parse(index, menu, search)
|
||||||
|
|
|
||||||
17
templates/forbidden.html
Normal file
17
templates/forbidden.html
Normal file
|
|
@ -0,0 +1,17 @@
|
||||||
|
{{define "body"}}
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-xs-4 offset-4">
|
||||||
|
<div class="card border-danger mx-auto">
|
||||||
|
<div class="card-header">
|
||||||
|
<strong>403 - Forbidden</strong>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<h5>Access to this page is restricted</h5>
|
||||||
|
<a href="/" class="btn btn-sm btn-primary">Back</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
@ -11,7 +11,7 @@
|
||||||
<input type="hidden" name="token" value="{{.Token}}">
|
<input type="hidden" name="token" value="{{.Token}}">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-form-label" for="user">Username</label>
|
<label class="col-form-label" for="user">Username</label>
|
||||||
<input class="form-control input-sm" type="text" id="user" name="user" autofocus required>
|
<input class="form-control input-sm" type="text" id="user" name="user" value="{{.Data}}" autofocus required>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-form-label" for="password">Password</label>
|
<label class="col-form-label" for="password">Password</label>
|
||||||
|
|
|
||||||
|
|
@ -4,10 +4,10 @@
|
||||||
<div class="col-xs-4 offset-4">
|
<div class="col-xs-4 offset-4">
|
||||||
<div class="card border-danger mx-auto">
|
<div class="card border-danger mx-auto">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<strong>404</strong>
|
<strong>404 - Page Not Found</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<h4>Page not found</h4>
|
<h5>The requested page could not be found</h5>
|
||||||
<a href="/" class="btn btn-sm btn-primary">Back</a>
|
<a href="/" class="btn btn-sm btn-primary">Back</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -36,7 +36,7 @@
|
||||||
<a href="/{{.Data.StoreTitle}}/share" class="btn btn-sm btn-primary">Share</a>
|
<a href="/{{.Data.StoreTitle}}/share" class="btn btn-sm btn-primary">Share</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{if ne .Data.StoreTitle "wiki/Index"}}
|
{{if ne .Data.StoreTitle "wiki/Index"}}
|
||||||
<a href="/{{.Data.StoreTitle}}/del" class="btn btn-sm btn-primary">Delete</a>
|
<a href="/{{.Data.StoreTitle}}/del" class="btn btn-sm btn-danger">Delete</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -28,6 +28,7 @@
|
||||||
<label class="custom-control-label" for="perm3">Private</label>
|
<label class="custom-control-label" for="perm3">Private</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-sm btn-primary" type="submit">Submit</button>
|
<button class="btn btn-sm btn-primary" type="submit">Update</button>
|
||||||
|
<a href="/{{.Data.StoreTitle}}" class="btn btn-sm btn-primary">Back</a>
|
||||||
</form>
|
</form>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{{define "body"}}
|
{{define "body"}}
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<h2>{{.BodyTitle}}</h2>
|
<a href="{{if .Key}}/view/{{.Key}}{{else}}/{{.Data.StoreTitle}}{{end}}"><h2>{{.BodyTitle}}</h2></a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="row mt-3">
|
<div class="row mt-3">
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,6 @@
|
||||||
<label class="custom-control-label" for="perm3">Private</label>
|
<label class="custom-control-label" for="perm3">Private</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-sm btn-primary" type="submit">Submit</button>
|
<button class="btn btn-sm btn-primary" type="submit">Create</button>
|
||||||
</form>
|
</form>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,4 @@
|
||||||
{{define "body"}}
|
{{define "body"}}
|
||||||
{{if not .Msg}}
|
|
||||||
<div class="page-header">
|
<div class="page-header">
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-4 offset-4">
|
<div class="col-xs-4 offset-4">
|
||||||
|
|
@ -13,15 +12,27 @@
|
||||||
<label class="col-form-label" for="key">Key</label>
|
<label class="col-form-label" for="key">Key</label>
|
||||||
<input class="form-control input-sm" type="text" id="key" value="{{.Key}}" disabled>
|
<input class="form-control input-sm" type="text" id="key" value="{{.Key}}" disabled>
|
||||||
</div>
|
</div>
|
||||||
<a href="/view/{{.Key}}">Share Link</a>
|
<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}}
|
{{else}}
|
||||||
<form class="form-horizontal" action="/{{.Data}}/share" method="post">
|
<form class="form-horizontal" action="/{{.Data}}/share" method="post">
|
||||||
<input type="hidden" name="token" value="{{.Token}}">
|
<input type="hidden" name="token" value="{{.Token}}">
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-form-label" for="duration">Lifetime</label>
|
<label class="col-form-label" for="duration">Lifetime</label>
|
||||||
<input class="form-control input-sm" type="text" id="duration" name="duration" placeholder="1d (day) 10m (minutes)" autofocus required>
|
<input class="form-control input-sm" type="text" id="duration" name="duration" placeholder="min: 30s max: 90d" 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</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-sm btn-primary" type="submit">Share</button>
|
<button class="btn btn-sm btn-primary" type="submit">Share</button>
|
||||||
|
<a href="/{{.Data}}" class="btn btn-sm btn-primary">Back</a>
|
||||||
</form>
|
</form>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -30,4 +41,3 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
{{end}}
|
|
||||||
|
|
|
||||||
|
|
@ -18,7 +18,7 @@
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Username</th>
|
<th>Username</th>
|
||||||
<th>Admin</th>
|
<th>Flags</th>
|
||||||
<th>Options</th>
|
<th>Options</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
|
|
@ -27,8 +27,12 @@
|
||||||
{{$admin := .Admin}}
|
{{$admin := .Admin}}
|
||||||
{{range $item := .Data}}
|
{{range $item := .Data}}
|
||||||
<tr>
|
<tr>
|
||||||
<td>{{$item.Username}} {{if $item.Secret}}<span class="badge badge-success">TOTP</span>{{end}}</td>
|
<td>{{$item.Username}}</td>
|
||||||
{{if $item.Admin}}<td>yes</td>{{else}}<td>no</td>{{end}}
|
<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>
|
<td>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
{{if $admin}}
|
{{if $admin}}
|
||||||
|
|
|
||||||
|
|
@ -36,12 +36,13 @@
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
<button class="btn btn-sm btn-primary" type="submit">Submit</button>
|
<button class="btn btn-sm btn-primary" type="submit">Update</button>
|
||||||
{{if .Admin}}
|
{{if .Admin}}
|
||||||
<a href="/user" class="btn btn-sm btn-primary">Back</a>
|
<a href="/user" class="btn btn-sm btn-primary">Back</a>
|
||||||
{{else}}
|
{{else}}
|
||||||
<a href="/" class="btn btn-sm btn-primary">Back</a>
|
<a href="/" class="btn btn-sm btn-primary">Back</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
|
<a class="btn btn-sm btn-danger" href="/user/del/{{.Data.Username}}">Delete</a>
|
||||||
</form>
|
</form>
|
||||||
{{if .Admin}}
|
{{if .Admin}}
|
||||||
{{if eq .Data.Locked 3}}
|
{{if eq .Data.Locked 3}}
|
||||||
|
|
|
||||||
|
|
@ -24,7 +24,7 @@
|
||||||
<label class="col-form-label" for="admin">Admin Privileges</label>
|
<label class="col-form-label" for="admin">Admin Privileges</label>
|
||||||
<input type="checkbox" id="admin" name="admin" value="0">
|
<input type="checkbox" id="admin" name="admin" value="0">
|
||||||
</div>
|
</div>
|
||||||
<button class="btn btn-sm btn-primary" type="submit">Submit</button>
|
<button class="btn btn-sm btn-primary" type="submit">Create</button>
|
||||||
<a href="/user" class="btn btn-sm btn-primary">Back</a>
|
<a href="/user" class="btn btn-sm btn-primary">Back</a>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
||||||
|
|
@ -16,7 +16,7 @@
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-form-label" for="secret">Secret</label>
|
<label class="col-form-label" for="secret">Secret</label>
|
||||||
<p id="secret">{{.Data.Secret}}</p>
|
<pre><code id="secret">{{.Data.Secret}}</code></pre>
|
||||||
</div>
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue