first working version
This commit is contained in:
parent
261ebed165
commit
c9dfecc519
21 changed files with 296 additions and 178 deletions
1
.gitignore
vendored
1
.gitignore
vendored
|
|
@ -2,3 +2,4 @@
|
||||||
tmp
|
tmp
|
||||||
run.sh
|
run.sh
|
||||||
TODO.txt
|
TODO.txt
|
||||||
|
consent.json
|
||||||
|
|
|
||||||
|
|
@ -31,6 +31,6 @@ type Config struct {
|
||||||
HydraAdminURL string `json:"hydra_admin_url"`
|
HydraAdminURL string `json:"hydra_admin_url"`
|
||||||
Issuer string `json:"issuer"`
|
Issuer string `json:"issuer"`
|
||||||
Version string `json:"-"`
|
Version string `json:"-"`
|
||||||
Admin bool `json:"admin,omitempty"`
|
AdminDisabled bool `json:"admin_disabled,omitempty"`
|
||||||
SecureCookie bool `json:"secure_cookie,omitempty"`
|
SecureCookie bool `json:"secure_cookie,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -9,6 +9,7 @@ type User struct {
|
||||||
Created string
|
Created string
|
||||||
Secret string
|
Secret string
|
||||||
Admin bool
|
Admin bool
|
||||||
|
Disabled bool
|
||||||
Locked int
|
Locked int
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
10
pkg/db/db.go
10
pkg/db/db.go
|
|
@ -21,12 +21,11 @@ const (
|
||||||
|
|
||||||
type DB struct {
|
type DB struct {
|
||||||
log logr.Logger
|
log logr.Logger
|
||||||
admin bool
|
|
||||||
store store.Store
|
store store.Store
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewPlain(log logr.Logger, cfg *core.Config) (db *DB, err error) {
|
func NewPlain(log logr.Logger, cfg *core.Config) (db *DB, err error) {
|
||||||
db = &DB{log: log, admin: cfg.Admin}
|
db = &DB{log: log}
|
||||||
dbFile := filepath.Join(cfg.DataDir, storeFile)
|
dbFile := filepath.Join(cfg.DataDir, storeFile)
|
||||||
//err = godrop.Unveil(dbFile, "rwc")
|
//err = godrop.Unveil(dbFile, "rwc")
|
||||||
//if err != nil {
|
//if err != nil {
|
||||||
|
|
@ -42,7 +41,12 @@ func New(log logr.Logger, cfg *core.Config) (db *DB, err error) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if db.UserExists(defUser) != nil {
|
if db.UserExists(defUser) != nil {
|
||||||
err = db.CreateUser(defUser, defEmail, defPassword, true)
|
err = db.CreateUser(defUser, defEmail, defPassword, true, cfg.AdminDisabled)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = db.AdminUpdateUser(defUser, "", true, cfg.AdminDisabled)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -62,7 +62,7 @@ func (db *DB) UserExists(username string) error {
|
||||||
return db.store.Get(userPrefix+username, nil)
|
return db.store.Get(userPrefix+username, nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) CreateUser(username, email, password string, admin bool) error {
|
func (db *DB) CreateUser(username, email, password string, admin, disabled bool) error {
|
||||||
username = userRe.ReplaceAllString(username, "")
|
username = userRe.ReplaceAllString(username, "")
|
||||||
if err := db.UserExists(username); err == nil {
|
if err := db.UserExists(username); err == nil {
|
||||||
return errUserExists
|
return errUserExists
|
||||||
|
|
@ -77,18 +77,19 @@ func (db *DB) CreateUser(username, email, password string, admin bool) error {
|
||||||
Password: string(hash),
|
Password: string(hash),
|
||||||
Created: strconv.Itoa(int(jwt.Now())),
|
Created: strconv.Itoa(int(jwt.Now())),
|
||||||
Admin: admin,
|
Admin: admin,
|
||||||
|
Disabled: disabled,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
||||||
}
|
}
|
||||||
|
if u.Disabled {
|
||||||
|
err = errors.New("db: user disabled")
|
||||||
|
return
|
||||||
|
}
|
||||||
if u.Locked == 3 {
|
if u.Locked == 3 {
|
||||||
err = errUserLocked
|
err = errUserLocked
|
||||||
return
|
return
|
||||||
|
|
@ -185,7 +186,7 @@ func (db *DB) ResetAdmin() error {
|
||||||
return db.UpdateUserPassword(defUser, defPassword)
|
return db.UpdateUserPassword(defUser, defPassword)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (db *DB) AdminUpdateUser(username, password string, admin bool) error {
|
func (db *DB) AdminUpdateUser(username, password string, admin, disabled bool) error {
|
||||||
if username == defUser && !admin {
|
if username == defUser && !admin {
|
||||||
return errors.New("user '" + defUser + "' cannot lose admin privileges")
|
return errors.New("user '" + defUser + "' cannot lose admin privileges")
|
||||||
}
|
}
|
||||||
|
|
@ -194,6 +195,7 @@ func (db *DB) AdminUpdateUser(username, password string, admin bool) error {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
u.Admin = admin
|
u.Admin = admin
|
||||||
|
u.Disabled = disabled
|
||||||
if password != "" {
|
if password != "" {
|
||||||
hash, err := bcrypt.GenerateFromPassword(validatePassword(password), bcryptCost)
|
hash, err := bcrypt.GenerateFromPassword(validatePassword(password), bcryptCost)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
||||||
|
|
@ -4,7 +4,6 @@ package server
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
"errors"
|
||||||
"html/template"
|
"html/template"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
@ -87,7 +86,7 @@ type loginData struct {
|
||||||
|
|
||||||
type consentData struct {
|
type consentData struct {
|
||||||
ConsentChallenge string
|
ConsentChallenge string
|
||||||
Info *consentInfo
|
Info *client.ConsentRequest
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Context) Exec() {
|
func (c *Context) Exec() {
|
||||||
|
|
@ -302,155 +301,117 @@ func (c *Context) Admin() bool {
|
||||||
return c.Token.Claims.GetBool(adminClaim)
|
return c.Token.Claims.GetBool(adminClaim)
|
||||||
}
|
}
|
||||||
|
|
||||||
type respBody struct {
|
|
||||||
URL string `json:"redirect_to,omitempty"`
|
|
||||||
Error string `json:"error,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Context) ApproveLogin(user, challenge string) (rdr string, err error) {
|
func (c *Context) ApproveLogin(user, challenge string) (rdr string, err error) {
|
||||||
req := c.Srv.API.AdminApi.AcceptLoginRequest(context.Background())
|
cr, resp, err := c.Srv.API.AdminApi.AcceptLoginRequest(context.Background()).
|
||||||
req.AcceptLoginRequest(*client.NewAcceptLoginRequest(user))
|
AcceptLoginRequest(*client.NewAcceptLoginRequest(user)).
|
||||||
req.LoginChallenge(challenge)
|
LoginChallenge(challenge).
|
||||||
_, resp, err := req.Execute()
|
Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
rb := new(respBody)
|
rdr = cr.RedirectTo
|
||||||
err = json.NewDecoder(resp.Body).Decode(rb)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if rb.Error != "" {
|
|
||||||
err = errors.New(rb.Error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
rdr = rb.URL
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Context) ApproveConsent(challenge string) (rdr string, err error) {
|
func (c *Context) ApproveConsent(challenge string) (rdr string, err error) {
|
||||||
req := c.Srv.API.AdminApi.AcceptConsentRequest(context.Background()).
|
ci, err := c.Consent(challenge)
|
||||||
AcceptConsentRequest(*client.NewAcceptConsentRequest()).
|
if err != nil {
|
||||||
ConsentChallenge(challenge)
|
return
|
||||||
_, resp, err := req.Execute()
|
}
|
||||||
|
accept := client.NewAcceptConsentRequest()
|
||||||
|
accept.GrantScope = ci.RequestedScope
|
||||||
|
accept.GrantAccessTokenAudience = ci.RequestedAccessTokenAudience
|
||||||
|
accept.Remember = new(bool)
|
||||||
|
*accept.Remember = true
|
||||||
|
cr, resp, err := c.Srv.API.AdminApi.AcceptConsentRequest(context.Background()).
|
||||||
|
AcceptConsentRequest(*accept).
|
||||||
|
ConsentChallenge(challenge).
|
||||||
|
Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
rb := new(respBody)
|
rdr = cr.RedirectTo
|
||||||
err = json.NewDecoder(resp.Body).Decode(rb)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if rb.Error != "" {
|
|
||||||
err = errors.New(rb.Error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
rdr = rb.URL
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Context) RejectConsent(challenge string) (rdr string, err error) {
|
func (c *Context) RejectConsent(challenge string) (rdr string, err error) {
|
||||||
req := c.Srv.API.AdminApi.RejectConsentRequest(context.Background()).
|
cr, resp, err := c.Srv.API.AdminApi.RejectConsentRequest(context.Background()).
|
||||||
RejectRequest(*client.NewRejectRequest()).
|
RejectRequest(*client.NewRejectRequest()).
|
||||||
ConsentChallenge(challenge)
|
ConsentChallenge(challenge).
|
||||||
_, resp, err := req.Execute()
|
Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
rb := new(respBody)
|
rdr = cr.RedirectTo
|
||||||
err = json.NewDecoder(resp.Body).Decode(rb)
|
|
||||||
if err != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if rb.Error != "" {
|
|
||||||
err = errors.New(rb.Error)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
rdr = rb.URL
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Context) Consent(challenge string) (ci *consentInfo, err error) {
|
func (c *Context) Consent(challenge string) (cr *client.ConsentRequest, err error) {
|
||||||
req := c.Srv.API.AdminApi.GetConsentRequest(context.Background()).
|
cr, resp, err := c.Srv.API.AdminApi.GetConsentRequest(context.Background()).
|
||||||
ConsentChallenge(challenge)
|
ConsentChallenge(challenge).
|
||||||
_, resp, err := req.Execute()
|
Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer resp.Body.Close()
|
defer resp.Body.Close()
|
||||||
ci = new(consentInfo)
|
return
|
||||||
err = json.NewDecoder(resp.Body).Decode(ci)
|
}
|
||||||
|
|
||||||
|
type sess struct {
|
||||||
|
ID string
|
||||||
|
Time string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Context) Sessions(user string) (m map[string]sess, err error) {
|
||||||
|
ss, resp, err := c.Srv.API.AdminApi.ListSubjectConsentSessions(context.Background()).
|
||||||
|
Subject(user).
|
||||||
|
Execute()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if ci.Error != "" {
|
defer resp.Body.Close()
|
||||||
err = errors.New(ci.Error)
|
m = make(map[string]sess)
|
||||||
|
for _, s := range ss {
|
||||||
|
name := s.ConsentRequest.Client.ClientName
|
||||||
|
if name == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
n := *name
|
||||||
|
cid := s.ConsentRequest.Client.ClientId
|
||||||
|
if cid == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
id := *cid
|
||||||
|
if _, ok := m[n]; !ok {
|
||||||
|
m[n] = sess{
|
||||||
|
id,
|
||||||
|
s.HandledAt.Format(timeFmt),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
type consentInfo struct {
|
func (c *Context) RevokeSession(user, client string, all bool) (err error) {
|
||||||
Acr string `json:"acr"`
|
if client == "" && !all {
|
||||||
Amr []string `json:"amr"`
|
return
|
||||||
Challenge string `json:"challenge"`
|
}
|
||||||
Error string `json:"error,omitempty"`
|
req := c.Srv.API.AdminApi.RevokeConsentSessions(context.Background()).
|
||||||
Client struct {
|
Subject(user)
|
||||||
AllowedCorsOrigins []string `json:"allowed_cors_origins"`
|
if all {
|
||||||
Audience []string `json:"audience"`
|
req = req.All(all)
|
||||||
BackchannelLogoutSessionRequired bool `json:"backchannel_logout_session_required"`
|
} else {
|
||||||
BackchannelLogoutURI string `json:"backchannel_logout_uri"`
|
req = req.Client(client)
|
||||||
ClientID string `json:"client_id"`
|
}
|
||||||
ClientName string `json:"client_name"`
|
resp, err := req.Execute()
|
||||||
ClientSecret string `json:"client_secret"`
|
if err != nil {
|
||||||
ClientSecretExpiresAt int `json:"client_secret_expires_at"`
|
return
|
||||||
ClientURI string `json:"client_uri"`
|
}
|
||||||
Contacts []string `json:"contacts"`
|
defer resp.Body.Close()
|
||||||
CreatedAt time.Time `json:"created_at"`
|
return
|
||||||
FrontchannelLogoutSessionRequired bool `json:"frontchannel_logout_session_required"`
|
|
||||||
FrontchannelLogoutURI string `json:"frontchannel_logout_uri"`
|
|
||||||
GrantTypes []string `json:"grant_types"`
|
|
||||||
Jwks struct {
|
|
||||||
} `json:"jwks"`
|
|
||||||
JwksURI string `json:"jwks_uri"`
|
|
||||||
LogoURI string `json:"logo_uri"`
|
|
||||||
Metadata struct {
|
|
||||||
} `json:"metadata"`
|
|
||||||
Owner string `json:"owner"`
|
|
||||||
PolicyURI string `json:"policy_uri"`
|
|
||||||
PostLogoutRedirectUris []string `json:"post_logout_redirect_uris"`
|
|
||||||
RedirectUris []string `json:"redirect_uris"`
|
|
||||||
RegistrationAccessToken string `json:"registration_access_token"`
|
|
||||||
RegistrationClientURI string `json:"registration_client_uri"`
|
|
||||||
RequestObjectSigningAlg string `json:"request_object_signing_alg"`
|
|
||||||
RequestUris []string `json:"request_uris"`
|
|
||||||
ResponseTypes []string `json:"response_types"`
|
|
||||||
Scope string `json:"scope"`
|
|
||||||
SectorIdentifierURI string `json:"sector_identifier_uri"`
|
|
||||||
SubjectType string `json:"subject_type"`
|
|
||||||
TokenEndpointAuthMethod string `json:"token_endpoint_auth_method"`
|
|
||||||
TokenEndpointAuthSigningAlg string `json:"token_endpoint_auth_signing_alg"`
|
|
||||||
TosURI string `json:"tos_uri"`
|
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
|
||||||
UserinfoSignedResponseAlg string `json:"userinfo_signed_response_alg"`
|
|
||||||
} `json:"client"`
|
|
||||||
LoginChallenge string `json:"login_challenge"`
|
|
||||||
LoginSessionID string `json:"login_session_id"`
|
|
||||||
OidcContext struct {
|
|
||||||
AcrValues []string `json:"acr_values"`
|
|
||||||
Display string `json:"display"`
|
|
||||||
IDTokenHintClaims struct {
|
|
||||||
} `json:"id_token_hint_claims"`
|
|
||||||
LoginHint string `json:"login_hint"`
|
|
||||||
UILocales []string `json:"ui_locales"`
|
|
||||||
} `json:"oidc_context"`
|
|
||||||
RequestURL string `json:"request_url"`
|
|
||||||
RequestedAccessTokenAudience []string `json:"requested_access_token_audience"`
|
|
||||||
RequestedScope []string `json:"requested_scope"`
|
|
||||||
Skip bool `json:"skip"`
|
|
||||||
Subject string `json:"subject"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ctxHandler func(*Context)
|
type ctxHandler func(*Context)
|
||||||
|
|
|
||||||
|
|
@ -106,8 +106,8 @@ func indexHandler(ctx *Context) {
|
||||||
ctx.Redirect(login, http.StatusFound)
|
ctx.Redirect(login, http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func profileHandler(ctx *Context) {
|
func sessionsHandler(ctx *Context) {
|
||||||
ctx.Redirect(uroot+"edit/"+ctx.User(), http.StatusFound)
|
ctx.Redirect(uroot+"sessions/"+ctx.User(), http.StatusFound)
|
||||||
}
|
}
|
||||||
|
|
||||||
func loginHandler(ctx *Context) {
|
func loginHandler(ctx *Context) {
|
||||||
|
|
@ -123,7 +123,7 @@ func loginHandler(ctx *Context) {
|
||||||
BodyTitle: "Login",
|
BodyTitle: "Login",
|
||||||
}
|
}
|
||||||
if ctx.LoggedOn() {
|
if ctx.LoggedOn() {
|
||||||
rdr := profile
|
rdr := sessions
|
||||||
if data.LoginChallenge != "" {
|
if data.LoginChallenge != "" {
|
||||||
rdr, err = ctx.ApproveLogin(ctx.User(), data.LoginChallenge)
|
rdr, err = ctx.ApproveLogin(ctx.User(), data.LoginChallenge)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -172,7 +172,7 @@ func loginHandler(ctx *Context) {
|
||||||
}
|
}
|
||||||
log.Info("user logged in", "user", data.User)
|
log.Info("user logged in", "user", data.User)
|
||||||
ctx.Login(u, remember)
|
ctx.Login(u, remember)
|
||||||
rdr := profile
|
rdr := sessions
|
||||||
if data.LoginChallenge != "" {
|
if data.LoginChallenge != "" {
|
||||||
rdr, err = ctx.ApproveLogin(data.User, data.LoginChallenge)
|
rdr, err = ctx.ApproveLogin(data.User, data.LoginChallenge)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -187,7 +187,7 @@ func loginHandler(ctx *Context) {
|
||||||
|
|
||||||
func loginTotpHandler(ctx *Context) {
|
func loginTotpHandler(ctx *Context) {
|
||||||
if ctx.LoggedOn() {
|
if ctx.LoggedOn() {
|
||||||
ctx.Redirect(profile, http.StatusFound)
|
ctx.Redirect(sessions, http.StatusFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Template("loginTotpHandler")
|
ctx.Template("loginTotpHandler")
|
||||||
|
|
@ -222,7 +222,7 @@ func loginTotpHandler(ctx *Context) {
|
||||||
}
|
}
|
||||||
log.Info("totp successful")
|
log.Info("totp successful")
|
||||||
ctx.Login(u, ctx.Remember())
|
ctx.Login(u, ctx.Remember())
|
||||||
rdr := root
|
rdr := sessions
|
||||||
if data.LoginChallenge != "" {
|
if data.LoginChallenge != "" {
|
||||||
rdr, err = ctx.ApproveLogin(user, data.LoginChallenge)
|
rdr, err = ctx.ApproveLogin(user, data.LoginChallenge)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
@ -239,21 +239,30 @@ func loginTotpHandler(ctx *Context) {
|
||||||
func consentHandler(ctx *Context) {
|
func consentHandler(ctx *Context) {
|
||||||
ctx.Template("consentHandler")
|
ctx.Template("consentHandler")
|
||||||
ctx.Data = webData{
|
ctx.Data = webData{
|
||||||
Title: "Consent",
|
Title: "Authorize",
|
||||||
BodyTitle: "Consent",
|
BodyTitle: "Authorize",
|
||||||
}
|
}
|
||||||
var data consentData
|
var data consentData
|
||||||
switch ctx.Method() {
|
switch ctx.Method() {
|
||||||
case "GET":
|
case "GET":
|
||||||
data.ConsentChallenge = ctx.Query("consent_challenge")
|
data.ConsentChallenge = ctx.Query("consent_challenge")
|
||||||
ctx.Data.Data = data
|
ctx.Data.Data = data
|
||||||
ci, err := ctx.Consent(data.ConsentChallenge)
|
cr, err := ctx.Consent(data.ConsentChallenge)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.log.Error(err, "error getting consent info")
|
ctx.log.Error(err, "error getting consent info")
|
||||||
ctx.Error("error getting consent info")
|
ctx.Error("error getting consent info")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
data.Info = ci
|
if cr.GetSkip() {
|
||||||
|
rdr, err := ctx.ApproveConsent(data.ConsentChallenge)
|
||||||
|
if err != nil {
|
||||||
|
ctx.log.Error(err, "error approving consent")
|
||||||
|
ctx.Error("error approving consent")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Redirect(rdr, http.StatusFound)
|
||||||
|
}
|
||||||
|
data.Info = cr
|
||||||
ctx.Data.Data = data
|
ctx.Data.Data = data
|
||||||
ctx.Exec()
|
ctx.Exec()
|
||||||
case "POST":
|
case "POST":
|
||||||
|
|
@ -321,11 +330,8 @@ func userNewHandler(ctx *Context) {
|
||||||
email := ctx.Form("email")
|
email := ctx.Form("email")
|
||||||
password := ctx.Form("password")
|
password := ctx.Form("password")
|
||||||
repeat := ctx.Form("repeat")
|
repeat := ctx.Form("repeat")
|
||||||
admin := ctx.Form("admin")
|
admin := ctx.Form("admin") == "0"
|
||||||
adm := false
|
disabled := ctx.Form("disabled") == "0"
|
||||||
if admin == "0" {
|
|
||||||
adm = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if user == "" || email == "" || password == "" {
|
if user == "" || email == "" || password == "" {
|
||||||
ctx.Error("empty user, email or password")
|
ctx.Error("empty user, email or password")
|
||||||
|
|
@ -335,11 +341,11 @@ func userNewHandler(ctx *Context) {
|
||||||
ctx.Error("passwords do not match")
|
ctx.Error("passwords do not match")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if err := ctx.Srv.DB.CreateUser(user, email, password, adm); err != nil {
|
if err := ctx.Srv.DB.CreateUser(user, email, password, admin, disabled); err != nil {
|
||||||
ctx.Error(err)
|
ctx.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.log.Info("user created", "user", user, "actor", ctx.User())
|
ctx.log.Info("user created", "user", user, "actor", ctx.User(), "admin", admin, "disabled", disabled)
|
||||||
ctx.Redirect(users, http.StatusFound)
|
ctx.Redirect(users, http.StatusFound)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -364,25 +370,27 @@ func userEditHandler(ctx *Context) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
user := ctx.Var("user")
|
user := ctx.Var("user")
|
||||||
log := ctx.log.WithValues("user", user, "actor", ctx.User())
|
self := ctx.User()
|
||||||
|
log := ctx.log.WithValues("user", user, "actor", self)
|
||||||
password := ctx.Form("password")
|
password := ctx.Form("password")
|
||||||
repeat := ctx.Form("repeat")
|
repeat := ctx.Form("repeat")
|
||||||
admin := ctx.Form("admin")
|
admin := ctx.Form("admin") == "0"
|
||||||
adm := false
|
disabled := ctx.Form("disabled") == "0"
|
||||||
if admin == "0" {
|
|
||||||
adm = true
|
|
||||||
}
|
|
||||||
|
|
||||||
if password != repeat {
|
if password != repeat {
|
||||||
ctx.Error("passwords do not match")
|
ctx.Error("passwords do not match")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if ctx.Admin() {
|
if ctx.Admin() {
|
||||||
if err := ctx.Srv.DB.AdminUpdateUser(user, password, adm); err != nil {
|
if err := ctx.Srv.DB.AdminUpdateUser(user, password, admin, disabled); err != nil {
|
||||||
ctx.Error(err)
|
ctx.Error(err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
log.Info("user updated", "admin", adm)
|
log.Info("user updated", "admin", admin, "disabled", disabled)
|
||||||
|
if user == self {
|
||||||
|
logoutHandler(ctx)
|
||||||
|
return
|
||||||
|
}
|
||||||
ctx.Redirect(users, http.StatusFound)
|
ctx.Redirect(users, http.StatusFound)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
@ -461,6 +469,40 @@ func userTotpHandler(ctx *Context) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func userSessionsHandler(ctx *Context) {
|
||||||
|
ctx.Template("userSessionsHandler")
|
||||||
|
ctx.Data = webData{
|
||||||
|
Title: "Sessions",
|
||||||
|
BodyTitle: "Sessions",
|
||||||
|
}
|
||||||
|
user := ctx.Var("user")
|
||||||
|
log := ctx.log.WithValues("user", user)
|
||||||
|
switch ctx.Method() {
|
||||||
|
case "GET":
|
||||||
|
s, err := ctx.Sessions(user)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err, "error getting sessions")
|
||||||
|
ctx.Error("error getting sessions")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Data.Data = s
|
||||||
|
ctx.Exec()
|
||||||
|
case "POST":
|
||||||
|
if !ctx.CheckXsrf() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
client := ctx.Form("client")
|
||||||
|
all := ctx.Form("all") == "all"
|
||||||
|
err := ctx.RevokeSession(user, client, all)
|
||||||
|
if err != nil {
|
||||||
|
log.Error(err, "error revoking session", "client", client, "all", all)
|
||||||
|
ctx.Error("error revoking session")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
sessionsHandler(ctx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func userUnlockHandler(ctx *Context) {
|
func userUnlockHandler(ctx *Context) {
|
||||||
user := ctx.Var("user")
|
user := ctx.Var("user")
|
||||||
log := ctx.log.WithValues("user", user, "actor", ctx.User())
|
log := ctx.log.WithValues("user", user, "actor", ctx.User())
|
||||||
|
|
@ -502,7 +544,7 @@ func userDelHandler(ctx *Context) {
|
||||||
log.Info("delete successful")
|
log.Info("delete successful")
|
||||||
}
|
}
|
||||||
if user == self {
|
if user == self {
|
||||||
ctx.Redirect(logout, http.StatusFound)
|
logoutHandler(ctx)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
ctx.Redirect(users, http.StatusFound)
|
ctx.Redirect(users, http.StatusFound)
|
||||||
|
|
|
||||||
|
|
@ -10,4 +10,6 @@ const (
|
||||||
day = time.Hour * 24
|
day = time.Hour * 24
|
||||||
min = time.Second * 30
|
min = time.Second * 30
|
||||||
max = day * 90
|
max = day * 90
|
||||||
|
|
||||||
|
timeFmt = "2006-01-02 15:04:05"
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -9,20 +9,21 @@ type route struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
root = "/"
|
root = "/"
|
||||||
login = root + "login"
|
login = root + "login"
|
||||||
totp = root + "totp"
|
totp = root + "totp"
|
||||||
profile = root + "profile"
|
sessions = root + "sessions"
|
||||||
consent = root + "consent"
|
consent = root + "consent"
|
||||||
users = root + "users"
|
users = root + "users"
|
||||||
logout = root + "logout"
|
logout = root + "logout"
|
||||||
uroot = root + "user/"
|
uroot = root + "user/"
|
||||||
|
|
||||||
userNew = uroot + "new"
|
userNew = uroot + "new"
|
||||||
userEdit = uroot + "edit/{user:[a-zA-Z0-9]+$}"
|
userEdit = uroot + "edit/{user:[a-zA-Z0-9]+$}"
|
||||||
userDel = uroot + "del/{user:[a-zA-Z0-9]+$}"
|
userDel = uroot + "del/{user:[a-zA-Z0-9]+$}"
|
||||||
userTotp = uroot + "totp/{user:[a-zA-Z0-9]+$}"
|
userTotp = uroot + "totp/{user:[a-zA-Z0-9]+$}"
|
||||||
userUnlock = uroot + "unlock/{user:[a-zA-Z0-9]+$}"
|
userSessions = uroot + "sessions/{user:[a-zA-Z0-9]+$}"
|
||||||
|
userUnlock = uroot + "unlock/{user:[a-zA-Z0-9]+$}"
|
||||||
)
|
)
|
||||||
|
|
||||||
var routes = []route{
|
var routes = []route{
|
||||||
|
|
@ -48,15 +49,15 @@ var routes = []route{
|
||||||
{
|
{
|
||||||
consent,
|
consent,
|
||||||
jwtHandler(
|
jwtHandler(
|
||||||
userAuthHandler(
|
authHandler(
|
||||||
consentHandler)),
|
consentHandler)),
|
||||||
[]string{"GET", "POST"},
|
[]string{"GET", "POST"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
profile,
|
sessions,
|
||||||
jwtHandler(
|
jwtHandler(
|
||||||
userAuthHandler(
|
authHandler(
|
||||||
profileHandler)),
|
sessionsHandler)),
|
||||||
[]string{"GET"},
|
[]string{"GET"},
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|
@ -100,6 +101,13 @@ var routes = []route{
|
||||||
userTotpHandler)),
|
userTotpHandler)),
|
||||||
[]string{"GET", "POST"},
|
[]string{"GET", "POST"},
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
userSessions,
|
||||||
|
jwtHandler(
|
||||||
|
userAuthHandler(
|
||||||
|
userSessionsHandler)),
|
||||||
|
[]string{"GET", "POST"},
|
||||||
|
},
|
||||||
{
|
{
|
||||||
userUnlock,
|
userUnlock,
|
||||||
jwtHandler(
|
jwtHandler(
|
||||||
|
|
|
||||||
|
|
@ -43,6 +43,7 @@ func (s *Server) loadTemplates() (err error) {
|
||||||
|
|
||||||
// pages
|
// pages
|
||||||
s.ts.Parse(tfs, "notFoundHandler", "index.html", "menu.html", "notFound.html")
|
s.ts.Parse(tfs, "notFoundHandler", "index.html", "menu.html", "notFound.html")
|
||||||
|
s.ts.Parse(tfs, "forbiddenHandler", "index.html", "menu.html", "forbidden.html")
|
||||||
s.ts.Parse(tfs, "loginHandler", "index.html", "menu.html", "login.html")
|
s.ts.Parse(tfs, "loginHandler", "index.html", "menu.html", "login.html")
|
||||||
s.ts.Parse(tfs, "loginTotpHandler", "index.html", "menu.html", "loginTotp.html")
|
s.ts.Parse(tfs, "loginTotpHandler", "index.html", "menu.html", "loginTotp.html")
|
||||||
s.ts.Parse(tfs, "consentHandler", "index.html", "menu.html", "consent.html")
|
s.ts.Parse(tfs, "consentHandler", "index.html", "menu.html", "consent.html")
|
||||||
|
|
@ -51,6 +52,7 @@ func (s *Server) loadTemplates() (err error) {
|
||||||
s.ts.Parse(tfs, "userNewHandler", "index.html", "menu.html", "userNew.html")
|
s.ts.Parse(tfs, "userNewHandler", "index.html", "menu.html", "userNew.html")
|
||||||
s.ts.Parse(tfs, "userEditHandler", "index.html", "menu.html", "userEdit.html")
|
s.ts.Parse(tfs, "userEditHandler", "index.html", "menu.html", "userEdit.html")
|
||||||
s.ts.Parse(tfs, "userDelHandler", "index.html", "menu.html", "userDel.html")
|
s.ts.Parse(tfs, "userDelHandler", "index.html", "menu.html", "userDel.html")
|
||||||
|
s.ts.Parse(tfs, "userSessionsHandler", "index.html", "menu.html", "userSessions.html")
|
||||||
// totp
|
// totp
|
||||||
s.ts.Parse(tfs, "userTotpHandler", "index.html", "menu.html", "userTotp.html")
|
s.ts.Parse(tfs, "userTotpHandler", "index.html", "menu.html", "userTotp.html")
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,15 +1,19 @@
|
||||||
{{define "body"}}
|
{{define "body"}}
|
||||||
{{if .Data}}
|
{{if .Data}}
|
||||||
<div class="row">
|
<div class="row">
|
||||||
<div class="col-xs-4 offset-4">
|
<div class="col-xs-4 offset-3">
|
||||||
<div class="card border-primary mx-auto">
|
<div class="card border-primary mx-auto">
|
||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<strong>{{.BodyTitle}}</strong>
|
<strong>{{.BodyTitle}} - {{.Data.Info.Client.ClientName}}</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<pre>
|
The client <code>{{.Data.Info.Client.ClientName}}</code> is requesting access to your data.<br>
|
||||||
{{.Data.Info}}
|
Requested Scopes:
|
||||||
</pre>
|
<ul>
|
||||||
|
{{range $item := .Data.Info.RequestedScope}}
|
||||||
|
<li>{{$item}}</li>
|
||||||
|
{{end}}
|
||||||
|
</ul>
|
||||||
<form class="form-horizontal" action="/consent" method="post">
|
<form class="form-horizontal" action="/consent" method="post">
|
||||||
<input type="hidden" name="token" value="{{.Token}}">
|
<input type="hidden" name="token" value="{{.Token}}">
|
||||||
<input type="hidden" name="consent_challenge" value="{{.Data.ConsentChallenge}}">
|
<input type="hidden" name="consent_challenge" value="{{.Data.ConsentChallenge}}">
|
||||||
|
|
|
||||||
17
pkg/server/templates/forbidden.html
Normal file
17
pkg/server/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}}
|
||||||
|
|
@ -9,7 +9,9 @@
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form class="form-horizontal" action="/login" method="post">
|
<form class="form-horizontal" action="/login" method="post">
|
||||||
<input type="hidden" name="token" value="{{.Token}}">
|
<input type="hidden" name="token" value="{{.Token}}">
|
||||||
|
{{if .Data}}
|
||||||
<input type="hidden" name="login_challenge" value="{{.Data.LoginChallenge}}">
|
<input type="hidden" name="login_challenge" value="{{.Data.LoginChallenge}}">
|
||||||
|
{{end}}
|
||||||
<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" value="{{.Data.User}}" autofocus required>
|
<input class="form-control input-sm" type="text" id="user" name="user" value="{{.Data.User}}" autofocus required>
|
||||||
|
|
|
||||||
|
|
@ -9,7 +9,9 @@
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<form class="form-horizontal" action="/totp" method="post">
|
<form class="form-horizontal" action="/totp" method="post">
|
||||||
<input type="hidden" name="token" value="{{.Token}}">
|
<input type="hidden" name="token" value="{{.Token}}">
|
||||||
|
{{if .Data}}
|
||||||
<input type="hidden" name="login_challenge" value="{{.Data.LoginChallenge}}">
|
<input type="hidden" name="login_challenge" value="{{.Data.LoginChallenge}}">
|
||||||
|
{{end}}
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label class="col-form-label" for="pin">PIN</label>
|
<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>
|
<input class="form-control input-sm" type="text" id="pin" name="pin" autocomplete="off" autofocus required>
|
||||||
|
|
|
||||||
|
|
@ -1,11 +1,13 @@
|
||||||
{{define "menu"}}
|
{{define "menu"}}
|
||||||
|
{{if .Admin}}
|
||||||
|
<ul class="navbar-nav">
|
||||||
|
<li class="nav-item"><a class="nav-link" href="/users">Users</a></li>
|
||||||
|
</ul>
|
||||||
|
{{end}}
|
||||||
<ul class="nav navbar-nav ml-auto">
|
<ul class="nav navbar-nav ml-auto">
|
||||||
{{if .Login}}
|
{{if .Login}}
|
||||||
{{if .Admin}}
|
<li class="nav-item"><a class="nav-link" href="/user/sessions/{{.User}}">Sessions</a></li>
|
||||||
<li class="nav-item"><a class="nav-link" href="/users">Users</a></li>
|
|
||||||
{{else}}
|
|
||||||
<li class="nav-item"><a class="nav-link" href="/user/edit/{{.User}}">Profile</a></li>
|
<li class="nav-item"><a class="nav-link" href="/user/edit/{{.User}}">Profile</a></li>
|
||||||
{{end}}
|
|
||||||
<li class="nav-item"><a class="nav-link" href="/logout">Logout</a></li>
|
<li class="nav-item"><a class="nav-link" href="/logout">Logout</a></li>
|
||||||
{{else}}
|
{{else}}
|
||||||
<li class="nav-item"><a class="nav-link" href="/login">Login</a></li>
|
<li class="nav-item"><a class="nav-link" href="/login">Login</a></li>
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@
|
||||||
<strong>Delete {{.Data.Username}}?</strong>
|
<strong>Delete {{.Data.Username}}?</strong>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p>You are about to delete user <a href="/{{.Data.Username}}">{{.Data.Username}}</a>.</p>
|
<p>You are about to delete user <a href="/user/edit/{{.Data.Username}}">{{.Data.Username}}</a>.</p>
|
||||||
<form class="form-horizontal" action="/user/del/{{.Data.Username}}" method="post">
|
<form class="form-horizontal" action="/user/del/{{.Data.Username}}" method="post">
|
||||||
<input type="hidden" name="token" value="{{.Token}}">
|
<input type="hidden" name="token" value="{{.Token}}">
|
||||||
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
|
<button class="btn btn-sm btn-danger" type="submit">Delete</button>
|
||||||
|
|
|
||||||
|
|
@ -41,6 +41,16 @@
|
||||||
<label class="custom-control-label" for="admin">Admin Privileges</label>
|
<label class="custom-control-label" for="admin">Admin Privileges</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="custom-control custom-checkbox">
|
||||||
|
{{if .Data.Disabled}}
|
||||||
|
<input type="checkbox" class="custom-control-input" id="disabled" name="disabled" value="0" checked>
|
||||||
|
{{else}}
|
||||||
|
<input type="checkbox" class="custom-control-input" id="disabled" name="disabled" value="0">
|
||||||
|
{{end}}
|
||||||
|
<label class="custom-control-label" for="disabled">Disabled</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{{end}}
|
{{end}}
|
||||||
<button class="btn btn-sm btn-primary" type="submit">Update</button>
|
<button class="btn btn-sm btn-primary" type="submit">Update</button>
|
||||||
{{if .Admin}}
|
{{if .Admin}}
|
||||||
|
|
|
||||||
|
|
@ -30,6 +30,12 @@
|
||||||
<label class="custom-control-label" for="admin">Admin Privileges</label>
|
<label class="custom-control-label" for="admin">Admin Privileges</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<div class="custom-control custom-checkbox">
|
||||||
|
<input type="checkbox" class="custom-control-input" id="disabled" name="disabled" value="0">
|
||||||
|
<label class="custom-control-label" for="disabled">Disabled</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<button class="btn btn-sm btn-primary" type="submit">Create</button>
|
<button class="btn btn-sm btn-primary" type="submit">Create</button>
|
||||||
<a href="/users" class="btn btn-sm btn-primary">Back</a>
|
<a href="/users" class="btn btn-sm btn-primary">Back</a>
|
||||||
</form>
|
</form>
|
||||||
|
|
|
||||||
50
pkg/server/templates/userSessions.html
Normal file
50
pkg/server/templates/userSessions.html
Normal file
|
|
@ -0,0 +1,50 @@
|
||||||
|
{{define "body"}}
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="row">
|
||||||
|
<h2>{{.BodyTitle}}</h2>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{$user := .User}}
|
||||||
|
{{$token := .Token}}
|
||||||
|
{{if .Data}}
|
||||||
|
<div class="row mb-3">
|
||||||
|
<div class="col-xs-5">
|
||||||
|
<form class="form-horizontal" action="/user/sessions/{{$user}}" method="post">
|
||||||
|
<input type="hidden" name="token" value="{{$token}}">
|
||||||
|
<input type="hidden" name="all" value="all">
|
||||||
|
<button class="btn btn-sm btn-danger" type="submit">Revoke All</button>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col">
|
||||||
|
{{if .Data}}
|
||||||
|
<table class="table table-hover">
|
||||||
|
<thead><tr>
|
||||||
|
<th scope="col">Client Name</th>
|
||||||
|
<th scope="col">Created</th>
|
||||||
|
<th scope="col">Options</th>
|
||||||
|
</tr></thead>
|
||||||
|
<tbody>
|
||||||
|
{{range $name, $s := .Data}}
|
||||||
|
<tr>
|
||||||
|
<th scope="row">{{$name}}</th>
|
||||||
|
<td>{{$s.Time}}</td>
|
||||||
|
<td>
|
||||||
|
<form class="form-horizontal" action="/user/sessions/{{$user}}" method="post">
|
||||||
|
<input type="hidden" name="token" value="{{$token}}">
|
||||||
|
<input type="hidden" name="client" value="{{$s.ID}}">
|
||||||
|
<button class="btn btn-sm btn-warning" type="submit">Revoke</button>
|
||||||
|
</form>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{{end}}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
{{else}}
|
||||||
|
No active sessions found.
|
||||||
|
{{end}}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{{end}}
|
||||||
|
|
@ -31,13 +31,15 @@
|
||||||
<td>{{$item.Username}}</td>
|
<td>{{$item.Username}}</td>
|
||||||
<td>{{$item.Email}}</td>
|
<td>{{$item.Email}}</td>
|
||||||
<td>
|
<td>
|
||||||
|
{{if $item.Admin}}<span class="badge badge-danger">admin</span>{{end}}
|
||||||
{{if $item.Secret}}<span class="badge badge-success">totp</span>{{end}}
|
{{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 eq $item.Locked 3}}<span class="badge badge-warning">locked</span>{{end}}
|
||||||
{{if $item.Admin}}<span class="badge badge-danger">admin</span>{{end}}
|
{{if $item.Disabled}}<span class="badge badge-warning">disabled</span>{{end}}
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div class="btn-group">
|
<div class="btn-group">
|
||||||
{{if $admin}}
|
{{if $admin}}
|
||||||
|
<a class="btn btn-sm btn-primary" href="/user/sessions/{{$item.Username}}">Sessions</a>
|
||||||
<a class="btn btn-sm btn-primary" href="/user/edit/{{$item.Username}}">Edit</a>
|
<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>
|
<a class="btn btn-sm btn-danger" href="/user/del/{{$item.Username}}">Delete</a>
|
||||||
{{else}}
|
{{else}}
|
||||||
|
|
|
||||||
|
|
@ -34,7 +34,7 @@ cat <<EOF> /var/acc/goacc.conf
|
||||||
"listen": "127.0.0.1:4443",
|
"listen": "127.0.0.1:4443",
|
||||||
"secret": "$(openssl rand -hex 32)",
|
"secret": "$(openssl rand -hex 32)",
|
||||||
"hydra_admin_url": "http://127.0.0.1:4445",
|
"hydra_admin_url": "http://127.0.0.1:4445",
|
||||||
"admin": true,
|
"admin_disabled": false,
|
||||||
"secure_cookie": true
|
"secure_cookie": true
|
||||||
}
|
}
|
||||||
EOF
|
EOF
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue