Merge pull request #203 from writefreely/T319-admin-delete-acct

T319 admin delete acct
This commit is contained in:
Matt Baer 2021-04-28 09:46:56 -04:00 committed by GitHub
commit 4e0912b32a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 143 additions and 16 deletions

View File

@ -189,6 +189,7 @@ func handleViewAdminUsers(app *App, u *User, w http.ResponseWriter, r *http.Requ
*AdminPage
Config config.AppCfg
Message string
Flashes []string
Users *[]User
CurPage int
@ -201,6 +202,7 @@ func handleViewAdminUsers(app *App, u *User, w http.ResponseWriter, r *http.Requ
Message: r.FormValue("m"),
}
p.Flashes, _ = getSessionFlashes(app, w, r, nil)
p.TotalUsers = app.db.GetAllUsersCount()
ttlPages := p.TotalUsers / adminUsersPerPage
p.TotalPages = []int{}
@ -312,6 +314,37 @@ func handleViewAdminUser(app *App, u *User, w http.ResponseWriter, r *http.Reque
return nil
}
func handleAdminDeleteUser(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
if !u.IsAdmin() {
return impart.HTTPError{http.StatusForbidden, "Administrator privileges required for this action"}
}
vars := mux.Vars(r)
username := vars["username"]
confirmUsername := r.PostFormValue("confirm-username")
if confirmUsername != username {
return impart.HTTPError{http.StatusBadRequest, "Username was not confirmed"}
}
user, err := app.db.GetUserForAuth(username)
if err == ErrUserNotFound {
return impart.HTTPError{http.StatusNotFound, fmt.Sprintf("User '%s' was not found", username)}
} else if err != nil {
log.Error("get user for deletion: %v", err)
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not get user with username '%s': %v", username, err)}
}
err = app.db.DeleteAccount(user.ID)
if err != nil {
log.Error("delete user %s: %v", user.Username, err)
return impart.HTTPError{http.StatusInternalServerError, fmt.Sprintf("Could not delete user account for '%s': %v", username, err)}
}
_ = addSessionFlash(app, w, r, fmt.Sprintf("User \"%s\" was deleted successfully.", username), nil)
return impart.HTTPError{http.StatusFound, "/admin/users"}
}
func handleAdminToggleUserStatus(app *App, u *User, w http.ResponseWriter, r *http.Request) error {
vars := mux.Vars(r)
username := vars["username"]

View File

@ -1044,6 +1044,19 @@ li {
background-color: #dff0d8;
border-color: #d6e9c6;
}
&.danger {
border-color: #856404;
background-color: white;
h3 {
margin: 0 0 0.5em 0;
font-size: 1em;
font-weight: bold;
color: black !important;
}
h3 + p, button {
font-size: 0.86em;
}
}
p {
margin: 0;

View File

@ -340,6 +340,15 @@ body#pad {
}
}
.body {
line-height: 1.5;
input[type=text].confirm {
width: 100%;
box-sizing: border-box;
}
}
.short {
text-align: center;
}

View File

@ -166,6 +166,7 @@ func InitRoutes(apper Apper, r *mux.Router) *mux.Router {
write.HandleFunc("/admin/settings", handler.Admin(handleViewAdminSettings)).Methods("GET")
write.HandleFunc("/admin/users", handler.Admin(handleViewAdminUsers)).Methods("GET")
write.HandleFunc("/admin/user/{username}", handler.Admin(handleViewAdminUser)).Methods("GET")
write.HandleFunc("/admin/user/{username}/delete", handler.Admin(handleAdminDeleteUser)).Methods("POST")
write.HandleFunc("/admin/user/{username}/status", handler.Admin(handleAdminToggleUserStatus)).Methods("POST")
write.HandleFunc("/admin/user/{username}/passphrase", handler.Admin(handleAdminResetUserPass)).Methods("POST")
write.HandleFunc("/admin/pages", handler.Admin(handleViewAdminPages)).Methods("GET")

24
static/js/modals.js Normal file
View File

@ -0,0 +1,24 @@
/*
* Copyright © 2016-2021 A Bunch Tell LLC.
*
* This file is part of WriteFreely.
*
* WriteFreely is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License, included
* in the LICENSE file in this source code package.
*/
function showModal(id) {
document.getElementById('overlay').style.display = 'block';
document.getElementById('modal-'+id).style.display = 'block';
}
var closeModals = function(e) {
e.preventDefault();
document.getElementById('overlay').style.display = 'none';
var modals = document.querySelectorAll('.modal');
for (var i=0; i<modals.length; i++) {
modals[i].style.display = 'none';
}
};
H.getEl('overlay').on('click', closeModals);

View File

@ -4,6 +4,12 @@
<div class="snug content-container">
{{template "admin-header" .}}
<!-- TODO: if other use for flashes use patern like account_import.go -->
{{if .Flashes}}
<p class="alert success">
{{range .Flashes}}{{.}}{{end}}
</p>
{{end}}
<div class="row admin-actions" style="justify-content: space-between;">
<span style="font-style: italic; font-size: 1.2em">{{.TotalUsers}} {{pluralize "user" "users" .TotalUsers}}</span>
<a class="btn cta" href="/me/invites">+ Invite people</a>

View File

@ -32,8 +32,13 @@ input.copy-text {
width: 100%;
box-sizing: border-box;
}
.modal {
position: fixed;
}
</style>
<div class="snug content-container">
<div id="overlay"></div>
{{template "admin-header" .}}
<h2 id="posts-header">{{.User.Username}}</h2>
@ -139,9 +144,60 @@ input.copy-text {
{{end}}
</table>
{{end}}
{{ if not .User.IsAdmin }}
<h2>Incinerator</h2>
<div class="alert danger">
<div class="row">
<div>
<h3>Delete this user</h3>
<p>Permanently erase all user data, with no way to recover it.</p>
</div>
<button class="cta danger" onclick="prepareDeleteUser()">Delete this user...</button>
</div>
</div>
{{end}}
</div>
<div id="modal-delete-user" class="modal">
<h2>Are you sure?</h2>
<div class="body">
<p style="text-align:left">This action <strong>cannot</strong> be undone. It will permanently erase all traces of this user, <strong>{{.User.Username}}</strong>, including their account information, blogs, and posts.</p>
<p>Please type <strong>{{.User.Username}}</strong> to confirm.</p>
<ul id="delete-errors" class="errors"></ul>
<form action="/admin/user/{{.User.Username}}/delete" method="post" onsubmit="confirmDeletion()">
<input id="confirm-text" placeholder="{{.User.Username}}" type="text" class="confirm boxy" name="confirm-username" style="margin-top: 0.5em;" />
<div style="text-align:right; margin-top: 1em;">
<a id="cancel-delete" style="margin-right:2em" href="#">Cancel</a>
<input class="danger" type="submit" id="confirm-delete" value="Delete this user" disabled />
</div>
</div>
</div>
<script src="/js/h.js"></script>
<script src="/js/modals.js"></script>
<script type="text/javascript">
H.getEl('cancel-delete').on('click', closeModals);
let $confirmDelBtn = document.getElementById('confirm-delete');
let $confirmText = document.getElementById('confirm-text')
$confirmText.addEventListener('input', function() {
$confirmDelBtn.disabled = this.value !== '{{.User.Username}}'
});
function prepareDeleteUser() {
$confirmText.value = ''
showModal('delete-user')
$confirmText.focus()
}
function confirmDeletion() {
$confirmDelBtn.disabled = true
$confirmDelBtn.value = 'Deleting...'
}
function confirmSilence() {
return confirm("Silence this user? They'll still be able to log in and access their posts, but no one else will be able to see them anymore. You can reverse this decision at any time.");
}

View File

@ -193,25 +193,10 @@ textarea.section.norm {
</div>
<script src="/js/h.js"></script>
<script src="/js/modals.js"></script>
<script src="/js/ace.js" type="text/javascript" charset="utf-8"></script>
<script>
// Begin shared modal code
function showModal(id) {
document.getElementById('overlay').style.display = 'block';
document.getElementById('modal-'+id).style.display = 'block';
}
var closeModals = function(e) {
e.preventDefault();
document.getElementById('overlay').style.display = 'none';
var modals = document.querySelectorAll('.modal');
for (var i=0; i<modals.length; i++) {
modals[i].style.display = 'none';
}
};
H.getEl('overlay').on('click', closeModals);
H.getEl('cancel-delete').on('click', closeModals);
// end
var deleteBlog = function(e) {
if (document.getElementById('confirm-text').value != '{{.Alias}}') {
document.getElementById('delete-errors').innerHTML = '<li class="urgent">Enter <strong>{{.Alias}}</strong> in the box below.</li>';