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:
parent
db52f88e25
commit
5efc57d2c3
11 changed files with 385 additions and 70 deletions
|
@ -579,10 +579,8 @@ func (app *application) proxyVoteSubmit(w http.ResponseWriter, r *http.Request)
|
|||
return
|
||||
}
|
||||
|
||||
voter, err := app.users.ByID(r.Context(), form.Voter.ID)
|
||||
if err != nil {
|
||||
app.serverError(w, err)
|
||||
|
||||
voter := app.getVoter(w, r, form.Voter.ID)
|
||||
if voter == nil {
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -637,6 +635,7 @@ func (app *application) editUserForm(_ http.ResponseWriter, _ *http.Request) {
|
|||
// TODO: implement editUserForm
|
||||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (app *application) editUserSubmit(_ http.ResponseWriter, _ *http.Request) {
|
||||
// TODO: implement editUserSubmit
|
||||
panic("not implemented")
|
||||
|
@ -652,14 +651,77 @@ func (app *application) userAddEmailSubmit(_ http.ResponseWriter, _ *http.Reques
|
|||
panic("not implemented")
|
||||
}
|
||||
|
||||
func (app *application) deleteUserForm(_ http.ResponseWriter, _ *http.Request) {
|
||||
// TODO: implement deleteUserForm
|
||||
panic("not implemented")
|
||||
func (app *application) deleteUserForm(w http.ResponseWriter, r *http.Request) {
|
||||
userToDelete := app.userFromRequestParam(w, r, httprouter.ParamsFromContext(r.Context()), app.users.CanDeleteOption())
|
||||
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) {
|
||||
// TODO: implement deleteUserSubmit
|
||||
panic("not implemented")
|
||||
func (app *application) deleteUserSubmit(w http.ResponseWriter, r *http.Request) {
|
||||
userToDelete := app.userFromRequestParam(w, r, httprouter.ParamsFromContext(r.Context()), app.users.CanDeleteOption())
|
||||
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) {
|
||||
|
|
|
@ -28,6 +28,7 @@ import (
|
|||
"net/http"
|
||||
"path/filepath"
|
||||
"runtime/debug"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/Masterminds/sprig/v3"
|
||||
|
@ -196,6 +197,32 @@ func (app *application) motionFromRequestParam(
|
|||
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 {
|
||||
choiceParam := params.ByName("choice")
|
||||
|
||||
|
|
|
@ -209,6 +209,31 @@ func (app *application) startHTTPSServer(config *Config) error {
|
|||
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) {
|
||||
redirect := &http.Server{
|
||||
Addr: config.HTTPAddress,
|
||||
|
|
|
@ -119,3 +119,22 @@ func (f *ProxyVoteForm) Validate() {
|
|||
func (f *ProxyVoteForm) Normalize() {
|
||||
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)
|
||||
}
|
||||
|
|
3
internal/migrations/2022060101_add_audit_table.down.sql
Normal file
3
internal/migrations/2022060101_add_audit_table.down.sql
Normal file
|
@ -0,0 +1,3 @@
|
|||
-- add an audit table to track changes to users
|
||||
DROP INDEX audit_change_idx;
|
||||
DROP TABLE audit;
|
18
internal/migrations/2022060101_add_audit_table.up.sql
Normal file
18
internal/migrations/2022060101_add_audit_table.up.sql
Normal 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
67
internal/models/audit.go
Normal 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
|
||||
}
|
|
@ -49,11 +49,21 @@ func parseSqlite3TimeStamp(timeStamp string) (*time.Time, error) {
|
|||
return nil, fmt.Errorf("could not parse timestamp: %w", err)
|
||||
}
|
||||
|
||||
const (
|
||||
errCouldNotExecuteQuery = "could not execute query: %w"
|
||||
errCouldNotFetchRow = "could not fetch row: %w"
|
||||
errCouldNotScanResult = "could not scan result: %w"
|
||||
errCouldNotStartTransaction = "could not start transaction: %w"
|
||||
errCouldNotCommitTransaction = "could not commit transaction: %w"
|
||||
errCouldNotCreateInQuery = "could not create query with IN clause: %w"
|
||||
)
|
||||
func errCouldNotExecuteQuery(err error) error {
|
||||
return fmt.Errorf("could not execute query: %w", err)
|
||||
}
|
||||
func errCouldNotFetchRow(err error) error {
|
||||
return fmt.Errorf("could not fetch row: %w", err)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
|
|
@ -302,7 +302,7 @@ VALUES (:proposed, :proponent, :title, :content, :votetype, :status, :due, :prop
|
|||
func (m *MotionModel) CloseDecisions(ctx context.Context) ([]*Motion, error) {
|
||||
tx, err := m.DB.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(errCouldNotStartTransaction, err)
|
||||
return nil, errCouldNotStartTransaction(err)
|
||||
}
|
||||
|
||||
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() {
|
||||
decision := &Motion{}
|
||||
if err = rows.StructScan(decision); err != nil {
|
||||
return nil, fmt.Errorf(errCouldNotScanResult, err)
|
||||
return nil, errCouldNotScanResult(err)
|
||||
}
|
||||
|
||||
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 {
|
||||
return nil, fmt.Errorf(errCouldNotCommitTransaction, err)
|
||||
return nil, errCouldNotCommitTransaction(err)
|
||||
}
|
||||
|
||||
return results, nil
|
||||
|
@ -379,7 +379,7 @@ func closeDecision(ctx context.Context, tx *sqlx.Tx, d *Motion) (*Motion, error)
|
|||
)
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(errCouldNotExecuteQuery, err)
|
||||
return nil, errCouldNotExecuteQuery(err)
|
||||
}
|
||||
|
||||
affectedRows, err := result.RowsAffected()
|
||||
|
@ -408,7 +408,7 @@ WHERE due < ? AND status=? AND NOT EXISTS(SELECT * FROM votes WHERE decision = d
|
|||
voteStatusPending,
|
||||
voter.ID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(errCouldNotExecuteQuery, err)
|
||||
return nil, errCouldNotExecuteQuery(err)
|
||||
}
|
||||
|
||||
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() {
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf(errCouldNotFetchRow, err)
|
||||
return nil, errCouldNotFetchRow(err)
|
||||
}
|
||||
|
||||
var motion Motion
|
||||
|
||||
if err := rows.StructScan(&motion); err != nil {
|
||||
return nil, fmt.Errorf(errCouldNotScanResult, err)
|
||||
return nil, errCouldNotScanResult(err)
|
||||
}
|
||||
|
||||
result = append(result, &motion)
|
||||
|
@ -590,11 +590,11 @@ func (m *MotionModel) List(ctx context.Context, options *MotionListOptions) ([]*
|
|||
var decision Motion
|
||||
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf(errCouldNotFetchRow, err)
|
||||
return nil, errCouldNotFetchRow(err)
|
||||
}
|
||||
|
||||
if err = rows.StructScan(&decision); err != nil {
|
||||
return nil, fmt.Errorf(errCouldNotScanResult, err)
|
||||
return nil, errCouldNotScanResult(err)
|
||||
}
|
||||
|
||||
motions = append(motions, &decision)
|
||||
|
@ -633,7 +633,7 @@ GROUP BY v.decision, v.vote`,
|
|||
|
||||
rows, err := m.DB.QueryContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf(errCouldNotExecuteQuery, err)
|
||||
return errCouldNotExecuteQuery(err)
|
||||
}
|
||||
|
||||
defer func(rows *sql.Rows) {
|
||||
|
@ -642,7 +642,7 @@ GROUP BY v.decision, v.vote`,
|
|||
|
||||
for rows.Next() {
|
||||
if err = rows.Err(); err != nil {
|
||||
return fmt.Errorf(errCouldNotFetchRow, err)
|
||||
return errCouldNotFetchRow(err)
|
||||
}
|
||||
|
||||
var (
|
||||
|
@ -653,7 +653,7 @@ GROUP BY v.decision, v.vote`,
|
|||
|
||||
err = rows.Scan(&decisionID, &vote, &count)
|
||||
if err != nil {
|
||||
return fmt.Errorf(errCouldNotScanResult, err)
|
||||
return errCouldNotScanResult(err)
|
||||
}
|
||||
|
||||
switch {
|
||||
|
@ -861,7 +861,7 @@ ORDER BY voters.name`,
|
|||
var vote Vote
|
||||
|
||||
if err := rows.StructScan(&vote); err != nil {
|
||||
return fmt.Errorf(errCouldNotScanResult, err)
|
||||
return errCouldNotScanResult(err)
|
||||
}
|
||||
|
||||
md.Votes = append(md.Votes, &vote)
|
||||
|
@ -893,7 +893,7 @@ func (m *MotionModel) Update(
|
|||
) error {
|
||||
tx, err := m.DB.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf(errCouldNotStartTransaction, err)
|
||||
return errCouldNotStartTransaction(err)
|
||||
}
|
||||
|
||||
defer func(tx *sqlx.Tx) {
|
||||
|
@ -908,7 +908,7 @@ func (m *MotionModel) Update(
|
|||
var motion Motion
|
||||
|
||||
if err := row.StructScan(&motion); err != nil {
|
||||
return fmt.Errorf(errCouldNotScanResult, err)
|
||||
return errCouldNotScanResult(err)
|
||||
}
|
||||
|
||||
updateFn(&motion)
|
||||
|
@ -932,7 +932,7 @@ WHERE id = :id`,
|
|||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf(errCouldNotCommitTransaction, err)
|
||||
return errCouldNotCommitTransaction(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
@ -950,7 +950,7 @@ type Vote struct {
|
|||
func (m *MotionModel) UpdateVote(ctx context.Context, userID, motionID int64, performVoteFn func(v *Vote)) error {
|
||||
tx, err := m.DB.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return fmt.Errorf(errCouldNotStartTransaction, err)
|
||||
return errCouldNotStartTransaction(err)
|
||||
}
|
||||
|
||||
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)
|
||||
if err := row.Err(); err != nil {
|
||||
return fmt.Errorf(errCouldNotExecuteQuery, err)
|
||||
return errCouldNotExecuteQuery(err)
|
||||
}
|
||||
|
||||
vote := Vote{UserID: userID, MotionID: motionID}
|
||||
|
@ -986,7 +986,7 @@ WHERE decision = :decision
|
|||
}
|
||||
|
||||
if err := tx.Commit(); err != nil {
|
||||
return fmt.Errorf(errCouldNotCommitTransaction, err)
|
||||
return errCouldNotCommitTransaction(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
@ -320,16 +320,16 @@ func (m *UserModel) Voters(ctx context.Context) ([]*User, error) {
|
|||
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(
|
||||
ctx,
|
||||
`SELECT DISTINCT v.id, v.name
|
||||
`SELECT DISTINCT v.id, v.name, e.address AS reminder
|
||||
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 = ?
|
||||
AND ur.role = ?`,
|
||||
AND e.reminder = TRUE
|
||||
`,
|
||||
voterID,
|
||||
RoleVoter,
|
||||
)
|
||||
|
||||
if err := row.Err(); err != nil {
|
||||
|
@ -342,6 +342,12 @@ WHERE v.id = ?
|
|||
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
|
||||
}
|
||||
|
||||
|
@ -350,7 +356,7 @@ type UserListOption func(context.Context, []*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`)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf(errCouldNotExecuteQuery, err)
|
||||
return nil, errCouldNotExecuteQuery(err)
|
||||
}
|
||||
|
||||
defer func(rows *sqlx.Rows) {
|
||||
|
@ -361,12 +367,12 @@ func (m *UserModel) List(ctx context.Context, options ...UserListOption) ([]*Use
|
|||
|
||||
for rows.Next() {
|
||||
if err = rows.Err(); err != nil {
|
||||
return nil, fmt.Errorf(errCouldNotFetchRow, err)
|
||||
return nil, errCouldNotFetchRow(err)
|
||||
}
|
||||
|
||||
var user User
|
||||
if err = rows.StructScan(&user); err != nil {
|
||||
return nil, fmt.Errorf(errCouldNotScanResult, err)
|
||||
return nil, errCouldNotScanResult(err)
|
||||
}
|
||||
|
||||
users = append(users, &user)
|
||||
|
@ -397,37 +403,21 @@ func (m *UserModel) WithRolesOption() UserListOption {
|
|||
userIDs,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf(errCouldNotCreateInQuery, err)
|
||||
return errCouldNotCreateInQuery(err)
|
||||
}
|
||||
|
||||
rows, err := m.DB.QueryxContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf(errCouldNotExecuteQuery, err)
|
||||
return errCouldNotExecuteQuery(err)
|
||||
}
|
||||
|
||||
defer func(rows *sqlx.Rows) {
|
||||
_ = rows.Close()
|
||||
}(rows)
|
||||
|
||||
roleMap := make(map[int64][]*Role, len(users))
|
||||
|
||||
for rows.Next() {
|
||||
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)
|
||||
}
|
||||
roleMap, err := buildRoleMap(rows, len(users))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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 {
|
||||
return func(ctx context.Context, users []*User) error {
|
||||
if len(users) == 0 {
|
||||
|
@ -465,12 +482,12 @@ WHERE id IN (?)
|
|||
userIDs,
|
||||
)
|
||||
if err != nil {
|
||||
return fmt.Errorf(errCouldNotCreateInQuery, err)
|
||||
return errCouldNotCreateInQuery(err)
|
||||
}
|
||||
|
||||
rows, err := m.DB.QueryxContext(ctx, query, args...)
|
||||
if err != nil {
|
||||
return fmt.Errorf(errCouldNotExecuteQuery, err)
|
||||
return errCouldNotExecuteQuery(err)
|
||||
}
|
||||
|
||||
defer func(rows *sqlx.Rows) {
|
||||
|
@ -479,13 +496,13 @@ WHERE id IN (?)
|
|||
|
||||
for rows.Next() {
|
||||
if err := rows.Err(); err != nil {
|
||||
return fmt.Errorf(errCouldNotFetchRow, err)
|
||||
return errCouldNotFetchRow(err)
|
||||
}
|
||||
|
||||
var userID int64
|
||||
|
||||
if err := rows.Scan(&userID); err != nil {
|
||||
return fmt.Errorf(errCouldNotScanResult, err)
|
||||
return errCouldNotScanResult(err)
|
||||
}
|
||||
|
||||
for idx := range users {
|
||||
|
@ -498,3 +515,40 @@ WHERE id IN (?)
|
|||
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
|
||||
}
|
||||
|
|
30
ui/html/pages/delete_user.html
Normal file
30
ui/html/pages/delete_user.html
Normal 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 }}
|
Loading…
Reference in a new issue