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
-
Users list
+
@@ -78,6 +80,51 @@
{% endfunc %}
+{% func AdminUserNewHTML(formData util.FormData) %}
+
+
+ New user
+
+ {% if formData.HasError() %}
+
+ Error:
+ {%s formData.Error() %}
+
+ {% endif %}
+
+
+
+
+{% 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
- 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: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:74
+//line views/admin.qtpl:76
}
-//line views/admin.qtpl:74
+//line views/admin.qtpl:76
qw422016.N().S(`
`)
-//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(`
+
+
+
+
+`)
+//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(`
@@ -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")
}