Implement add email address
This commit is contained in:
parent
a5475ec16e
commit
be14a37b4d
6 changed files with 251 additions and 18 deletions
|
@ -626,11 +626,6 @@ func (app *application) userList(w http.ResponseWriter, r *http.Request) {
|
||||||
app.render(w, http.StatusOK, "users.html", data)
|
app.render(w, http.StatusOK, "users.html", data)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *application) submitUserRoles(_ http.ResponseWriter, _ *http.Request) {
|
|
||||||
// TODO: implement submitUserRoles
|
|
||||||
panic("not implemented")
|
|
||||||
}
|
|
||||||
|
|
||||||
func (app *application) editUserForm(w http.ResponseWriter, r *http.Request) {
|
func (app *application) editUserForm(w http.ResponseWriter, r *http.Request) {
|
||||||
params := httprouter.ParamsFromContext(r.Context())
|
params := httprouter.ParamsFromContext(r.Context())
|
||||||
|
|
||||||
|
@ -677,13 +672,14 @@ func (app *application) editUserSubmit(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
userToEdit := app.userFromRequestParam(w, r, params, app.users.WithRoles(), app.users.WithEmailAddresses())
|
userToEdit := app.userFromRequestParam(w, r, params, app.users.WithRoles(), app.users.WithEmailAddresses())
|
||||||
if userToEdit == nil {
|
if userToEdit == nil {
|
||||||
|
app.notFound(w)
|
||||||
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
var form forms.EditUserForm
|
var form forms.EditUserForm
|
||||||
|
|
||||||
err := app.decodePostForm(r, &form)
|
if err := app.decodePostForm(r, &form); err != nil {
|
||||||
if err != nil {
|
|
||||||
app.clientError(w, http.StatusBadRequest)
|
app.clientError(w, http.StatusBadRequest)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
@ -733,7 +729,7 @@ func (app *application) editUserSubmit(w http.ResponseWriter, r *http.Request) {
|
||||||
form.Normalize()
|
form.Normalize()
|
||||||
form.UpdateUser(userToEdit)
|
form.UpdateUser(userToEdit)
|
||||||
|
|
||||||
if err = app.users.EditUser(r.Context(), userToEdit, data.User, form.Roles, form.Reasoning); err != nil {
|
if err := app.users.EditUser(r.Context(), userToEdit, data.User, form.Roles, form.Reasoning); err != nil {
|
||||||
app.serverError(w, err)
|
app.serverError(w, err)
|
||||||
|
|
||||||
return
|
return
|
||||||
|
@ -748,14 +744,104 @@ func (app *application) editUserSubmit(w http.ResponseWriter, r *http.Request) {
|
||||||
http.Redirect(w, r, "/users/", http.StatusSeeOther)
|
http.Redirect(w, r, "/users/", http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *application) userAddEmailForm(_ http.ResponseWriter, _ *http.Request) {
|
func (app *application) userAddEmailForm(w http.ResponseWriter, r *http.Request) {
|
||||||
// TODO: implement userAddEmailForm
|
params := httprouter.ParamsFromContext(r.Context())
|
||||||
panic("not implemented")
|
|
||||||
|
userToEdit := app.userFromRequestParam(w, r, params, app.users.WithEmailAddresses())
|
||||||
|
if userToEdit == nil {
|
||||||
|
app.notFound(w)
|
||||||
|
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *application) userAddEmailSubmit(_ http.ResponseWriter, _ *http.Request) {
|
emailAddresses, err := userToEdit.EmailAddresses()
|
||||||
// TODO: implement userAddEmailSubmit
|
if err != nil {
|
||||||
panic("not implemented")
|
app.serverError(w, err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data := app.newTemplateData(r, topLevelNavUsers, subLevelNavUsers)
|
||||||
|
|
||||||
|
data.Form = &forms.AddEmailForm{
|
||||||
|
User: userToEdit,
|
||||||
|
EmailAddresses: emailAddresses,
|
||||||
|
}
|
||||||
|
|
||||||
|
app.render(w, http.StatusOK, "add_email.html", data)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (app *application) userAddEmailSubmit(w http.ResponseWriter, r *http.Request) {
|
||||||
|
params := httprouter.ParamsFromContext(r.Context())
|
||||||
|
|
||||||
|
userToEdit := app.userFromRequestParam(w, r, params, app.users.WithEmailAddresses())
|
||||||
|
if userToEdit == nil {
|
||||||
|
app.notFound(w)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var form forms.AddEmailForm
|
||||||
|
|
||||||
|
if err := app.decodePostForm(r, &form); err != nil {
|
||||||
|
app.clientError(w, http.StatusBadRequest)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
form.User = userToEdit
|
||||||
|
|
||||||
|
form.Validate()
|
||||||
|
|
||||||
|
data := app.newTemplateData(r, topLevelNavUsers, subLevelNavUsers)
|
||||||
|
|
||||||
|
if form.Valid() {
|
||||||
|
emailExists, err := app.users.EmailExists(r.Context(), form.EmailAddress)
|
||||||
|
if err != nil {
|
||||||
|
app.serverError(w, err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if emailExists {
|
||||||
|
form.FieldErrors = map[string]string{
|
||||||
|
"email_address": "Email address must be unique",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !form.Valid() {
|
||||||
|
emailAddresses, err := userToEdit.EmailAddresses()
|
||||||
|
if err != nil {
|
||||||
|
app.serverError(w, err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
form.EmailAddresses = emailAddresses
|
||||||
|
|
||||||
|
data.Form = &form
|
||||||
|
|
||||||
|
app.render(w, http.StatusUnprocessableEntity, "add_email.html", data)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
form.Normalize()
|
||||||
|
|
||||||
|
if err := app.users.AddEmail(r.Context(), userToEdit, data.User, form.EmailAddress, form.Reasoning); err != nil {
|
||||||
|
app.serverError(w, err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
app.sessionManager.Put(
|
||||||
|
r.Context(),
|
||||||
|
"flash",
|
||||||
|
fmt.Sprintf("Added email address %s for user %s", form.EmailAddress, userToEdit.Name),
|
||||||
|
)
|
||||||
|
|
||||||
|
http.Redirect(w, r, fmt.Sprintf("/users/%d/", userToEdit.ID), http.StatusSeeOther)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (app *application) userDeleteEmailForm(_ http.ResponseWriter, _ *http.Request) {
|
func (app *application) userDeleteEmailForm(_ http.ResponseWriter, _ *http.Request) {
|
||||||
|
|
|
@ -83,7 +83,6 @@ func (app *application) routes() http.Handler {
|
||||||
router.Handler(http.MethodPost, "/newmotion/", canEditVote.ThenFunc(app.newMotionSubmit))
|
router.Handler(http.MethodPost, "/newmotion/", canEditVote.ThenFunc(app.newMotionSubmit))
|
||||||
|
|
||||||
router.Handler(http.MethodGet, "/users/", canManageUsers.ThenFunc(app.userList))
|
router.Handler(http.MethodGet, "/users/", canManageUsers.ThenFunc(app.userList))
|
||||||
router.Handler(http.MethodPost, "/users/", canManageUsers.ThenFunc(app.submitUserRoles))
|
|
||||||
router.Handler(http.MethodGet, "/new-user/", canManageUsers.ThenFunc(app.newUserForm))
|
router.Handler(http.MethodGet, "/new-user/", canManageUsers.ThenFunc(app.newUserForm))
|
||||||
router.Handler(http.MethodPost, "/new-user/", canManageUsers.ThenFunc(app.newUserSubmit))
|
router.Handler(http.MethodPost, "/new-user/", canManageUsers.ThenFunc(app.newUserSubmit))
|
||||||
router.Handler(http.MethodGet, "/users/:id/", canManageUsers.ThenFunc(app.editUserForm))
|
router.Handler(http.MethodGet, "/users/:id/", canManageUsers.ThenFunc(app.editUserForm))
|
||||||
|
|
|
@ -199,3 +199,27 @@ func (f *DeleteUserForm) Validate() {
|
||||||
func (f *DeleteUserForm) Normalize() {
|
func (f *DeleteUserForm) Normalize() {
|
||||||
f.Reasoning = strings.TrimSpace(f.Reasoning)
|
f.Reasoning = strings.TrimSpace(f.Reasoning)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type AddEmailForm struct {
|
||||||
|
EmailAddress string `form:"email_address"`
|
||||||
|
Reasoning string `form:"reasoning"`
|
||||||
|
User *models.User `form:"-""`
|
||||||
|
EmailAddresses []string `form:"-"`
|
||||||
|
validator.Validator `form:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *AddEmailForm) Validate() {
|
||||||
|
f.CheckField(validator.NotBlank(f.EmailAddress), "email_address", "This field cannot be blank")
|
||||||
|
f.CheckField(validator.IsEmail(f.EmailAddress), "email_address", "This field must be an email address")
|
||||||
|
f.CheckField(validator.NotBlank(f.Reasoning), "reasoning", "This field cannot be blank")
|
||||||
|
f.CheckField(
|
||||||
|
validator.MinChars(f.Reasoning, minimumJustificationLen),
|
||||||
|
"reasoning",
|
||||||
|
fmt.Sprintf("This field must be at least %d characters long", minimumJustificationLen),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *AddEmailForm) Normalize() {
|
||||||
|
f.EmailAddress = strings.TrimSpace(f.EmailAddress)
|
||||||
|
f.Reasoning = strings.TrimSpace(f.Reasoning)
|
||||||
|
}
|
||||||
|
|
|
@ -452,7 +452,7 @@ func (m *UserModel) WithEmailAddresses() UserListOption {
|
||||||
userIDs := userIDsFromUsers(users)
|
userIDs := userIDsFromUsers(users)
|
||||||
|
|
||||||
query, args, err := sqlx.In(
|
query, args, err := sqlx.In(
|
||||||
`SELECT voter, address FROM emails WHERE voter IN (?)`,
|
`SELECT voter, address FROM emails WHERE voter IN (?) ORDER BY address`,
|
||||||
userIDs,
|
userIDs,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -725,7 +725,60 @@ func (m *UserModel) EditUser(ctx context.Context, edit *User, admin *User, roles
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err = AuditLog(ctx, tx, admin, AuditEditUser, reasoning, userChangeInfo(userBefore, edit)); err != nil {
|
if err = AuditLog(
|
||||||
|
ctx, tx, admin, AuditEditUser, reasoning,
|
||||||
|
userChangeInfo(userBefore, edit),
|
||||||
|
); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = tx.Commit(); err != nil {
|
||||||
|
return errCouldNotCommitTransaction(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *UserModel) EmailExists(ctx context.Context, address string) (bool, error) {
|
||||||
|
row := m.DB.QueryRowxContext(ctx, `SELECT EXISTS(SELECT * FROM emails WHERE address=?)`, address)
|
||||||
|
|
||||||
|
if err := row.Err(); err != nil {
|
||||||
|
return false, errCouldNotExecuteQuery(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var exists bool
|
||||||
|
|
||||||
|
if err := row.Scan(&exists); err != nil {
|
||||||
|
return false, errCouldNotScanResult(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return exists, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *UserModel) AddEmail(ctx context.Context, user *User, admin *User, emailAddress string, reasoning string) error {
|
||||||
|
userBefore, err := m.ByID(ctx, user.ID, m.WithEmailAddresses())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tx, err := m.DB.BeginTxx(ctx, nil)
|
||||||
|
if err != nil {
|
||||||
|
return errCouldNotStartTransaction(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer func(tx *sqlx.Tx) {
|
||||||
|
_ = tx.Rollback()
|
||||||
|
}(tx)
|
||||||
|
|
||||||
|
if _, err := tx.ExecContext(ctx, `INSERT INTO emails (voter, address) VALUES (?, ?)`,
|
||||||
|
user.ID, emailAddress); err != nil {
|
||||||
|
return errCouldNotExecuteQuery(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err = AuditLog(
|
||||||
|
ctx, tx, admin, AuditAddEmail, reasoning,
|
||||||
|
emailChangeInfo(userBefore.emailAddresses, append(userBefore.emailAddresses, emailAddress)),
|
||||||
|
); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -799,7 +852,7 @@ WHERE NOT EXISTS(SELECT * FROM user_roles WHERE voter_id = ? AND role = ?)`,
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func userChangeInfo(before *User, after *User) any {
|
func userChangeInfo(before, after *User) any {
|
||||||
beforeRoleNames, beforeAddresses := before.rolesAndAddresses()
|
beforeRoleNames, beforeAddresses := before.rolesAndAddresses()
|
||||||
afterRoleNames, afterAddresses := after.rolesAndAddresses()
|
afterRoleNames, afterAddresses := after.rolesAndAddresses()
|
||||||
|
|
||||||
|
@ -826,3 +879,10 @@ func userChangeInfo(before *User, after *User) any {
|
||||||
|
|
||||||
return details
|
return details
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func emailChangeInfo(before, after []string) any {
|
||||||
|
return struct {
|
||||||
|
Before []string `json:"before"`
|
||||||
|
After []string `json:"after"`
|
||||||
|
}{Before: before, After: after}
|
||||||
|
}
|
||||||
|
|
|
@ -18,15 +18,24 @@ limitations under the License.
|
||||||
package validator
|
package validator
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"net"
|
||||||
|
"net/mail"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"unicode/utf8"
|
"unicode/utf8"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Validator struct {
|
type Validator struct {
|
||||||
FieldErrors map[string]string
|
FieldErrors map[string]string
|
||||||
|
rxEmail *regexp.Regexp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var rxEmail = regexp.MustCompile(
|
||||||
|
"^[a-zA-Z0-9.!#$%&'*+\\\\/=?^_`{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?" +
|
||||||
|
"(?:\\\\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$",
|
||||||
|
)
|
||||||
|
|
||||||
func (v *Validator) Valid() bool {
|
func (v *Validator) Valid() bool {
|
||||||
return len(v.FieldErrors) == 0
|
return len(v.FieldErrors) == 0
|
||||||
}
|
}
|
||||||
|
@ -103,3 +112,18 @@ func PermittedStringSet(value []string, permittedValues []string) bool {
|
||||||
|
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func IsEmail(value string) bool {
|
||||||
|
addr, err := mail.ParseAddress(value)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
parts := strings.SplitN(addr.Address, "@", 2)
|
||||||
|
mxs, err := net.LookupMX(parts[1])
|
||||||
|
if err != nil || len(mxs) < 1 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
40
ui/html/pages/add_email.html
Normal file
40
ui/html/pages/add_email.html
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
{{ define "title" }}Add email address for {{ .Form.User.Name }}{{ end }}
|
||||||
|
|
||||||
|
{{ define "main" }}
|
||||||
|
<form action="/users/{{ .Form.User.ID }}/add-mail" method="post">
|
||||||
|
<input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
|
||||||
|
<div class="ui form segment{{ if .Form.FieldErrors }} error{{ end }}">
|
||||||
|
<div class="ui message">
|
||||||
|
<div class="header">
|
||||||
|
Add email address for {{ .Form.User.Name }}
|
||||||
|
</div>
|
||||||
|
<p>The following addresses are registered for {{ .Form.User.Name }}:</p>
|
||||||
|
<div class="ui list">
|
||||||
|
{{ range .Form.EmailAddresses }}
|
||||||
|
<div class="item">
|
||||||
|
<i class="mail icon"></i>
|
||||||
|
<div class="content">{{ . }}</div>
|
||||||
|
</div>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="required field{{ if .Form.FieldErrors.email_address }} error{{ end }}">
|
||||||
|
<label for="email_address">Email address:</label>
|
||||||
|
<input id="email_address" name="email_address" type="text" value="{{ .Form.EmailAddress }}">
|
||||||
|
{{ if .Form.FieldErrors.email_address }}
|
||||||
|
<span class="ui small error text">{{ .Form.FieldErrors.email_address }}</span>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
<div class="required field{{ if .Form.FieldErrors.reasoning }} error{{ end }}">
|
||||||
|
<label for="reasoning">Reasoning for the change</label>
|
||||||
|
<textarea id="reasoning" name="reasoning" rows="2">{{ .Form.Reasoning }}</textarea>
|
||||||
|
{{ if .Form.FieldErrors.reasoning }}
|
||||||
|
<span class="ui small error text">{{ .Form.FieldErrors.reasoning }}</span>
|
||||||
|
{{ end }}
|
||||||
|
</div>
|
||||||
|
<button class="ui primary labeled icon button" type="submit"><i class="add icon"></i> Add email address
|
||||||
|
</button>
|
||||||
|
<a href="/users/{{ .Form.User.ID }}/" class="ui button">Cancel</a>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
{{ end }}
|
Loading…
Reference in a new issue