Implement user deletion

- add audit logging for user changes
- refactor model errors into functions
- implement user delete form and submit handlers
This commit is contained in:
Jan Dittberner 2022-06-01 18:57:38 +02:00
parent db52f88e25
commit 5efc57d2c3
11 changed files with 385 additions and 70 deletions

View file

@ -579,10 +579,8 @@ func (app *application) proxyVoteSubmit(w http.ResponseWriter, r *http.Request)
return return
} }
voter, err := app.users.ByID(r.Context(), form.Voter.ID) voter := app.getVoter(w, r, form.Voter.ID)
if err != nil { if voter == nil {
app.serverError(w, err)
return return
} }
@ -637,6 +635,7 @@ func (app *application) editUserForm(_ http.ResponseWriter, _ *http.Request) {
// TODO: implement editUserForm // TODO: implement editUserForm
panic("not implemented") panic("not implemented")
} }
func (app *application) editUserSubmit(_ http.ResponseWriter, _ *http.Request) { func (app *application) editUserSubmit(_ http.ResponseWriter, _ *http.Request) {
// TODO: implement editUserSubmit // TODO: implement editUserSubmit
panic("not implemented") panic("not implemented")
@ -652,14 +651,77 @@ func (app *application) userAddEmailSubmit(_ http.ResponseWriter, _ *http.Reques
panic("not implemented") panic("not implemented")
} }
func (app *application) deleteUserForm(_ http.ResponseWriter, _ *http.Request) { func (app *application) deleteUserForm(w http.ResponseWriter, r *http.Request) {
// TODO: implement deleteUserForm userToDelete := app.userFromRequestParam(w, r, httprouter.ParamsFromContext(r.Context()), app.users.CanDeleteOption())
panic("not implemented") if userToDelete == nil {
return
}
if !userToDelete.CanDelete() {
app.notFound(w)
return
}
data := app.newTemplateData(r, topLevelNavUsers, subLevelNavUsers)
data.Form = &forms.DeleteUserForm{
User: userToDelete,
}
app.render(w, http.StatusOK, "delete_user.html", data)
} }
func (app *application) deleteUserSubmit(_ http.ResponseWriter, _ *http.Request) { func (app *application) deleteUserSubmit(w http.ResponseWriter, r *http.Request) {
// TODO: implement deleteUserSubmit userToDelete := app.userFromRequestParam(w, r, httprouter.ParamsFromContext(r.Context()), app.users.CanDeleteOption())
panic("not implemented") if userToDelete == nil {
return
}
if !userToDelete.CanDelete() {
app.notFound(w)
return
}
var form forms.DeleteUserForm
err := app.decodePostForm(r, &form)
if err != nil {
app.clientError(w, http.StatusBadRequest)
return
}
form.User = userToDelete
form.Validate()
data := app.newTemplateData(r, topLevelNavUsers, subLevelNavUsers)
if !form.Valid() {
data.Form = &form
app.render(w, http.StatusUnprocessableEntity, "delete_user.html", data)
return
}
form.Normalize()
if err = app.users.DeleteUser(r.Context(), userToDelete, data.User, form.Reasoning); err != nil {
app.serverError(w, err)
return
}
app.sessionManager.Put(
r.Context(),
"flash",
fmt.Sprintf("User %s has been deleted.", userToDelete.Name),
)
http.Redirect(w, r, "/users/", http.StatusSeeOther)
} }
func (app *application) healthCheck(w http.ResponseWriter, _ *http.Request) { func (app *application) healthCheck(w http.ResponseWriter, _ *http.Request) {

View file

@ -28,6 +28,7 @@ import (
"net/http" "net/http"
"path/filepath" "path/filepath"
"runtime/debug" "runtime/debug"
"strconv"
"strings" "strings"
"github.com/Masterminds/sprig/v3" "github.com/Masterminds/sprig/v3"
@ -196,6 +197,32 @@ func (app *application) motionFromRequestParam(
return motion return motion
} }
func (app *application) userFromRequestParam(
w http.ResponseWriter, r *http.Request, params httprouter.Params, options ...models.UserListOption,
) *models.User {
userID, err := strconv.Atoi(params.ByName("id"))
if err != nil {
app.clientError(w, http.StatusBadRequest)
return nil
}
user, err := app.users.ByID(r.Context(), int64(userID), options...)
if err != nil {
app.serverError(w, err)
return nil
}
if user == nil {
app.notFound(w)
return nil
}
return user
}
func (app *application) choiceFromRequestParam(w http.ResponseWriter, params httprouter.Params) *models.VoteChoice { func (app *application) choiceFromRequestParam(w http.ResponseWriter, params httprouter.Params) *models.VoteChoice {
choiceParam := params.ByName("choice") choiceParam := params.ByName("choice")

View file

@ -209,6 +209,31 @@ func (app *application) startHTTPSServer(config *Config) error {
return nil return nil
} }
func (app *application) getVoter(w http.ResponseWriter, r *http.Request, voterID int64) *models.User {
voter, err := app.users.ByID(r.Context(), voterID, app.users.WithRolesOption())
if err != nil {
app.serverError(w, err)
return nil
}
var isVoter bool
if isVoter, err = voter.HasRole(models.RoleVoter); err != nil {
app.serverError(w, err)
return nil
}
if !isVoter {
app.clientError(w, http.StatusBadRequest)
return nil
}
return voter
}
func setupHTTPRedirect(config *Config, errChan chan error) { func setupHTTPRedirect(config *Config, errChan chan error) {
redirect := &http.Server{ redirect := &http.Server{
Addr: config.HTTPAddress, Addr: config.HTTPAddress,

View file

@ -119,3 +119,22 @@ func (f *ProxyVoteForm) Validate() {
func (f *ProxyVoteForm) Normalize() { func (f *ProxyVoteForm) Normalize() {
f.Justification = strings.TrimSpace(f.Justification) f.Justification = strings.TrimSpace(f.Justification)
} }
type DeleteUserForm struct {
User *models.User `form:"user"`
Reasoning string `form:"reasoning"`
validator.Validator `form:"-"`
}
func (f *DeleteUserForm) Validate() {
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 *DeleteUserForm) Normalize() {
f.Reasoning = strings.TrimSpace(f.Reasoning)
}

View file

@ -0,0 +1,3 @@
-- add an audit table to track changes to users
DROP INDEX audit_change_idx;
DROP TABLE audit;

View file

@ -0,0 +1,18 @@
-- add an audit table to track changes to users
CREATE TABLE audit
(
id INTEGER PRIMARY KEY AUTOINCREMENT,
-- copied authenticated user name to keep the information when the admin is locked/deleted later
user_name VARCHAR(255) NOT NULL,
-- copied authenticated user email address to keep the information when the admin is locked/deleted later
user_address VARCHAR(255) NOT NULL,
created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
-- an enum (in code) value to specify the action. Stored as text to be flexible for future changes
change TEXT NOT NULL,
-- reasoning for the change specified by the user
reasoning TEXT NOT NULL,
-- additional information about the change in an application specific json format
details TEXT
);
CREATE INDEX audit_change_idx ON audit (change);

67
internal/models/audit.go Normal file
View file

@ -0,0 +1,67 @@
/*
Copyright 2022 CAcert Inc.
SPDX-License-Identifier: Apache-2.0
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.
*/
package models
import (
"context"
"encoding/json"
"fmt"
"time"
"github.com/jmoiron/sqlx"
)
type AuditChange string
const (
AuditCreateUser AuditChange = "CREATE_USER"
AuditDeleteUser AuditChange = "DELETE_USER"
AuditEditUser AuditChange = "EDIT_USER"
AuditAddEmail AuditChange = "ADD_EMAIL"
AuditRemoveEmail AuditChange = "REMOVE_EMAIL"
AuditAddRole AuditChange = "ADD_ROLE"
AuditRemoveRole AuditChange = "REMOVE_ROLE"
)
type Audit struct {
ID int64 `db:"id"`
UserName string `db:"user_name"`
UserAddress string `db:"user_address"`
Created time.Time `db:"created"`
Change *AuditChange `db:"change"`
Reasoning string `db:"reasoning"`
Details string `db:"details"`
}
func AuditLog(ctx context.Context, tx *sqlx.Tx, user *User, change AuditChange, reasoning string, details any) error {
jsonDetails, err := json.Marshal(details)
if err != nil {
return fmt.Errorf("could not transform details to JSON: %w", err)
}
_, err = tx.ExecContext(
ctx,
`INSERT INTO audit (user_name, user_address, created, change, reasoning, details) VALUES (?, ?, ?, ?, ?, ?)`,
user.Name, user.Reminder, time.Now().UTC(), change, reasoning, string(jsonDetails),
)
if err != nil {
return errCouldNotExecuteQuery(err)
}
return nil
}

View file

@ -49,11 +49,21 @@ func parseSqlite3TimeStamp(timeStamp string) (*time.Time, error) {
return nil, fmt.Errorf("could not parse timestamp: %w", err) return nil, fmt.Errorf("could not parse timestamp: %w", err)
} }
const ( func errCouldNotExecuteQuery(err error) error {
errCouldNotExecuteQuery = "could not execute query: %w" return fmt.Errorf("could not execute query: %w", err)
errCouldNotFetchRow = "could not fetch row: %w" }
errCouldNotScanResult = "could not scan result: %w" func errCouldNotFetchRow(err error) error {
errCouldNotStartTransaction = "could not start transaction: %w" return fmt.Errorf("could not fetch row: %w", err)
errCouldNotCommitTransaction = "could not commit transaction: %w" }
errCouldNotCreateInQuery = "could not create query with IN clause: %w" func errCouldNotScanResult(err error) error {
) return fmt.Errorf("could not scan result: %w", err)
}
func errCouldNotStartTransaction(err error) error {
return fmt.Errorf("could not start transaction: %w", err)
}
func errCouldNotCommitTransaction(err error) error {
return fmt.Errorf("could not commit transaction: %w", err)
}
func errCouldNotCreateInQuery(err error) error {
return fmt.Errorf("could not create query with IN clause: %w", err)
}

View file

@ -302,7 +302,7 @@ VALUES (:proposed, :proponent, :title, :content, :votetype, :status, :due, :prop
func (m *MotionModel) CloseDecisions(ctx context.Context) ([]*Motion, error) { func (m *MotionModel) CloseDecisions(ctx context.Context) ([]*Motion, error) {
tx, err := m.DB.BeginTxx(ctx, nil) tx, err := m.DB.BeginTxx(ctx, nil)
if err != nil { if err != nil {
return nil, fmt.Errorf(errCouldNotStartTransaction, err) return nil, errCouldNotStartTransaction(err)
} }
defer func(tx *sqlx.Tx) { defer func(tx *sqlx.Tx) {
@ -327,7 +327,7 @@ WHERE decisions.status=0 AND :now > due`, struct{ Now time.Time }{Now: time.Now
for rows.Next() { for rows.Next() {
decision := &Motion{} decision := &Motion{}
if err = rows.StructScan(decision); err != nil { if err = rows.StructScan(decision); err != nil {
return nil, fmt.Errorf(errCouldNotScanResult, err) return nil, errCouldNotScanResult(err)
} }
if rows.Err() != nil { if rows.Err() != nil {
@ -350,7 +350,7 @@ WHERE decisions.status=0 AND :now > due`, struct{ Now time.Time }{Now: time.Now
} }
if err = tx.Commit(); err != nil { if err = tx.Commit(); err != nil {
return nil, fmt.Errorf(errCouldNotCommitTransaction, err) return nil, errCouldNotCommitTransaction(err)
} }
return results, nil return results, nil
@ -379,7 +379,7 @@ func closeDecision(ctx context.Context, tx *sqlx.Tx, d *Motion) (*Motion, error)
) )
if err != nil { if err != nil {
return nil, fmt.Errorf(errCouldNotExecuteQuery, err) return nil, errCouldNotExecuteQuery(err)
} }
affectedRows, err := result.RowsAffected() affectedRows, err := result.RowsAffected()
@ -408,7 +408,7 @@ WHERE due < ? AND status=? AND NOT EXISTS(SELECT * FROM votes WHERE decision = d
voteStatusPending, voteStatusPending,
voter.ID) voter.ID)
if err != nil { if err != nil {
return nil, fmt.Errorf(errCouldNotExecuteQuery, err) return nil, errCouldNotExecuteQuery(err)
} }
defer func(rows *sqlx.Rows) { defer func(rows *sqlx.Rows) {
@ -419,13 +419,13 @@ WHERE due < ? AND status=? AND NOT EXISTS(SELECT * FROM votes WHERE decision = d
for rows.Next() { for rows.Next() {
if err := rows.Err(); err != nil { if err := rows.Err(); err != nil {
return nil, fmt.Errorf(errCouldNotFetchRow, err) return nil, errCouldNotFetchRow(err)
} }
var motion Motion var motion Motion
if err := rows.StructScan(&motion); err != nil { if err := rows.StructScan(&motion); err != nil {
return nil, fmt.Errorf(errCouldNotScanResult, err) return nil, errCouldNotScanResult(err)
} }
result = append(result, &motion) result = append(result, &motion)
@ -590,11 +590,11 @@ func (m *MotionModel) List(ctx context.Context, options *MotionListOptions) ([]*
var decision Motion var decision Motion
if err = rows.Err(); err != nil { if err = rows.Err(); err != nil {
return nil, fmt.Errorf(errCouldNotFetchRow, err) return nil, errCouldNotFetchRow(err)
} }
if err = rows.StructScan(&decision); err != nil { if err = rows.StructScan(&decision); err != nil {
return nil, fmt.Errorf(errCouldNotScanResult, err) return nil, errCouldNotScanResult(err)
} }
motions = append(motions, &decision) motions = append(motions, &decision)
@ -633,7 +633,7 @@ GROUP BY v.decision, v.vote`,
rows, err := m.DB.QueryContext(ctx, query, args...) rows, err := m.DB.QueryContext(ctx, query, args...)
if err != nil { if err != nil {
return fmt.Errorf(errCouldNotExecuteQuery, err) return errCouldNotExecuteQuery(err)
} }
defer func(rows *sql.Rows) { defer func(rows *sql.Rows) {
@ -642,7 +642,7 @@ GROUP BY v.decision, v.vote`,
for rows.Next() { for rows.Next() {
if err = rows.Err(); err != nil { if err = rows.Err(); err != nil {
return fmt.Errorf(errCouldNotFetchRow, err) return errCouldNotFetchRow(err)
} }
var ( var (
@ -653,7 +653,7 @@ GROUP BY v.decision, v.vote`,
err = rows.Scan(&decisionID, &vote, &count) err = rows.Scan(&decisionID, &vote, &count)
if err != nil { if err != nil {
return fmt.Errorf(errCouldNotScanResult, err) return errCouldNotScanResult(err)
} }
switch { switch {
@ -861,7 +861,7 @@ ORDER BY voters.name`,
var vote Vote var vote Vote
if err := rows.StructScan(&vote); err != nil { if err := rows.StructScan(&vote); err != nil {
return fmt.Errorf(errCouldNotScanResult, err) return errCouldNotScanResult(err)
} }
md.Votes = append(md.Votes, &vote) md.Votes = append(md.Votes, &vote)
@ -893,7 +893,7 @@ func (m *MotionModel) Update(
) error { ) error {
tx, err := m.DB.BeginTxx(ctx, nil) tx, err := m.DB.BeginTxx(ctx, nil)
if err != nil { if err != nil {
return fmt.Errorf(errCouldNotStartTransaction, err) return errCouldNotStartTransaction(err)
} }
defer func(tx *sqlx.Tx) { defer func(tx *sqlx.Tx) {
@ -908,7 +908,7 @@ func (m *MotionModel) Update(
var motion Motion var motion Motion
if err := row.StructScan(&motion); err != nil { if err := row.StructScan(&motion); err != nil {
return fmt.Errorf(errCouldNotScanResult, err) return errCouldNotScanResult(err)
} }
updateFn(&motion) updateFn(&motion)
@ -932,7 +932,7 @@ WHERE id = :id`,
} }
if err := tx.Commit(); err != nil { if err := tx.Commit(); err != nil {
return fmt.Errorf(errCouldNotCommitTransaction, err) return errCouldNotCommitTransaction(err)
} }
return nil return nil
@ -950,7 +950,7 @@ type Vote struct {
func (m *MotionModel) UpdateVote(ctx context.Context, userID, motionID int64, performVoteFn func(v *Vote)) error { func (m *MotionModel) UpdateVote(ctx context.Context, userID, motionID int64, performVoteFn func(v *Vote)) error {
tx, err := m.DB.BeginTxx(ctx, nil) tx, err := m.DB.BeginTxx(ctx, nil)
if err != nil { if err != nil {
return fmt.Errorf(errCouldNotStartTransaction, err) return errCouldNotStartTransaction(err)
} }
defer func(tx *sqlx.Tx) { defer func(tx *sqlx.Tx) {
@ -959,7 +959,7 @@ func (m *MotionModel) UpdateVote(ctx context.Context, userID, motionID int64, pe
row := tx.QueryRowxContext(ctx, `SELECT * FROM votes WHERE voter=? AND decision=?`, userID, motionID) row := tx.QueryRowxContext(ctx, `SELECT * FROM votes WHERE voter=? AND decision=?`, userID, motionID)
if err := row.Err(); err != nil { if err := row.Err(); err != nil {
return fmt.Errorf(errCouldNotExecuteQuery, err) return errCouldNotExecuteQuery(err)
} }
vote := Vote{UserID: userID, MotionID: motionID} vote := Vote{UserID: userID, MotionID: motionID}
@ -986,7 +986,7 @@ WHERE decision = :decision
} }
if err := tx.Commit(); err != nil { if err := tx.Commit(); err != nil {
return fmt.Errorf(errCouldNotCommitTransaction, err) return errCouldNotCommitTransaction(err)
} }
return nil return nil

View file

@ -320,16 +320,16 @@ func (m *UserModel) Voters(ctx context.Context) ([]*User, error) {
return m.InRole(ctx, RoleVoter) return m.InRole(ctx, RoleVoter)
} }
func (m *UserModel) ByID(ctx context.Context, voterID int64) (*User, error) { func (m *UserModel) ByID(ctx context.Context, voterID int64, options ...UserListOption) (*User, error) {
row := m.DB.QueryRowxContext( row := m.DB.QueryRowxContext(
ctx, ctx,
`SELECT DISTINCT v.id, v.name `SELECT DISTINCT v.id, v.name, e.address AS reminder
FROM voters v FROM voters v
JOIN user_roles ur ON v.id = ur.voter_id LEFT OUTER JOIN emails e ON e.voter = v.id
WHERE v.id = ? WHERE v.id = ?
AND ur.role = ?`, AND e.reminder = TRUE
`,
voterID, voterID,
RoleVoter,
) )
if err := row.Err(); err != nil { if err := row.Err(); err != nil {
@ -342,6 +342,12 @@ WHERE v.id = ?
return nil, fmt.Errorf("could not scan row: %w", err) return nil, fmt.Errorf("could not scan row: %w", err)
} }
for _, option := range options {
if err := option(ctx, []*User{&user}); err != nil {
return nil, err
}
}
return &user, nil return &user, nil
} }
@ -350,7 +356,7 @@ type UserListOption func(context.Context, []*User) error
func (m *UserModel) List(ctx context.Context, options ...UserListOption) ([]*User, error) { func (m *UserModel) List(ctx context.Context, options ...UserListOption) ([]*User, error) {
rows, err := m.DB.QueryxContext(ctx, `SELECT id, name FROM voters ORDER BY name`) rows, err := m.DB.QueryxContext(ctx, `SELECT id, name FROM voters ORDER BY name`)
if err != nil { if err != nil {
return nil, fmt.Errorf(errCouldNotExecuteQuery, err) return nil, errCouldNotExecuteQuery(err)
} }
defer func(rows *sqlx.Rows) { defer func(rows *sqlx.Rows) {
@ -361,12 +367,12 @@ func (m *UserModel) List(ctx context.Context, options ...UserListOption) ([]*Use
for rows.Next() { for rows.Next() {
if err = rows.Err(); err != nil { if err = rows.Err(); err != nil {
return nil, fmt.Errorf(errCouldNotFetchRow, err) return nil, errCouldNotFetchRow(err)
} }
var user User var user User
if err = rows.StructScan(&user); err != nil { if err = rows.StructScan(&user); err != nil {
return nil, fmt.Errorf(errCouldNotScanResult, err) return nil, errCouldNotScanResult(err)
} }
users = append(users, &user) users = append(users, &user)
@ -397,37 +403,21 @@ func (m *UserModel) WithRolesOption() UserListOption {
userIDs, userIDs,
) )
if err != nil { if err != nil {
return fmt.Errorf(errCouldNotCreateInQuery, err) return errCouldNotCreateInQuery(err)
} }
rows, err := m.DB.QueryxContext(ctx, query, args...) rows, err := m.DB.QueryxContext(ctx, query, args...)
if err != nil { if err != nil {
return fmt.Errorf(errCouldNotExecuteQuery, err) return errCouldNotExecuteQuery(err)
} }
defer func(rows *sqlx.Rows) { defer func(rows *sqlx.Rows) {
_ = rows.Close() _ = rows.Close()
}(rows) }(rows)
roleMap := make(map[int64][]*Role, len(users)) roleMap, err := buildRoleMap(rows, len(users))
if err != nil {
for rows.Next() { return err
if err := rows.Err(); err != nil {
return fmt.Errorf(errCouldNotFetchRow, err)
}
var userID int64
var role Role
if err := rows.Scan(&userID, &role); err != nil {
return fmt.Errorf(errCouldNotScanResult, err)
}
if _, ok := roleMap[userID]; !ok {
roleMap[userID] = []*Role{&role}
} else {
roleMap[userID] = append(roleMap[userID], &role)
}
} }
for idx := range users { for idx := range users {
@ -444,6 +434,33 @@ func (m *UserModel) WithRolesOption() UserListOption {
} }
} }
func buildRoleMap(rows *sqlx.Rows, userCount int) (map[int64][]*Role, error) {
roleMap := make(map[int64][]*Role, userCount)
for rows.Next() {
if err := rows.Err(); err != nil {
return nil, errCouldNotFetchRow(err)
}
var (
userID int64
role Role
)
if err := rows.Scan(&userID, &role); err != nil {
return nil, errCouldNotScanResult(err)
}
if _, ok := roleMap[userID]; !ok {
roleMap[userID] = []*Role{&role}
} else {
roleMap[userID] = append(roleMap[userID], &role)
}
}
return roleMap, nil
}
func (m *UserModel) CanDeleteOption() UserListOption { func (m *UserModel) CanDeleteOption() UserListOption {
return func(ctx context.Context, users []*User) error { return func(ctx context.Context, users []*User) error {
if len(users) == 0 { if len(users) == 0 {
@ -465,12 +482,12 @@ WHERE id IN (?)
userIDs, userIDs,
) )
if err != nil { if err != nil {
return fmt.Errorf(errCouldNotCreateInQuery, err) return errCouldNotCreateInQuery(err)
} }
rows, err := m.DB.QueryxContext(ctx, query, args...) rows, err := m.DB.QueryxContext(ctx, query, args...)
if err != nil { if err != nil {
return fmt.Errorf(errCouldNotExecuteQuery, err) return errCouldNotExecuteQuery(err)
} }
defer func(rows *sqlx.Rows) { defer func(rows *sqlx.Rows) {
@ -479,13 +496,13 @@ WHERE id IN (?)
for rows.Next() { for rows.Next() {
if err := rows.Err(); err != nil { if err := rows.Err(); err != nil {
return fmt.Errorf(errCouldNotFetchRow, err) return errCouldNotFetchRow(err)
} }
var userID int64 var userID int64
if err := rows.Scan(&userID); err != nil { if err := rows.Scan(&userID); err != nil {
return fmt.Errorf(errCouldNotScanResult, err) return errCouldNotScanResult(err)
} }
for idx := range users { for idx := range users {
@ -498,3 +515,40 @@ WHERE id IN (?)
return nil return nil
} }
} }
func (m *UserModel) DeleteUser(ctx context.Context, userToDelete, admin *User, reasoning string) error {
userInfo := struct {
Name string `json:"user"`
Address string `json:"address"`
}{Name: userToDelete.Name, Address: userToDelete.Reminder}
details := struct {
User any `json:"user"`
}{User: userInfo}
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, `DELETE FROM emails WHERE voter=?`, userToDelete.ID); err != nil {
return errCouldNotExecuteQuery(err)
}
if _, err = tx.ExecContext(ctx, `DELETE FROM voters WHERE id=?`, userToDelete.ID); err != nil {
return errCouldNotExecuteQuery(err)
}
if err = AuditLog(ctx, tx, admin, AuditDeleteUser, reasoning, details); err != nil {
return err
}
if err = tx.Commit(); err != nil {
return errCouldNotCommitTransaction(err)
}
return nil
}

View file

@ -0,0 +1,30 @@
{{ define "title" }}Delete User {{ .Form.User.Name }}{{ end }}
{{ define "main" }}
{{ $form := .Form }}
{{ $user := .User }}
<div class="ui form segment">
<div class="ui negative message">
<div class="header">
Withdraw motion?
</div>
<p>Do you want to delete user <strong>{{ .Form.User.Name }}</strong>?</p>
</div>
<form action="/users/{{ .Form.User.ID }}/delete" method="post">
<input type="hidden" name="csrf_token" value="{{ .CSRFToken }}">
<div class="ui form{{ if .Form.FieldErrors }} error{{ end }}">
<div class="required field{{ if .Form.FieldErrors.reasoning }} error{{ end }}">
<label for="reasoning">Reasoning for the deletion</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 negative labeled icon button" type="submit">
<i class="trash icon"></i> Delete user
</button>
<a href="/motions/" class="ui button">Cancel</a>
</div>
</form>
</div>
{{ end }}