first working version

This commit is contained in:
ston1th 2022-07-10 15:51:31 +02:00
commit c9dfecc519
21 changed files with 296 additions and 178 deletions

1
.gitignore vendored
View file

@ -2,3 +2,4 @@
tmp tmp
run.sh run.sh
TODO.txt TODO.txt
consent.json

View file

@ -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"`
} }

View file

@ -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
} }

View file

@ -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
} }

View file

@ -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 {

View file

@ -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)

View file

@ -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)

View file

@ -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"
) )

View file

@ -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(

View file

@ -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")

View file

@ -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}}">

View 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}}

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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}}

View file

@ -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>

View 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}}

View file

@ -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}}

View file

@ -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