diff --git a/static/default.css b/static/default.css index bfb855f..56bb632 100644 --- a/static/default.css +++ b/static/default.css @@ -313,9 +313,6 @@ mark { background: rgba(130, 80, 30, 5); color: inherit; } } } -/* handlerug: sorry but I can't write in that unique and very special way */ -/* i have to resort to the BORING way of writing CSS */ -/* bouncepaw: they say that the best codes style is the consistent code style ☝️ */ kbd { display: inline-block; min-width: 1.5ch; @@ -382,7 +379,7 @@ kbd { .shortcuts-help .dialog__content { display: grid; grid-template-columns: repeat(auto-fit, minmax(300px, 1fr)); - grid-column-gap: 32px; + grid-column-gap: 32px; } .shortcuts-group-heading { @@ -435,3 +432,55 @@ kbd { .table-cell--fill { width: 100%; } + +/* + * Form fields + */ + +.form-field { + margin: 1em 0; +} + +.form-field label { + display: block; +} + +@media (min-width: 600px) { + .form-field { + display: grid; + grid-template-columns: 150px max-content; + grid-column-gap: 16px; + } + .form-field label { + grid-column: 1; + } + .form-field input, + .form-field button, + .form-field select, + .form-field textarea, + .form-field .form-field__input { + grid-column: 2; + } +} + +/* + * Notices + */ + +.notice { + margin: 0.5em 0; + border: 1px solid; + padding: 0.5em 0.7em; +} + +.notice--error { + border-color: #e15757; + background-color: #ffb1b1; +} + +@media (prefers-color-scheme: dark) { + .notice--error { + border-color: #a84343; + background-color: #5b3535; + } +} diff --git a/user/net.go b/user/net.go index 5998fac..c90dfa8 100644 --- a/user/net.go +++ b/user/net.go @@ -41,10 +41,12 @@ func Register(username, password, group string, force bool) error { switch { case !util.IsPossibleUsername(username): return fmt.Errorf("illegal username \"%s\"", username) + case !ValidGroup(group): + return fmt.Errorf("invalid group \"%s\"", group) + case HasUsername(username): + return fmt.Errorf("username \"%s\" is already taken", username) case !force && cfg.RegistrationLimit > 0 && Count() >= cfg.RegistrationLimit: return fmt.Errorf("reached the limit of registered users (%d)", cfg.RegistrationLimit) - case !force && HasUsername(username): - return fmt.Errorf("username \"%s\" is already taken", username) } hash, err := bcrypt.GenerateFromPassword([]byte(password), bcrypt.DefaultCost) diff --git a/util/util.go b/util/util.go index a21e8ff..e55000f 100644 --- a/util/util.go +++ b/util/util.go @@ -91,3 +91,49 @@ func HyphaNameFromRq(rq *http.Request, actions ...string) string { log.Println("HyphaNameFromRq: this request is invalid, fall back to home hypha") return cfg.HomeHypha } + +// FormData is a convenient struct for passing user input and errors to HTML +// forms and showing to the user. +type FormData struct { + err error + fields map[string]string +} + +func NewFormData() FormData { + return FormData{ + err: nil, + fields: map[string]string{}, + } +} + +func FormDataFromRequest(r *http.Request, keys []string) FormData { + formData := NewFormData() + for _, key := range keys { + formData.Put(key, r.FormValue(key)) + } + return formData +} + +func (f FormData) HasError() bool { + return f.err != nil +} + +func (f FormData) Error() string { + if f.err == nil { + return "" + } + return f.err.Error() +} + +func (f FormData) WithError(err error) FormData { + f.err = err + return f +} + +func (f FormData) Get(key string) string { + return f.fields[key] +} + +func (f FormData) Put(key, value string) { + f.fields[key] = value +} diff --git a/views/admin.qtpl b/views/admin.qtpl index 6854f0a..b4c477f 100644 --- a/views/admin.qtpl +++ b/views/admin.qtpl @@ -1,5 +1,6 @@ {% import "github.com/bouncepaw/mycorrhiza/cfg" %} {% import "github.com/bouncepaw/mycorrhiza/user" %} +{% import "github.com/bouncepaw/mycorrhiza/util" %} {% func AdminPanelHTML() %}
@@ -39,10 +40,11 @@

Manage users

+ Create a new user
-

Users list

+
@@ -78,6 +80,51 @@ {% endfunc %} +{% func AdminUserNewHTML(formData util.FormData) %} +
+
+

New user

+ + {% if formData.HasError() %} +
+ Error: + {%s formData.Error() %} +
+ {% endif %} + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ + Cancel +
+
+ +
+
+{% endfunc %} + {% func AdminUsersUserHTML(u *user.User) %}
diff --git a/views/admin.qtpl.go b/views/admin.qtpl.go index fc30b2b..3b80898 100644 --- a/views/admin.qtpl.go +++ b/views/admin.qtpl.go @@ -10,22 +10,25 @@ import "github.com/bouncepaw/mycorrhiza/cfg" //line views/admin.qtpl:2 import "github.com/bouncepaw/mycorrhiza/user" -//line views/admin.qtpl:4 +//line views/admin.qtpl:3 +import "github.com/bouncepaw/mycorrhiza/util" + +//line views/admin.qtpl:5 import ( qtio422016 "io" qt422016 "github.com/valyala/quicktemplate" ) -//line views/admin.qtpl:4 +//line views/admin.qtpl:5 var ( _ = qtio422016.Copy _ = qt422016.AcquireByteBuffer ) -//line views/admin.qtpl:4 +//line views/admin.qtpl:5 func StreamAdminPanelHTML(qw422016 *qt422016.Writer) { -//line views/admin.qtpl:4 +//line views/admin.qtpl:5 qw422016.N().S(`
@@ -57,48 +60,49 @@ func StreamAdminPanelHTML(qw422016 *qt422016.Writer) {
`) -//line views/admin.qtpl:34 +//line views/admin.qtpl:35 } -//line views/admin.qtpl:34 +//line views/admin.qtpl:35 func WriteAdminPanelHTML(qq422016 qtio422016.Writer) { -//line views/admin.qtpl:34 +//line views/admin.qtpl:35 qw422016 := qt422016.AcquireWriter(qq422016) -//line views/admin.qtpl:34 +//line views/admin.qtpl:35 StreamAdminPanelHTML(qw422016) -//line views/admin.qtpl:34 +//line views/admin.qtpl:35 qt422016.ReleaseWriter(qw422016) -//line views/admin.qtpl:34 +//line views/admin.qtpl:35 } -//line views/admin.qtpl:34 +//line views/admin.qtpl:35 func AdminPanelHTML() string { -//line views/admin.qtpl:34 +//line views/admin.qtpl:35 qb422016 := qt422016.AcquireByteBuffer() -//line views/admin.qtpl:34 +//line views/admin.qtpl:35 WriteAdminPanelHTML(qb422016) -//line views/admin.qtpl:34 +//line views/admin.qtpl:35 qs422016 := string(qb422016.B) -//line views/admin.qtpl:34 +//line views/admin.qtpl:35 qt422016.ReleaseByteBuffer(qb422016) -//line views/admin.qtpl:34 +//line views/admin.qtpl:35 return qs422016 -//line views/admin.qtpl:34 +//line views/admin.qtpl:35 } -//line views/admin.qtpl:36 +//line views/admin.qtpl:37 func StreamAdminUsersPanelHTML(qw422016 *qt422016.Writer, userList []*user.User) { -//line views/admin.qtpl:36 +//line views/admin.qtpl:37 qw422016.N().S(`

Manage users

+ Create a new user -

Users list

+
@@ -111,111 +115,247 @@ func StreamAdminUsersPanelHTML(qw422016 *qt422016.Writer, userList []*user.User) `) -//line views/admin.qtpl:57 +//line views/admin.qtpl:59 for _, u := range userList { -//line views/admin.qtpl:57 +//line views/admin.qtpl:59 qw422016.N().S(` `) -//line views/admin.qtpl:74 +//line views/admin.qtpl:76 } -//line views/admin.qtpl:74 +//line views/admin.qtpl:76 qw422016.N().S(`
`) -//line views/admin.qtpl:60 +//line views/admin.qtpl:62 qw422016.E().S(u.Name) -//line views/admin.qtpl:60 +//line views/admin.qtpl:62 qw422016.N().S(` `) -//line views/admin.qtpl:62 +//line views/admin.qtpl:64 qw422016.E().S(u.Group) -//line views/admin.qtpl:62 +//line views/admin.qtpl:64 qw422016.N().S(` `) -//line views/admin.qtpl:64 +//line views/admin.qtpl:66 if u.RegisteredAt.IsZero() { -//line views/admin.qtpl:64 +//line views/admin.qtpl:66 qw422016.N().S(` unknown `) -//line views/admin.qtpl:66 +//line views/admin.qtpl:68 } else { -//line views/admin.qtpl:66 +//line views/admin.qtpl:68 qw422016.N().S(` `) -//line views/admin.qtpl:67 +//line views/admin.qtpl:69 qw422016.E().S(u.RegisteredAt.UTC().Format("2006-01-02 15:04")) -//line views/admin.qtpl:67 +//line views/admin.qtpl:69 qw422016.N().S(` `) -//line views/admin.qtpl:68 +//line views/admin.qtpl:70 } -//line views/admin.qtpl:68 +//line views/admin.qtpl:70 qw422016.N().S(` Edit
`) -//line views/admin.qtpl:79 +//line views/admin.qtpl:81 } -//line views/admin.qtpl:79 +//line views/admin.qtpl:81 func WriteAdminUsersPanelHTML(qq422016 qtio422016.Writer, userList []*user.User) { -//line views/admin.qtpl:79 +//line views/admin.qtpl:81 qw422016 := qt422016.AcquireWriter(qq422016) -//line views/admin.qtpl:79 +//line views/admin.qtpl:81 StreamAdminUsersPanelHTML(qw422016, userList) -//line views/admin.qtpl:79 +//line views/admin.qtpl:81 qt422016.ReleaseWriter(qw422016) -//line views/admin.qtpl:79 +//line views/admin.qtpl:81 } -//line views/admin.qtpl:79 +//line views/admin.qtpl:81 func AdminUsersPanelHTML(userList []*user.User) string { -//line views/admin.qtpl:79 +//line views/admin.qtpl:81 qb422016 := qt422016.AcquireByteBuffer() -//line views/admin.qtpl:79 +//line views/admin.qtpl:81 WriteAdminUsersPanelHTML(qb422016, userList) -//line views/admin.qtpl:79 +//line views/admin.qtpl:81 qs422016 := string(qb422016.B) -//line views/admin.qtpl:79 +//line views/admin.qtpl:81 qt422016.ReleaseByteBuffer(qb422016) -//line views/admin.qtpl:79 +//line views/admin.qtpl:81 return qs422016 -//line views/admin.qtpl:79 +//line views/admin.qtpl:81 } -//line views/admin.qtpl:81 +//line views/admin.qtpl:83 +func StreamAdminUserNewHTML(qw422016 *qt422016.Writer, formData util.FormData) { +//line views/admin.qtpl:83 + qw422016.N().S(` +
+
+

New user

+ + `) +//line views/admin.qtpl:88 + if formData.HasError() { +//line views/admin.qtpl:88 + qw422016.N().S(` +
+ Error: + `) +//line views/admin.qtpl:91 + qw422016.E().S(formData.Error()) +//line views/admin.qtpl:91 + qw422016.N().S(` +
+ `) +//line views/admin.qtpl:93 + } +//line views/admin.qtpl:93 + qw422016.N().S(` + +
+
+ + +
+ +
+ + +
+ +
+ + +
+ +
+
+ + Cancel +
+
+
+
+
+`) +//line views/admin.qtpl:126 +} + +//line views/admin.qtpl:126 +func WriteAdminUserNewHTML(qq422016 qtio422016.Writer, formData util.FormData) { +//line views/admin.qtpl:126 + qw422016 := qt422016.AcquireWriter(qq422016) +//line views/admin.qtpl:126 + StreamAdminUserNewHTML(qw422016, formData) +//line views/admin.qtpl:126 + qt422016.ReleaseWriter(qw422016) +//line views/admin.qtpl:126 +} + +//line views/admin.qtpl:126 +func AdminUserNewHTML(formData util.FormData) string { +//line views/admin.qtpl:126 + qb422016 := qt422016.AcquireByteBuffer() +//line views/admin.qtpl:126 + WriteAdminUserNewHTML(qb422016, formData) +//line views/admin.qtpl:126 + qs422016 := string(qb422016.B) +//line views/admin.qtpl:126 + qt422016.ReleaseByteBuffer(qb422016) +//line views/admin.qtpl:126 + return qs422016 +//line views/admin.qtpl:126 +} + +//line views/admin.qtpl:128 func StreamAdminUsersUserHTML(qw422016 *qt422016.Writer, u *user.User) { -//line views/admin.qtpl:81 +//line views/admin.qtpl:128 qw422016.N().S(`

`) -//line views/admin.qtpl:84 +//line views/admin.qtpl:131 qw422016.E().S(u.Name) -//line views/admin.qtpl:84 +//line views/admin.qtpl:131 qw422016.N().S(`

@@ -223,49 +363,49 @@ func StreamAdminUsersUserHTML(qw422016 *qt422016.Writer, u *user.User) {
@@ -276,31 +416,31 @@ func StreamAdminUsersUserHTML(qw422016 *qt422016.Writer, u *user.User) { `) -//line views/admin.qtpl:103 +//line views/admin.qtpl:150 } -//line views/admin.qtpl:103 +//line views/admin.qtpl:150 func WriteAdminUsersUserHTML(qq422016 qtio422016.Writer, u *user.User) { -//line views/admin.qtpl:103 +//line views/admin.qtpl:150 qw422016 := qt422016.AcquireWriter(qq422016) -//line views/admin.qtpl:103 +//line views/admin.qtpl:150 StreamAdminUsersUserHTML(qw422016, u) -//line views/admin.qtpl:103 +//line views/admin.qtpl:150 qt422016.ReleaseWriter(qw422016) -//line views/admin.qtpl:103 +//line views/admin.qtpl:150 } -//line views/admin.qtpl:103 +//line views/admin.qtpl:150 func AdminUsersUserHTML(u *user.User) string { -//line views/admin.qtpl:103 +//line views/admin.qtpl:150 qb422016 := qt422016.AcquireByteBuffer() -//line views/admin.qtpl:103 +//line views/admin.qtpl:150 WriteAdminUsersUserHTML(qb422016, u) -//line views/admin.qtpl:103 +//line views/admin.qtpl:150 qs422016 := string(qb422016.B) -//line views/admin.qtpl:103 +//line views/admin.qtpl:150 qt422016.ReleaseByteBuffer(qb422016) -//line views/admin.qtpl:103 +//line views/admin.qtpl:150 return qs422016 -//line views/admin.qtpl:103 +//line views/admin.qtpl:150 } diff --git a/web/admin.go b/web/admin.go index 7fa0d91..fa25936 100644 --- a/web/admin.go +++ b/web/admin.go @@ -23,6 +23,7 @@ func initAdmin() { http.HandleFunc("/admin/reindex-users/", handlerAdminReindexUsers) http.HandleFunc("/admin/users/", handlerAdminUsers) + http.HandleFunc("/admin/user/new", handlerAdminUserNew) } } @@ -124,7 +125,41 @@ func handlerAdminUsers(w http.ResponseWriter, r *http.Request) { } } } - - util.HTTP404Page(w, "404 page not found") } + + util.HTTP404Page(w, "404 page not found") +} + +func handlerAdminUserNew(w http.ResponseWriter, r *http.Request) { + util.PrepareRq(r) + if user.CanProceed(r, "admin") { + if r.Method == http.MethodGet { + // New user form + html := views.AdminUserNewHTML(util.NewFormData()) + html = views.BaseHTML("New user", html, user.FromRequest(r)) + + w.Header().Set("Content-Type", mime.TypeByExtension(".html")) + io.WriteString(w, html) + return + } else if r.Method == http.MethodPost { + // Create a user + f := util.FormDataFromRequest(r, []string{"name", "password", "group"}) + + err := user.Register(f.Get("name"), f.Get("password"), f.Get("group"), true) + + if err != nil { + html := views.AdminUserNewHTML(f.WithError(err)) + html = views.BaseHTML("New user", html, user.FromRequest(r)) + + w.WriteHeader(http.StatusBadRequest) + w.Header().Set("Content-Type", mime.TypeByExtension(".html")) + io.WriteString(w, html) + } else { + http.Redirect(w, r, "/admin/users/", http.StatusSeeOther) + } + return + } + } + + util.HTTP404Page(w, "404 page not found") }