added totp support
This commit is contained in:
parent
1681a2b4a0
commit
bac08dc7cf
16 changed files with 448 additions and 32 deletions
2
Makefile
2
Makefile
|
|
@ -26,7 +26,7 @@ generate:
|
|||
$(CC) generate
|
||||
|
||||
gofmt:
|
||||
gofmt -w .
|
||||
gofmt -w pkg/ main.go
|
||||
|
||||
golint:
|
||||
$(GOPATH)/bin/golint .
|
||||
|
|
|
|||
|
|
@ -68,7 +68,7 @@ func initServer(conf core.Config) (err error) {
|
|||
if err != nil {
|
||||
return
|
||||
}
|
||||
log.Println("gowiki started")
|
||||
log.Println("gowiki " + conf.Version + " started")
|
||||
sigs := make(chan os.Signal)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
sig := <-sigs
|
||||
|
|
|
|||
|
|
@ -7,4 +7,7 @@ const (
|
|||
WikiSection = "wiki"
|
||||
|
||||
IndexURI = WikiSection + "/" + IndexPage
|
||||
|
||||
LoginURI = "/login"
|
||||
TotpURI = "/totp"
|
||||
)
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ type User struct {
|
|||
Username string
|
||||
Password string
|
||||
Created string
|
||||
Secret string
|
||||
Admin bool
|
||||
Locked int
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"errors"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/log"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/otp"
|
||||
"git.giftfish.de/ston1th/jwt/v3"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"regexp"
|
||||
|
|
@ -45,6 +46,13 @@ func (db *DB) GetUserWithoutPassword(username string) (u core.User, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
func (db *DB) GetUserSecret(username string) (secret string, err error) {
|
||||
var user core.User
|
||||
user, err = db.GetUser(username)
|
||||
secret = user.Secret
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) GetUser(username string) (u core.User, err error) {
|
||||
err = db.store.Get(userPrefix+username, &u)
|
||||
return
|
||||
|
|
@ -102,6 +110,34 @@ func (db *DB) Login(username, password string) (u core.User, err error) {
|
|||
return
|
||||
}
|
||||
|
||||
func (db *DB) Totp(username, pin string) (u core.User, err error) {
|
||||
u, err = db.GetUser(username)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if u.Locked == 3 {
|
||||
err = errors.New("db: user locked")
|
||||
return
|
||||
}
|
||||
if !otp.Validate(pin, u.Secret) {
|
||||
u.Locked++
|
||||
err = db.store.Set(userPrefix+username, u)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = errors.New("db: wrong pin")
|
||||
return
|
||||
}
|
||||
if u.Locked > 0 {
|
||||
u.Locked = 0
|
||||
err = db.store.Set(userPrefix+username, u)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (db *DB) UnlockUser(username string) (err error) {
|
||||
var u core.User
|
||||
u, err = db.GetUser(username)
|
||||
|
|
@ -118,6 +154,15 @@ func (db *DB) UnlockUser(username string) (err error) {
|
|||
return
|
||||
}
|
||||
|
||||
func (db *DB) UpdateUserSecret(username, secret string) error {
|
||||
u, err := db.GetUser(username)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
u.Secret = secret
|
||||
return db.store.Set(userPrefix+username, u)
|
||||
}
|
||||
|
||||
func (db *DB) UpdateUserPassword(username, password string) error {
|
||||
u, err := db.GetUser(username)
|
||||
if err != nil {
|
||||
|
|
|
|||
72
pkg/otp/otp.go
Normal file
72
pkg/otp/otp.go
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
// Copyright (C) 2018 Marius Schellenberger
|
||||
|
||||
package otp
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"github.com/pquerna/otp"
|
||||
"github.com/pquerna/otp/totp"
|
||||
"html/template"
|
||||
"image/png"
|
||||
)
|
||||
|
||||
const dataUrl = "data:image/png;base64,"
|
||||
|
||||
func base64Image(key *otp.Key) (data template.URL, err error) {
|
||||
var buf bytes.Buffer
|
||||
img, err := key.Image(200, 200)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = png.Encode(&buf, img)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
data = template.URL(dataUrl + base64.StdEncoding.EncodeToString(buf.Bytes()))
|
||||
return
|
||||
}
|
||||
|
||||
type Request struct {
|
||||
Username string
|
||||
Image template.URL
|
||||
Secret string
|
||||
URL string
|
||||
}
|
||||
|
||||
func New(username string) (req Request, err error) {
|
||||
key, err := totp.Generate(totp.GenerateOpts{
|
||||
Issuer: "gowiki",
|
||||
AccountName: username,
|
||||
})
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
data, err := base64Image(key)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
return Request{
|
||||
Username: username,
|
||||
Image: data,
|
||||
Secret: key.Secret(),
|
||||
URL: key.URL(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func Validate(pin, secret string) bool {
|
||||
if secret == "" {
|
||||
return true
|
||||
}
|
||||
return totp.Validate(pin, secret)
|
||||
}
|
||||
|
||||
func ImageSecretFromURL(url string) (data template.URL, secret string, err error) {
|
||||
key, err := otp.NewKeyFromURL(url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
secret = key.Secret()
|
||||
data, err = base64Image(key)
|
||||
return
|
||||
}
|
||||
|
|
@ -4,6 +4,7 @@ package server
|
|||
|
||||
import (
|
||||
"errors"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/log"
|
||||
"git.giftfish.de/ston1th/jwt/v3"
|
||||
"github.com/gorilla/mux"
|
||||
|
|
@ -19,6 +20,7 @@ const (
|
|||
sharedClaim = "shared"
|
||||
sectionClaim = "section"
|
||||
titleClaim = "title"
|
||||
totpClaim = "totp"
|
||||
)
|
||||
|
||||
func newContext(w http.ResponseWriter, r *http.Request, s *HTTPServer) (ctx *Context) {
|
||||
|
|
@ -26,29 +28,43 @@ func newContext(w http.ResponseWriter, r *http.Request, s *HTTPServer) (ctx *Con
|
|||
h.Set("X-Frame-Options", "DENY")
|
||||
h.Set("X-Content-Type-Options", "nosniff")
|
||||
h.Set("X-XSS-Protection", "1; mode=block")
|
||||
h.Set("Content-Security-Policy", "default-src 'none';style-src 'self';frame-ancestors 'none'")
|
||||
h.Set("Content-Security-Policy", "default-src 'none';style-src 'self';img-src 'self' data:;frame-ancestors 'none'")
|
||||
ctx = &Context{
|
||||
Request: r,
|
||||
Response: w,
|
||||
Srv: s,
|
||||
Time: time.Now(),
|
||||
}
|
||||
path := ctx.Path()
|
||||
if path == "/bootstrap.css" || path == "/custom.css" {
|
||||
return
|
||||
}
|
||||
|
||||
if c, err := r.Cookie(cookieName); err == nil {
|
||||
t, err := jwt.DecodeToken(c.Value)
|
||||
if err != nil {
|
||||
log.Println("DecodeToken:", err)
|
||||
} else {
|
||||
if err = s.JWT.Verify(t); err != nil {
|
||||
log.Println("VerifyToken:", err)
|
||||
} else {
|
||||
if !s.DB.LockedOut(t.Claims.GetString(userClaim), t.Claims.GetString(createdClaim)) {
|
||||
ctx.Token = *t
|
||||
return
|
||||
}
|
||||
if err = s.JWT.Invalidate(t); err != nil {
|
||||
log.Println("Invalidate:", err)
|
||||
}
|
||||
ctx.LogSetCookie("DecodeToken:", err)
|
||||
return
|
||||
}
|
||||
if err = s.JWT.Verify(t); err != nil {
|
||||
ctx.LogSetCookie("VerifyToken:", err)
|
||||
return
|
||||
}
|
||||
if t.Claims.GetString(totpClaim) != "" {
|
||||
if path == core.TotpURI || path == core.LoginURI {
|
||||
ctx.Token = *t
|
||||
return
|
||||
}
|
||||
ctx.Redirect(core.TotpURI, 302)
|
||||
return nil
|
||||
|
||||
}
|
||||
if !s.DB.LockedOut(t.Claims.GetString(userClaim), t.Claims.GetString(createdClaim)) {
|
||||
ctx.Token = *t
|
||||
return
|
||||
}
|
||||
if err = s.JWT.Invalidate(t); err != nil {
|
||||
log.Println("Invalidate:", err)
|
||||
}
|
||||
}
|
||||
ctx.SetCookie(nil)
|
||||
|
|
@ -204,8 +220,16 @@ func (c *Context) LoggedOn() (ok bool) {
|
|||
return
|
||||
}
|
||||
|
||||
func (c *Context) LogSetCookie(msg string, err error) {
|
||||
log.Println(msg, err)
|
||||
c.SetCookie(nil)
|
||||
}
|
||||
|
||||
func (c *Context) SetCookie(claims map[string]interface{}) {
|
||||
t := jwt.NewToken(claims, nil)
|
||||
c.SetCookieToken(jwt.NewToken(claims, nil))
|
||||
}
|
||||
|
||||
func (c *Context) SetCookieToken(t *jwt.Token) {
|
||||
if err := c.Srv.JWT.Sign(t); err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
|
|
@ -233,6 +257,10 @@ func (c *Context) User() string {
|
|||
return c.Token.Claims.GetString(userClaim)
|
||||
}
|
||||
|
||||
func (c *Context) Totp() string {
|
||||
return c.Token.Claims.GetString(totpClaim)
|
||||
}
|
||||
|
||||
func (c *Context) Admin() bool {
|
||||
return c.Token.Claims.GetBool(adminClaim)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ package server
|
|||
import (
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/core"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/log"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/otp"
|
||||
"git.giftfish.de/ston1th/gowiki/pkg/render"
|
||||
"git.giftfish.de/ston1th/jwt/v3"
|
||||
"net/http"
|
||||
|
|
@ -19,6 +20,16 @@ func (nf *notFoundHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
|||
newContext(w, r, nf.s).NotFound()
|
||||
}
|
||||
|
||||
func totpAuthHandler(h ctxHandler) ctxHandler {
|
||||
return func(ctx *Context) {
|
||||
if ctx.Totp() != "" {
|
||||
h(ctx)
|
||||
return
|
||||
}
|
||||
ctx.Redirect(core.IndexURI, 302)
|
||||
}
|
||||
}
|
||||
|
||||
func adminAuthHandler(h ctxHandler) ctxHandler {
|
||||
return func(ctx *Context) {
|
||||
if ctx.Admin() {
|
||||
|
|
@ -100,6 +111,46 @@ func loginHandler(ctx *Context) {
|
|||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
if u.Secret == "" {
|
||||
ctx.SetCookie(jwt.Claims{userClaim: u.Username, createdClaim: u.Created, adminClaim: u.Admin})
|
||||
ctx.Redirect(core.IndexURI, 302)
|
||||
return
|
||||
}
|
||||
ctx.SetCookieToken(jwt.NewToken(map[string]interface{}{
|
||||
totpClaim: u.Username,
|
||||
jwt.ExpClaim: jwt.NewExp(time.Minute),
|
||||
}, nil))
|
||||
ctx.Redirect(core.TotpURI, 302)
|
||||
}
|
||||
}
|
||||
|
||||
func loginTotpHandler(ctx *Context) {
|
||||
if ctx.LoggedOn() {
|
||||
ctx.Redirect(core.IndexURI, 302)
|
||||
return
|
||||
}
|
||||
ctx.Template("loginTotpHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "TOTP Verification",
|
||||
BodyTitle: "TOTP Veriftcation",
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
pin := ctx.Form("pin")
|
||||
if pin == "" {
|
||||
ctx.Error("wrong inputs")
|
||||
return
|
||||
}
|
||||
u, err := ctx.Srv.DB.Totp(ctx.Totp(), pin)
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
ctx.SetCookie(jwt.Claims{userClaim: u.Username, createdClaim: u.Created, adminClaim: u.Admin})
|
||||
ctx.Redirect(core.IndexURI, 302)
|
||||
}
|
||||
|
|
@ -483,6 +534,70 @@ func userEditHandler(ctx *Context) {
|
|||
}
|
||||
}
|
||||
|
||||
func userTotpHandler(ctx *Context) {
|
||||
ctx.Template("userTotpHandler")
|
||||
ctx.Data = webData{
|
||||
Title: "TOTP",
|
||||
BodyTitle: "TOTP",
|
||||
}
|
||||
req := otp.Request{Username: ctx.Var("user")}
|
||||
u, err := ctx.Srv.DB.GetUser(req.Username)
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
if u.Secret == "" {
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
req, err = otp.New(req.Username)
|
||||
if err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
case "POST":
|
||||
req.URL = ctx.Form("url")
|
||||
req.Image, req.Secret, err = otp.ImageSecretFromURL(req.URL)
|
||||
if err != nil {
|
||||
ctx.Error("wrong inputs")
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
switch ctx.Method() {
|
||||
case "GET":
|
||||
ctx.Data.Data = req
|
||||
ctx.Exec()
|
||||
case "POST":
|
||||
if !ctx.CheckXsrf() {
|
||||
return
|
||||
}
|
||||
pin := ctx.Form("pin")
|
||||
if pin == "" {
|
||||
ctx.Error("empty pin")
|
||||
return
|
||||
}
|
||||
ctx.Data.Data = req
|
||||
secret := req.Secret
|
||||
if u.Secret != "" {
|
||||
secret = u.Secret
|
||||
}
|
||||
valid := otp.Validate(pin, secret)
|
||||
if ctx.User() != req.Username && ctx.Admin() {
|
||||
valid = true
|
||||
}
|
||||
|
||||
if !valid {
|
||||
ctx.Error("validation failed")
|
||||
return
|
||||
}
|
||||
if err := ctx.Srv.DB.UpdateUserSecret(req.Username, req.Secret); err != nil {
|
||||
ctx.Error(err)
|
||||
return
|
||||
}
|
||||
ctx.Redirect("/user/edit/"+req.Username, 302)
|
||||
}
|
||||
}
|
||||
|
||||
func userUnlockHandler(ctx *Context) {
|
||||
user := ctx.Var("user")
|
||||
if err := ctx.Srv.DB.UnlockUser(user); err != nil {
|
||||
|
|
|
|||
|
|
@ -24,11 +24,6 @@ var routes = []route{
|
|||
staticHandler,
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/login",
|
||||
loginHandler,
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/search",
|
||||
searchHandler,
|
||||
|
|
@ -45,6 +40,17 @@ var routes = []route{
|
|||
allHandler,
|
||||
[]string{"GET"},
|
||||
},
|
||||
{
|
||||
"/login",
|
||||
loginHandler,
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/totp",
|
||||
totpAuthHandler(
|
||||
loginTotpHandler),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/blacklist",
|
||||
authHandler(
|
||||
|
|
@ -81,6 +87,12 @@ var routes = []route{
|
|||
userEditHandler),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/user/totp/{user:[a-zA-Z0-9]+$}",
|
||||
userAuthHandler(
|
||||
userTotpHandler),
|
||||
[]string{"GET", "POST"},
|
||||
},
|
||||
{
|
||||
"/logout",
|
||||
logoutHandler,
|
||||
|
|
|
|||
|
|
@ -94,6 +94,10 @@ func (s *HTTPServer) buildRoutes() http.Handler {
|
|||
}
|
||||
func (s *HTTPServer) contextWrapper(h ctxHandler) http.HandlerFunc {
|
||||
return func(w http.ResponseWriter, r *http.Request) {
|
||||
h(newContext(w, r, s))
|
||||
ctx := newContext(w, r, s)
|
||||
if ctx == nil {
|
||||
return
|
||||
}
|
||||
h(ctx)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -112,6 +112,29 @@ const (
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}`
|
||||
loginTotp = `{{define "body"}}
|
||||
<div class="page-header">
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-primary mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>{{.BodyTitle}}</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="form-horizontal" action="/totp" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="pin">PIN</label>
|
||||
<input class="form-control input-sm" type="text" id="pin" name="pin" autocomplete="off" autofocus required>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary" type="submit">Verify</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}`
|
||||
menu = `{{define "menu"}}
|
||||
<ul class="navbar-nav">
|
||||
|
|
@ -458,6 +481,11 @@ const (
|
|||
<div class="card border-primary mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>{{.BodyTitle}}</strong>
|
||||
{{if .Data.Secret}}
|
||||
<a href="/user/totp/{{.Data.Username}}" class="btn btn-sm btn-danger">Disable TOTP</a>
|
||||
{{else}}
|
||||
<a href="/user/totp/{{.Data.Username}}" class="btn btn-sm btn-success">Enable TOTP</a>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="form-horizontal" action="/user/edit/{{.Data.Username}}" method="post">
|
||||
|
|
@ -496,8 +524,8 @@ const (
|
|||
<form class="form-horizontal" action="/user/unlock/{{.Data.Username}}" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="unlock">Admin Privileges</label>
|
||||
<button class="btn btn-sm btn-primary" type="submit" id="unlock">Unlock</button>
|
||||
<label class="col-form-label" for="unlock">Locked</label>
|
||||
<button class="btn btn-sm btn-warning" type="submit" id="unlock">Unlock</button>
|
||||
</div>
|
||||
</form>
|
||||
{{end}}
|
||||
|
|
@ -536,7 +564,7 @@ const (
|
|||
{{$admin := .Admin}}
|
||||
{{range $item := .Data}}
|
||||
<tr>
|
||||
<td>{{$item.Username}}</td>
|
||||
<td>{{$item.Username}} {{if $item.Secret}}<span class="badge badge-success">TOTP</span>{{end}}</td>
|
||||
{{if $item.Admin}}<td>yes</td>{{else}}<td>no</td>{{end}}
|
||||
<td>
|
||||
<div class="btn-group">
|
||||
|
|
@ -590,6 +618,43 @@ const (
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}`
|
||||
userTotp = `{{define "body"}}
|
||||
{{if .Data}}
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-primary mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>{{.BodyTitle}}</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="form-horizontal" action="/user/totp/{{.Data.Username}}" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
{{if .Data.Secret}}
|
||||
<input type="hidden" name="url" value="{{.Data.URL}}">
|
||||
<div class="form-group">
|
||||
<img src="{{.Data.Image}}" alt="{{.Data.Secret}}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="secret">Secret</label>
|
||||
<p id="secret">{{.Data.Secret}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="pin">PIN</label>
|
||||
<input class="form-control input-sm" type="text" id="pin" name="pin" autocomplete="off" autofocus required>
|
||||
</div>
|
||||
{{if .Data.Secret}}
|
||||
<button class="btn btn-sm btn-success" type="submit">Enable</button>
|
||||
{{else}}
|
||||
<button class="btn btn-sm btn-danger" type="submit">Disable</button>
|
||||
{{end}}
|
||||
<a href="/user/edit/{{.Data.Username}}" class="btn btn-sm btn-primary">Back</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}`
|
||||
bootstrapCSS = `/*!
|
||||
* Bootswatch v4.1.3
|
||||
|
|
@ -769,9 +834,10 @@ func (s *HTTPServer) loadTemplates() {
|
|||
s.res["bootstrap.css"] = []byte(bootstrapCSS)
|
||||
s.res["custom.css"] = []byte(customCSS)
|
||||
|
||||
// pages.go
|
||||
// pages
|
||||
s.templ["notFoundHandler"] = parse(index, menu, notFound)
|
||||
s.templ["loginHandler"] = parse(index, menu, login)
|
||||
s.templ["loginTotpHandler"] = parse(index, menu, loginTotp)
|
||||
s.templ["searchHandler"] = parse(index, menu, search)
|
||||
s.templ["allHandler"] = parse(index, menu, all)
|
||||
s.templ["pageHandler"] = parse(index, menu, page)
|
||||
|
|
@ -782,11 +848,13 @@ func (s *HTTPServer) loadTemplates() {
|
|||
s.templ["pageShareHandler"] = parse(index, menu, pageShare)
|
||||
s.templ["pageBlacklistHandler"] = parse(index, menu, pageBlacklist)
|
||||
s.templ["pageDelHandler"] = parse(index, menu, pageDel)
|
||||
// users.go
|
||||
// users
|
||||
s.templ["userHandler"] = parse(index, menu, user)
|
||||
s.templ["userNewHandler"] = parse(index, menu, userNew)
|
||||
s.templ["userEditHandler"] = parse(index, menu, userEdit)
|
||||
s.templ["userDelHandler"] = parse(index, menu, userDel)
|
||||
// totp
|
||||
s.templ["userTotpHandler"] = parse(index, menu, userTotp)
|
||||
|
||||
if errf {
|
||||
log.Fatal("parse: ", errors.New("parsing templates failed"))
|
||||
|
|
|
|||
|
|
@ -68,9 +68,10 @@ func (s *HTTPServer) loadTemplates() {
|
|||
s.res["bootstrap.css"] = []byte(bootstrapCSS)
|
||||
s.res["custom.css"] = []byte(customCSS)
|
||||
|
||||
// pages.go
|
||||
// pages
|
||||
s.templ["notFoundHandler"] = parse(index, menu, notFound)
|
||||
s.templ["loginHandler"] = parse(index, menu, login)
|
||||
s.templ["loginTotpHandler"] = parse(index, menu, loginTotp)
|
||||
s.templ["searchHandler"] = parse(index, menu, search)
|
||||
s.templ["allHandler"] = parse(index, menu, all)
|
||||
s.templ["pageHandler"] = parse(index, menu, page)
|
||||
|
|
@ -81,11 +82,13 @@ func (s *HTTPServer) loadTemplates() {
|
|||
s.templ["pageShareHandler"] = parse(index, menu, pageShare)
|
||||
s.templ["pageBlacklistHandler"] = parse(index, menu, pageBlacklist)
|
||||
s.templ["pageDelHandler"] = parse(index, menu, pageDel)
|
||||
// users.go
|
||||
// users
|
||||
s.templ["userHandler"] = parse(index, menu, user)
|
||||
s.templ["userNewHandler"] = parse(index, menu, userNew)
|
||||
s.templ["userEditHandler"] = parse(index, menu, userEdit)
|
||||
s.templ["userDelHandler"] = parse(index, menu, userDel)
|
||||
// totp
|
||||
s.templ["userTotpHandler"] = parse(index, menu, userTotp)
|
||||
|
||||
if errf {
|
||||
log.Fatal("parse: ", errors.New("parsing templates failed"))
|
||||
|
|
|
|||
23
templates/loginTotp.html
Normal file
23
templates/loginTotp.html
Normal 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-primary mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>{{.BodyTitle}}</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="form-horizontal" action="/totp" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="pin">PIN</label>
|
||||
<input class="form-control input-sm" type="text" id="pin" name="pin" autocomplete="off" autofocus required>
|
||||
</div>
|
||||
<button class="btn btn-sm btn-primary" type="submit">Verify</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
|
|
@ -27,7 +27,7 @@
|
|||
{{$admin := .Admin}}
|
||||
{{range $item := .Data}}
|
||||
<tr>
|
||||
<td>{{$item.Username}}</td>
|
||||
<td>{{$item.Username}} {{if $item.Secret}}<span class="badge badge-success">TOTP</span>{{end}}</td>
|
||||
{{if $item.Admin}}<td>yes</td>{{else}}<td>no</td>{{end}}
|
||||
<td>
|
||||
<div class="btn-group">
|
||||
|
|
|
|||
|
|
@ -5,6 +5,11 @@
|
|||
<div class="card border-primary mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>{{.BodyTitle}}</strong>
|
||||
{{if .Data.Secret}}
|
||||
<a href="/user/totp/{{.Data.Username}}" class="btn btn-sm btn-danger">Disable TOTP</a>
|
||||
{{else}}
|
||||
<a href="/user/totp/{{.Data.Username}}" class="btn btn-sm btn-success">Enable TOTP</a>
|
||||
{{end}}
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="form-horizontal" action="/user/edit/{{.Data.Username}}" method="post">
|
||||
|
|
@ -43,8 +48,8 @@
|
|||
<form class="form-horizontal" action="/user/unlock/{{.Data.Username}}" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="unlock">Admin Privileges</label>
|
||||
<button class="btn btn-sm btn-primary" type="submit" id="unlock">Unlock</button>
|
||||
<label class="col-form-label" for="unlock">Locked</label>
|
||||
<button class="btn btn-sm btn-warning" type="submit" id="unlock">Unlock</button>
|
||||
</div>
|
||||
</form>
|
||||
{{end}}
|
||||
|
|
|
|||
37
templates/userTotp.html
Normal file
37
templates/userTotp.html
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
{{define "body"}}
|
||||
{{if .Data}}
|
||||
<div class="row">
|
||||
<div class="col-xs-4 offset-4">
|
||||
<div class="card border-primary mx-auto">
|
||||
<div class="card-header">
|
||||
<strong>{{.BodyTitle}}</strong>
|
||||
</div>
|
||||
<div class="card-body">
|
||||
<form class="form-horizontal" action="/user/totp/{{.Data.Username}}" method="post">
|
||||
<input type="hidden" name="token" value="{{.Token}}">
|
||||
{{if .Data.Secret}}
|
||||
<input type="hidden" name="url" value="{{.Data.URL}}">
|
||||
<div class="form-group">
|
||||
<img src="{{.Data.Image}}" alt="{{.Data.Secret}}">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="secret">Secret</label>
|
||||
<p id="secret">{{.Data.Secret}}</p>
|
||||
</div>
|
||||
{{end}}
|
||||
<div class="form-group">
|
||||
<label class="col-form-label" for="pin">PIN</label>
|
||||
<input class="form-control input-sm" type="text" id="pin" name="pin" autocomplete="off" autofocus required>
|
||||
</div>
|
||||
{{if .Data.Secret}}
|
||||
<button class="btn btn-sm btn-success" type="submit">Enable</button>
|
||||
{{else}}
|
||||
<button class="btn btn-sm btn-danger" type="submit">Disable</button>
|
||||
{{end}}
|
||||
<a href="/user/edit/{{.Data.Username}}" class="btn btn-sm btn-primary">Back</a>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{{end}}
|
||||
{{end}}
|
||||
Loading…
Add table
Add a link
Reference in a new issue