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)
|
||||
}
|
||||
|
||||
func (app *application) submitUserRoles(_ http.ResponseWriter, _ *http.Request) {
|
||||
// TODO: implement submitUserRoles
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (app *application) editUserForm(w http.ResponseWriter, r *http.Request) {
|
||||
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())
|
||||
if userToEdit == nil {
|
||||
app.notFound(w)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
var form forms.EditUserForm
|
||||
|
||||
err := app.decodePostForm(r, &form)
|
||||
if err != nil {
|
||||
if err := app.decodePostForm(r, &form); err != nil {
|
||||
app.clientError(w, http.StatusBadRequest)
|
||||
|
||||
return
|
||||
|
@ -733,7 +729,7 @@ func (app *application) editUserSubmit(w http.ResponseWriter, r *http.Request) {
|
|||
form.Normalize()
|
||||
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)
|
||||
|
||||
return
|
||||
|
@ -748,14 +744,104 @@ func (app *application) editUserSubmit(w http.ResponseWriter, r *http.Request) {
|
|||
http.Redirect(w, r, "/users/", http.StatusSeeOther)
|
||||
}
|
||||
|
||||
func (app *application) userAddEmailForm(_ http.ResponseWriter, _ *http.Request) {
|
||||
// TODO: implement userAddEmailForm
|
||||
panic("not implemented")
|
||||
func (app *application) userAddEmailForm(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
|
||||
}
|
||||
|
||||
emailAddresses, err := userToEdit.EmailAddresses()
|
||||
if err != nil {
|
||||
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(_ http.ResponseWriter, _ *http.Request) {
|
||||
// TODO: implement userAddEmailSubmit
|
||||
panic("not implemented")
|
||||
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) {
|
||||
|
|
|
@ -83,7 +83,6 @@ func (app *application) routes() http.Handler {
|
|||
router.Handler(http.MethodPost, "/newmotion/", canEditVote.ThenFunc(app.newMotionSubmit))
|
||||
|
||||
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.MethodPost, "/new-user/", canManageUsers.ThenFunc(app.newUserSubmit))
|
||||
router.Handler(http.MethodGet, "/users/:id/", canManageUsers.ThenFunc(app.editUserForm))
|
||||
|
|
|
@ -199,3 +199,27 @@ func (f *DeleteUserForm) Validate() {
|
|||
func (f *DeleteUserForm) Normalize() {
|
||||
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)
|
||||
|
||||
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,
|
||||
)
|
||||
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
|
||||
}
|
||||
|
||||
|
@ -799,7 +852,7 @@ WHERE NOT EXISTS(SELECT * FROM user_roles WHERE voter_id = ? AND role = ?)`,
|
|||
return nil
|
||||
}
|
||||
|
||||
func userChangeInfo(before *User, after *User) any {
|
||||
func userChangeInfo(before, after *User) any {
|
||||
beforeRoleNames, beforeAddresses := before.rolesAndAddresses()
|
||||
afterRoleNames, afterAddresses := after.rolesAndAddresses()
|
||||
|
||||
|
@ -826,3 +879,10 @@ func userChangeInfo(before *User, after *User) any {
|
|||
|
||||
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
|
||||
|
||||
import (
|
||||
"net"
|
||||
"net/mail"
|
||||
"reflect"
|
||||
"regexp"
|
||||
"strings"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
type Validator struct {
|
||||
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 {
|
||||
return len(v.FieldErrors) == 0
|
||||
}
|
||||
|
@ -103,3 +112,18 @@ func PermittedStringSet(value []string, permittedValues []string) bool {
|
|||
|
||||
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